add service page

fdroid
Kherel 2020-12-01 20:08:19 +01:00
parent cd02c75e2f
commit 90df52e895
17 changed files with 470 additions and 173 deletions

Binary file not shown.

View File

@ -0,0 +1,21 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:selfprivacy/logic/cubit/services/services_cubit.dart';
class BlocAndProviderConfig extends StatelessWidget {
const BlocAndProviderConfig({Key key, this.child}) : super(key: key);
final Widget child;
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
BlocProvider<ServicesCubit>(
create: (BuildContext context) => ServicesCubit(),
),
],
child: child,
);
}
}

View File

@ -28,4 +28,17 @@ class BrandColors {
static const textColor2 = gray1;
static get navBackground => white.withOpacity(0.8);
static const List<Color> uninitializedGradientColors = [
Color(0xFF555555),
Color(0xFFABABAB),
];
static const List<Color> stableGradientColors = [
Color(0xFF093CEF),
Color(0xFF14A1CB),
];
static const List<Color> warningGradientColors = [
Color(0xFFEF4E09),
Color(0xFFEFD135),
];
}

View File

@ -4,7 +4,7 @@ import 'package:selfprivacy/config/text_themes.dart';
import 'brand_colors.dart';
var theme = ThemeData(
final theme = ThemeData(
primaryColor: BrandColors.primary,
brightness: Brightness.light,
scaffoldBackgroundColor: BrandColors.scaffoldBackground,
@ -13,7 +13,9 @@ var theme = ThemeData(
headline1: headline1Style,
headline2: headline2Style,
caption: captionStyle,
bodyText1: bodyText1Style,
bodyText1: body1Style,
),
),
);
final brandPagePadding = EdgeInsets.symmetric(horizontal: 15, vertical: 30);

View File

@ -29,7 +29,7 @@ final captionStyle = GoogleFonts.inter(
color: BrandColors.headlineColor,
);
final bodyText1Style = defaultTextStyle;
final body2TextStyle = defaultTextStyle.copyWith(
final body1Style = defaultTextStyle;
final body2Style = defaultTextStyle.copyWith(
color: BrandColors.textColor2,
);

View File

@ -0,0 +1,24 @@
import 'package:bloc/bloc.dart';
import 'package:meta/meta.dart';
import 'package:selfprivacy/logic/models/service.dart';
export 'package:provider/provider.dart';
part 'services_state.dart';
class ServicesCubit extends Cubit<ServicesState> {
ServicesCubit() : super(ServicesState(all));
void connect(Service service) {
var newState = state.updateElement(service, ServiceStateType.stable);
emit(newState);
}
}
final all = ServiceTypes.values
.map(
(type) => Service(
state: ServiceStateType.uninitialized,
type: type,
),
)
.toList();

View File

@ -0,0 +1,23 @@
part of 'services_cubit.dart';
@immutable
class ServicesState {
ServicesState(this.all);
final List<Service> all;
ServicesState updateElement(Service service, ServiceStateType newState) {
var newList = [...all];
var index = newList.indexOf(service);
newList[index] = service.updateState(newState);
return ServicesState(newList);
}
List<Service> get connected => all
.where((service) => service.state != ServiceStateType.uninitialized)
.toList();
List<Service> get uninitialized => all
.where((service) => service.state == ServiceStateType.uninitialized)
.toList();
}

View File

@ -0,0 +1,26 @@
import 'package:equatable/equatable.dart';
enum ServiceStateType { uninitialized, stable, warning }
enum ServiceTypes {
messanger,
mail,
passwordManager,
backup,
github,
cloud,
}
class Service extends Equatable {
const Service({this.state, this.type});
final ServiceStateType state;
final ServiceTypes type;
Service updateState(ServiceStateType newState) => Service(
state: newState,
type: type,
);
@override
List<Object> get props => [state, type];
}

View File

@ -3,6 +3,7 @@ import 'package:flutter/services.dart';
import 'package:selfprivacy/ui/pages/onboarding/onboarding.dart';
import 'package:easy_localization/easy_localization.dart';
import 'config/bloc_config.dart';
import 'config/brand_theme.dart';
import 'config/localization.dart';
@ -11,7 +12,9 @@ void main() {
runApp(
Localization(
child: MyApp(),
child: BlocAndProviderConfig(
child: MyApp(),
),
),
);
}

View File

@ -23,32 +23,26 @@ class BrandIcons {
static const IconData connection =
IconData(0xe800, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData envelope =
IconData(0xe801, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData document =
IconData(0xe802, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData envelope =
static const IconData key =
IconData(0xe803, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData github =
static const IconData save =
IconData(0xe804, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData globe =
IconData(0xe805, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData help =
IconData(0xe806, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData key =
IconData(0xe807, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData messenger =
IconData(0xe809, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData refresh =
IconData(0xe80a, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData save =
IconData(0xe80b, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData settings =
IconData(0xe80d, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData share =
IconData(0xe80e, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData triangle =
IconData(0xe80f, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData upload =
IconData(0xe810, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData server =
IconData(0xe811, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData box =
@ -57,4 +51,10 @@ class BrandIcons {
IconData(0xe813, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData users =
IconData(0xe814, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData messanger =
IconData(0xe815, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData upload =
IconData(0xe816, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData github =
IconData(0xe817, fontFamily: _kFontFam, fontPackage: _kFontPkg);
}

View File

@ -0,0 +1,35 @@
import 'package:flutter/material.dart';
import 'package:selfprivacy/config/brand_colors.dart';
import 'package:selfprivacy/logic/models/service.dart';
class IconStatusMaks extends StatelessWidget {
IconStatusMaks({this.child, this.status});
final Icon child;
final ServiceStateType status;
@override
Widget build(BuildContext context) {
List<Color> colors;
switch (status) {
case ServiceStateType.uninitialized:
colors = BrandColors.uninitializedGradientColors;
break;
case ServiceStateType.stable:
colors = BrandColors.stableGradientColors;
break;
case ServiceStateType.warning:
colors = BrandColors.warningGradientColors;
break;
}
return ShaderMask(
shaderCallback: (bounds) => LinearGradient(
begin: Alignment(-1, -0.8),
end: Alignment(0.9, 0.9),
colors: colors,
tileMode: TileMode.mirror,
).createShader(bounds),
child: child,
);
}
}

View File

@ -2,6 +2,7 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:selfprivacy/ui/components/brand_tab_bar/brand_tab_bar.dart';
import 'package:selfprivacy/ui/pages/servers/servers.dart';
import 'package:selfprivacy/ui/pages/services/services.dart';
class RootPage extends StatefulWidget {
const RootPage({Key key}) : super(key: key);
@ -34,7 +35,7 @@ class _RootPageState extends State<RootPage>
controller: tabController,
children: [
ServersPage(),
Text('services'),
ServicesPage(),
Text('users'),
Text('more'),
],

View File

@ -1,11 +1,13 @@
import 'package:flutter/material.dart';
import 'package:selfprivacy/config/brand_colors.dart';
import 'package:selfprivacy/config/brand_theme.dart';
import 'package:selfprivacy/config/text_themes.dart';
import 'package:selfprivacy/ui/components/brand_button/brand_button.dart';
import 'package:selfprivacy/ui/components/brand_card/brand_card.dart';
import 'package:selfprivacy/ui/components/brand_modal_sheet/brand_modal_sheet.dart';
import 'package:selfprivacy/ui/components/brand_span_button/brand_span_button.dart';
import 'package:selfprivacy/utils/extensions/text_extension.dart';
export 'package:bloc/bloc.dart';
class ServersPage extends StatelessWidget {
const ServersPage({Key key}) : super(key: key);
@ -13,70 +15,69 @@ class ServersPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
child: ListView(
padding: EdgeInsets.symmetric(horizontal: 15, vertical: 30),
children: [
Text('Начало').caption,
Text('SelfPrivacy').h1,
SizedBox(
height: 10,
body: ListView(
padding: brandPagePadding,
children: [
Text('Начало').caption,
Text('SelfPrivacy').h1,
SizedBox(
height: 10,
),
RichText(
text: TextSpan(
children: [
TextSpan(
text:
'Для устойчивости и приватности требует много учёток. Полная инструкция на ',
style: body2Style,
),
BrandSpanButton.link(
text: 'selfprivacy.org/start',
urlString: 'https://selfprivacy.org/start',
),
],
),
RichText(
text: TextSpan(
children: [
TextSpan(
text:
'Для устойчивости и приватности требует много учёток. Полная инструкция на ',
style: body2TextStyle,
),
BrandSpanButton.link(
text: 'selfprivacy.org/start',
urlString: 'https://selfprivacy.org/start',
),
],
),
),
SizedBox(height: 50),
BrandCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.asset('assets/images/logos/hetzner.png'),
SizedBox(height: 10),
Text('1. Подключите сервер Hetzner').h2,
SizedBox(height: 10),
Text('Здесь будут жить наши данные и SelfPrivacy-сервисы')
.body2,
_MockForm(
hintText: 'Hetzner API Token',
),
SizedBox(height: 20),
BrandButton.text(
onPressed: () => showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (BuildContext context) {
return BrandModalSheet(
child: Column(
children: [
Text('Как получить Hetzner API Token').h2,
SizedBox(height: 20),
RichText(
text: TextSpan(
children: [
TextSpan(
text: '1 Переходим по ссылке ',
style: bodyText1Style,
),
BrandSpanButton.link(
text: 'hetzner.com/sdfsdfsdfsdf',
urlString:
'https://hetzner.com/sdfsdfsdfsdf',
),
TextSpan(
text: '''
),
SizedBox(height: 50),
BrandCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.asset('assets/images/logos/hetzner.png'),
SizedBox(height: 10),
Text('1. Подключите сервер Hetzner').h2,
SizedBox(height: 10),
Text('Здесь будут жить наши данные и SelfPrivacy-сервисы')
.body2,
_MockForm(
hintText: 'Hetzner API Token',
),
SizedBox(height: 20),
BrandButton.text(
onPressed: () => showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (BuildContext context) {
return BrandModalSheet(
child: Column(
children: [
Text('Как получить Hetzner API Token').h2,
SizedBox(height: 20),
RichText(
text: TextSpan(
children: [
TextSpan(
text: '1 Переходим по ссылке ',
style: body1Style,
),
BrandSpanButton.link(
text: 'hetzner.com/sdfsdfsdfsdf',
urlString:
'https://hetzner.com/sdfsdfsdfsdf',
),
TextSpan(
text: '''
2 Заходим в созданный нами проект. Если такового - нет, значит создаём.
@ -88,104 +89,103 @@ class ServersPage extends StatelessWidget {
6 В поле Description, даём нашему токену название (это может быть любое название, которые вам нравиться. Сути оно не меняет.
''',
style: bodyText1Style,
),
],
),
''',
style: body1Style,
),
],
),
],
),
);
},
),
title: 'Как получить API Token',
),
],
),
),
BrandCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.asset('assets/images/logos/namecheap.png'),
SizedBox(height: 10),
Text('2. Настройте домен ').h2,
SizedBox(height: 10),
RichText(
text: TextSpan(
children: [
TextSpan(
text: 'Зарегистрируйте домен в ',
style: body2TextStyle,
),
],
),
BrandSpanButton.link(
text: 'NameCheap',
urlString: 'https://www.namecheap.com',
),
TextSpan(
text:
' или у любого другого регистратора. После этого настройте его на DNS-сервер CloudFlare',
style: body2TextStyle,
),
],
),
);
},
),
_MockForm(
hintText: 'Домен, например, selfprivacy.org',
submitButtonText: 'Проверить DNS',
),
SizedBox(height: 20),
BrandButton.text(
onPressed: () {},
title: 'Как настроить DNS CloudFlare',
),
],
),
title: 'Как получить API Token',
),
],
),
BrandCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.asset('assets/images/logos/cloudflare.png'),
SizedBox(height: 10),
Text('3. Подключите CloudFlare DNS').h2,
SizedBox(height: 10),
Text('Для управления DNS вашего домена').body2,
_MockForm(
hintText: 'CloudFlare API Token',
),
BrandCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.asset('assets/images/logos/namecheap.png'),
SizedBox(height: 10),
Text('2. Настройте домен ').h2,
SizedBox(height: 10),
RichText(
text: TextSpan(
children: [
TextSpan(
text: 'Зарегистрируйте домен в ',
style: body2Style,
),
BrandSpanButton.link(
text: 'NameCheap',
urlString: 'https://www.namecheap.com',
),
TextSpan(
text:
' или у любого другого регистратора. После этого настройте его на DNS-сервер CloudFlare',
style: body2Style,
),
],
),
SizedBox(height: 20),
BrandButton.text(
onPressed: () {},
title: 'Как получить API Token',
),
],
),
),
_MockForm(
hintText: 'Домен, например, selfprivacy.org',
submitButtonText: 'Проверить DNS',
),
SizedBox(height: 20),
BrandButton.text(
onPressed: () {},
title: 'Как настроить DNS CloudFlare',
),
],
),
BrandCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.asset('assets/images/logos/aws.png'),
SizedBox(height: 10),
Text('4. Подключите Amazon AWS для бекапа').h2,
SizedBox(height: 10),
Text('IaaS-провайдер, для бесплатного хранения резервных копии ваших данных в зашифрованном виде')
.body2,
_MockForm(
hintText: 'Amazon AWS Access Key',
),
SizedBox(height: 20),
BrandButton.text(
onPressed: () {},
title: 'Как получить API Token',
),
],
),
)
],
),
),
BrandCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.asset('assets/images/logos/cloudflare.png'),
SizedBox(height: 10),
Text('3. Подключите CloudFlare DNS').h2,
SizedBox(height: 10),
Text('Для управления DNS вашего домена').body2,
_MockForm(
hintText: 'CloudFlare API Token',
),
SizedBox(height: 20),
BrandButton.text(
onPressed: () {},
title: 'Как получить API Token',
),
],
),
),
BrandCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.asset('assets/images/logos/aws.png'),
SizedBox(height: 10),
Text('4. Подключите Amazon AWS для бекапа').h2,
SizedBox(height: 10),
Text('IaaS-провайдер, для бесплатного хранения резервных копии ваших данных в зашифрованном виде')
.body2,
_MockForm(
hintText: 'Amazon AWS Access Key',
),
SizedBox(height: 20),
BrandButton.text(
onPressed: () {},
title: 'Как получить API Token',
),
],
),
)
],
),
);
}

View File

@ -0,0 +1,110 @@
import 'package:flutter/material.dart';
import 'package:selfprivacy/config/brand_theme.dart';
import 'package:selfprivacy/logic/cubit/services/services_cubit.dart';
import 'package:selfprivacy/logic/models/service.dart';
import 'package:selfprivacy/ui/components/brand_button/brand_button.dart';
import 'package:selfprivacy/ui/components/brand_card/brand_card.dart';
import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart';
import 'package:selfprivacy/ui/components/icon_status_mask/icon_status_mask.dart';
import 'package:selfprivacy/utils/extensions/text_extension.dart';
class ServicesPage extends StatefulWidget {
ServicesPage({Key key}) : super(key: key);
@override
_ServicesPageState createState() => _ServicesPageState();
}
class _ServicesPageState extends State<ServicesPage> {
@override
Widget build(BuildContext context) {
final serviceCubit = context.watch<ServicesCubit>();
final connected = serviceCubit.state.connected;
final uninitialized = serviceCubit.state.uninitialized;
return Scaffold(
body: ListView(
padding: brandPagePadding,
children: [
Text('Сервисы').caption,
SizedBox(height: 24),
...connected.map((service) => _Card(service: service)).toList(),
if (uninitialized.isNotEmpty) ...[
Text('не подключены').body1,
SizedBox(height: 30),
],
...uninitialized.map((service) => _Card(service: service)).toList()
],
),
);
}
}
class _Card extends StatelessWidget {
const _Card({Key key, @required this.service}) : super(key: key);
final Service service;
@override
Widget build(BuildContext context) {
String title;
IconData iconData;
String description;
switch (service.type) {
case ServiceTypes.messanger:
iconData = BrandIcons.messanger;
title = 'Мессенджер';
description =
'Delta Chat срфеТекст-текст описание. Если бы мне надо было обсудить что-то от чего зависит жизнь. Я бы выбрал Delta.Chat + свой почтовый сервер.';
break;
case ServiceTypes.mail:
iconData = BrandIcons.envelope;
title = 'Почта';
description = 'Электронная почта для семьи или компании ';
break;
case ServiceTypes.passwordManager:
iconData = BrandIcons.key;
title = 'Менеджер паролей';
description = 'Надёжное хранилище для ваших паролей и ключей доступа';
break;
case ServiceTypes.github:
iconData = BrandIcons.github;
title = 'Git сервер';
description = 'Сервис для приватного хранения своих разработок';
break;
case ServiceTypes.backup:
iconData = BrandIcons.save;
title = 'Резервное копирование';
description = 'Обеспеченье целосности и сохранности ваших данных';
break;
case ServiceTypes.cloud:
iconData = BrandIcons.upload;
title = 'Файловое Облако';
description = 'Сервис для доступа к вашим файлам в любой точке мира';
break;
}
return BrandCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
IconStatusMaks(
status: service.state,
child: Icon(iconData, size: 30, color: Colors.white),
),
SizedBox(height: 10),
Text(title).h2,
SizedBox(height: 10),
if (service.state == ServiceStateType.uninitialized) ...[
Text(description).body1,
SizedBox(height: 10),
BrandButton.text(
title: 'Подключить',
onPressed: () {
context.read<ServicesCubit>().connect(service);
})
],
if (service.state == ServiceStateType.stable) Text('Подключен').body1,
],
),
);
}
}

View File

@ -9,7 +9,8 @@ extension TextExtension on Text {
Text get h2 => copyWith(style: headline2Style);
Text get caption => copyWith(style: captionStyle);
Text get body2 => copyWith(style: body2TextStyle);
Text get body1 => copyWith(style: body1Style);
Text get body2 => copyWith(style: body2Style);
Text setKey(Key key) => copyWith(key: key);

View File

@ -22,6 +22,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.5.0-nullsafety.1"
bloc:
dependency: transitive
description:
name: bloc
url: "https://pub.dartlang.org"
source: hosted
version: "6.1.0"
boolean_selector:
dependency: transitive
description:
@ -85,6 +92,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.3.3"
equatable:
dependency: "direct main"
description:
name: equatable
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.5"
fake_async:
dependency: transitive
description:
@ -111,6 +125,13 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
flutter_bloc:
dependency: "direct main"
description:
name: flutter_bloc
url: "https://pub.dartlang.org"
source: hosted
version: "6.1.1"
flutter_launcher_icons:
dependency: "direct dev"
description:
@ -182,6 +203,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.0-nullsafety.3"
nested:
dependency: transitive
description:
name: nested
url: "https://pub.dartlang.org"
source: hosted
version: "0.0.4"
path:
dependency: transitive
description:
@ -259,6 +287,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.13"
provider:
dependency: "direct main"
description:
name: provider
url: "https://pub.dartlang.org"
source: hosted
version: "4.3.2+2"
shared_preferences:
dependency: transitive
description:

View File

@ -11,7 +11,10 @@ dependencies:
sdk: flutter
cupertino_icons: ^1.0.0
easy_localization: ^2.3.3
equatable: ^1.2.5
flutter_bloc: ^6.1.1
google_fonts: ^1.1.1
provider: ^4.3.2+2
url_launcher: ^5.7.10
dev_dependencies: