diff --git a/assets/images/gifs/Backblaze.gif b/assets/images/gifs/Backblaze.gif new file mode 100755 index 00000000..eb48c5d2 Binary files /dev/null and b/assets/images/gifs/Backblaze.gif differ diff --git a/assets/images/gifs/CloudFlare.gif b/assets/images/gifs/CloudFlare.gif new file mode 100755 index 00000000..749f514e Binary files /dev/null and b/assets/images/gifs/CloudFlare.gif differ diff --git a/assets/images/gifs/Hetzner.gif b/assets/images/gifs/Hetzner.gif new file mode 100755 index 00000000..632492f5 Binary files /dev/null and b/assets/images/gifs/Hetzner.gif differ diff --git a/assets/markdown/how_backblaze-en.md b/assets/markdown/how_backblaze-en.md new file mode 100644 index 00000000..b1e341d1 --- /dev/null +++ b/assets/markdown/how_backblaze-en.md @@ -0,0 +1,8 @@ +### How to get Backblaze API Token +1. Visit the following link and authorize: https://secure.backblaze.com/user_signin.htm +2. On the left side of the interface, select **App Keys** in the **B2 Cloud Storage** subcategory. +3. Click on the blue **Generate New Master Application Key** button. +4. In the appeared pop-up window confirm the generation. +5. Save _keyID_ and _applicationKey_ in the safe place. For example, in the password manager. + +![Backblaze token setup](resource:assets/images/gifs/Backblaze.gif) diff --git a/assets/markdown/how_backblaze-ru.md b/assets/markdown/how_backblaze-ru.md new file mode 100644 index 00000000..2938f1cb --- /dev/null +++ b/assets/markdown/how_backblaze-ru.md @@ -0,0 +1,8 @@ +### Как получить Backblaze API Token +1. Переходим по ссылке https://secure.backblaze.com/user_signin.htm и авторизуемся. +2. В левой части интерфейса выбираем **App Keys** в подкатегории **"Account"**. +3. Нажимаем на синюю кнопку **Generate New Master Application Key**. +4. Во всплывающем окне подтверждаем генерацию. +5. Сохраняем _keyID_ и _applicationKey_ в надёжном месте. Например в менеджере паролей. + +![Backblaze token setup](resource:assets/images/gifs/Backblaze.gif) diff --git a/assets/markdown/how_cloudflare-en.md b/assets/markdown/how_cloudflare-en.md new file mode 100644 index 00000000..61fb4b4e --- /dev/null +++ b/assets/markdown/how_cloudflare-en.md @@ -0,0 +1,17 @@ +### How to get Cloudflare API Token +1. Visit the following link: https://dash.cloudflare.com/ +2. the right corner, click on the profile icon (a man in a circle). For the mobile version of the site, in the upper left corner, click the **Menu** button (three horizontal bars), in the dropdown menu, click on **My Profile** +3. There are four configuration categories to choose from: *Communication*, *Authentication*, **API Tokens**, *Session*. Choose **API Tokens**. +4. Click on **Create Token** button. +5. Go down to the bottom and see the **Create Custom Token** field and press **Get Started** button on the right side. +6. In the **Token Name** field, give your token a name. +7. Next we have Permissions. In the leftmost field, select **Zone**. In the longest field, center, select **DNS**. In the rightmost field, select **Edit**. +8. Next, right under this line, click Add More. Similar field will appear. +9. In the leftmost field of the new line, select, similar to the last line — **Zone**. In the center — a little different. Here choose the same as in the left — **Zone**. In the rightmost field, select **Read**. +10. Next look at **Zone Resources**. Under this inscription there is a line with two fields. The left must have **Include** and the right must have **Specific Zone**. Once you select Specific Zone, another field appears on the right. Choose your domain in it. +11. Flick to the bottom and press the blue **Continue to Summary** button. +12. Check if you got everything right. A similar string must be present: *Domain — DNS:Edit, Zone:Read*. +13. Click on **Create Token**. +14. We copy the created token, and save it in a reliable place (preferably in the password manager). + +![Cloudflare token setup](resource:assets/images/gifs/CloudFlare.gif) diff --git a/assets/markdown/how_cloudflare-ru.md b/assets/markdown/how_cloudflare-ru.md new file mode 100644 index 00000000..7da91f77 --- /dev/null +++ b/assets/markdown/how_cloudflare-ru.md @@ -0,0 +1,15 @@ +### Как получить Cloudflare API Token +1. Переходим по [ссылке](https://dash.cloudflare.com/) и авторизуемся в ранее созданном аккаунте. https://dash.cloudflare.com/ +В правом углу кликаем на иконку профиля (человечек в кружочке). Для мобильной версии сайта, в верхнем левом углу, нажимаем кнопку **Меню** (три горизонтальных полоски), в выпавшем меню, ищем пункт **My Profile**. +3. Нам предлагается на выбор, четыре категории настройки: **Preferences**, **Authentication**, **API Tokens**, **Sessions**. Выбираем **API Tokens**. +4. Самым первым пунктом видим кнопку **Create Token**. С полной уверенностью в себе и желанием обрести приватность, нажимаем на неё. +5. Спускаемся в самый низ и видим поле **Create Custom Token** и кнопку **Get Started** с правой стороны. Нажимаем. +6. В поле **Token Name** даём своему токену имя. Можете покреативить и отнестись к этому как к наименованию домашнего зверька :) +7. Далее, у нас **Permissions**. В первом поле выбираем Zone. Во втором поле, по центру, выбираем **DNS**. В последнем поле выбираем **Edit**. +8. Далее смотрим на **Zone Resources**. Под этой надписью есть строка с двумя полями. В первом должно быть **Include**, а во втором — **Specific Zone**. Как только Вы выберите **Specific Zone**, справа появится ещё одно поле. В нём выбираем наш домен. +9. Листаем в самый низ и нажимаем на синюю кнопку **Continue to Summary**. +10. Проверяем, всё ли мы правильно выбрали. Должна присутствовать подобная строка: ваш.домен — **DNS:Edit, Zone:Read**. +11. Нажимаем **Create Token**. +12. Копируем созданный токен, и сохраняем его в надёжном месте (желательно — в менеджере паролей). + +![Cloudflare token setup](resource:assets/images/gifs/CloudFlare.gif) diff --git a/assets/markdown/how_hetzner-en.md b/assets/markdown/how_hetzner-en.md index 47042903..6f859c18 100644 --- a/assets/markdown/how_hetzner-en.md +++ b/assets/markdown/how_hetzner-en.md @@ -1,3 +1,4 @@ +### How to get Hetzner API Token 1. Visit the following [link](https://console.hetzner.cloud/) and sign into newly created account. 2. Enter into previously created project. If you haven't created one, @@ -17,4 +18,6 @@ **permissions**. Pick **Read & Write**. 8. Click **Generate API Token.** 9. After that, our key will be shown. Store it in the reliable place, - or in the password manager, which is better. \ No newline at end of file + or in the password manager, which is better. + +![Hetzner token setup](resource:assets/images/gifs/Hetzner.gif) diff --git a/assets/markdown/how_hetzner-ru.md b/assets/markdown/how_hetzner-ru.md index 805b8497..94d19c37 100644 --- a/assets/markdown/how_hetzner-ru.md +++ b/assets/markdown/how_hetzner-ru.md @@ -4,4 +4,6 @@ 3. Наводим мышкой на боковую панель. Она должна раскрыться, показав нам пункты меню. Нас интересует последний — Security (с иконкой ключика). 4. Далее, в верхней части интерфейса видим примерно такой список: SSH Keys, API Tokens, Certificates, Members. Нам нужен API Tokens. Переходим по нему. 5. В правой части интерфейса, нас будет ожидать кнопка Generate API token. Если же Вы используете мобильную версию сайта, в нижнем правом углу Вы увидите красный плюсик. Нажимаем на эту кнопку. -6. В поле Description, даём нашему токену название (это может быть любое название, которые Вам нравиться. Сути оно не меняет. \ No newline at end of file +6. В поле Description, даём нашему токену название (это может быть любое название, которые Вам нравиться. Сути оно не меняет. + +![Hetzner token setup](resource:assets/images/gifs/Hetzner.gif) diff --git a/assets/translations/en.json b/assets/translations/en.json index 76ee8bb9..8675c86e 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -127,6 +127,7 @@ "mail": { "title": "E-Mail", "subtitle": "E-Mail for company and family.", + "login_info": "Use username and password from users tab. IMAP port is 143 with STARTTLS, SMTP port is 587 with STARTTLS.", "bottom_sheet": { "1": "To connect to the mailserver, please use {} domain alongside with username and password, that you created. Also feel free to invite", "2": "new users" @@ -135,6 +136,7 @@ "messenger": { "title": "Messenger", "subtitle": "Telegram or Signal not so private as Delta.Chat that uses your private server.", + "login_info": "Use the same username and password as for e-mail.", "bottom_sheet": { "1": "For connection, please use {} domain and credentials that you created." } @@ -142,6 +144,7 @@ "password_manager": { "title": "Password Manager", "subtitle": "Base of your security. Bitwarden will help you to create, store and move passwords between devices, as well as input them, when requested using autocompletion.", + "login_info": "You will have to create an account on the website.", "bottom_sheet": { "1": "You can connect to the service and create a user via this link:" } @@ -149,6 +152,7 @@ "video": { "title": "Videomeet", "subtitle": "Zoom and Google Meet are good, but Jitsi Meet is a worth alternative that also gives you confidence that you're not being listened.", + "login_info": "No account needed.", "bottom_sheet": { "1": "Using Jitsi as simple as just visiting this link:" } @@ -156,6 +160,7 @@ "cloud": { "title": "Cloud Storage", "subtitle": "Do not allow cloud services to read your data by using NextCloud.", + "login_info": "Login is admin, password is the same as with your main user. Create new accounts in Nextcloud interface.", "bottom_sheet": { "1": "You can connect and create a new user here:" } @@ -163,6 +168,7 @@ "social_network": { "title": "Social Network", "subtitle": "It's hard to believe, but it became possible to create your own social network, with your own rules and target audience.", + "login_info": "You will have to create an account on the website.", "bottom_sheet": { "1": "You can connect and create new social user here:" } @@ -170,6 +176,7 @@ "git": { "title": "Git Server", "subtitle": "Private alternative to the Github, that belongs to you, but not a Microsoft.", + "login_info": "You will have to create an account on the website. First user will become an admin.", "bottom_sheet": { "1": "You can connect and create a new user here:" } @@ -248,7 +255,8 @@ "title": "Jobs list", "start": "Start", "empty": "No jobs", - "createUser": "Create", + "createUser": "Create user", + "deleteUser": "Delete user", "serviceTurnOff": "Turn off", "serviceTurnOn": "Turn on", "jobAdded": "Job added", diff --git a/assets/translations/ru.json b/assets/translations/ru.json index 192d3164..a9b23e07 100644 --- a/assets/translations/ru.json +++ b/assets/translations/ru.json @@ -128,6 +128,7 @@ "mail": { "title": "Почта", "subtitle": "Электронная почта для семьи или компании.", + "login_info": "Используйте логин и пароль из вкладки пользователей. IMAP порт: 143, STARTTLS. SMTP порт: 587, STARTTLS.", "bottom_sheet": { "1": "Для подключения почтового ящика используйте {} и профиль, который Вы создали. Так же приглашайте", "2": "новых пользователей." @@ -136,6 +137,7 @@ "messenger": { "title": "Мессенджер", "subtitle": "Telegram и Signal не так приватны, как Delta.Chat — он использует Ваш личный сервер.", + "login_info": "Используйте те же логин и пароль, что и для почты.", "bottom_sheet": { "1": "Для подключения используйте {} и логин пароль, который Вы создали." } @@ -143,6 +145,7 @@ "password_manager": { "title": "Менеджер паролей", "subtitle": "Это фундамент Вашей безопасности. Создавать, хранить, копировать пароли между устройствами и вбивать их в формы поможет Bitwarden.", + "login_info": "Аккаунт нужно создать на сайте.", "bottom_sheet": { "1": "Подключиться к серверу и создать пользователя можно по адресу:." } @@ -150,6 +153,7 @@ "video": { "title": "Видеоконференция", "subtitle": "Jitsi meet — отличный аналог Zoom и Google meet который помимо удобства ещё и гарантирует Вам защищённые высококачественные видеоконференции.", + "login_info": "Аккаунт не требуется.", "bottom_sheet": { "1": "Для использования просто перейдите по ссылке:." } @@ -157,6 +161,7 @@ "cloud": { "title": "Файловое облако", "subtitle": "Не позволяйте облачным сервисам просматривать ваши данные. Используйте NextCloud — надёжный дом для всех Ваших данных.", + "login_info": "Логин администратора: admin, пароль такой же как у основного пользователя. Создавайте новых пользователей в интерфейсе администратора NextCloud.", "bottom_sheet": { "1": "Подключиться к серверу и создать пользователя можно по адресу:." } @@ -164,6 +169,7 @@ "social_network": { "title": "Социальная сеть", "subtitle": "Сложно поверить, но стало возможным создать свою собственную социальную сеть, со своими правилами и аудиторией.", + "login_info": "Аккаунт нужно создать на сайте.", "bottom_sheet": { "1": "Подключиться к серверу и создать пользователя можно по адресу:." } @@ -171,6 +177,7 @@ "git": { "title": "Git-сервер", "subtitle": "Приватная альтернатива Github, которая принадлежит вам, а не Microsoft.", + "login_info": "Аккаунт нужно создать на сайте. Первый зарегистрированный пользователь становится администратором.", "bottom_sheet": { "1": "Подключиться к серверу и создать пользователя можно по адресу:." } @@ -249,7 +256,8 @@ "title": "Задачи", "start": "Начать выполенение", "empty": "Пусто.", - "createUser": "Создать запись", + "createUser": "Создать пользователя", + "deleteUser": "Удалить пользователя", "serviceTurnOff": "Остановить", "serviceTurnOn": "Запустить", "jobAdded": "Задача добавленна", diff --git a/lib/logic/api_maps/hetzner.dart b/lib/logic/api_maps/hetzner.dart index 51833622..f78595c1 100644 --- a/lib/logic/api_maps/hetzner.dart +++ b/lib/logic/api_maps/hetzner.dart @@ -114,7 +114,8 @@ class HetznerApi extends ApiMap { final apiToken = StringGenerators.apiToken(); - final hostname = domainName.split('.')[0]; + // Replace all non-alphanumeric characters with an underscore + final hostname = domainName.split('.')[0].replaceAll(RegExp(r'[^a-zA-Z0-9]'), '-'); /// add ssh key when you need it: e.g. "ssh_keys":["kherel"] /// check the branch name, it could be "development" or "master". diff --git a/lib/logic/api_maps/server.dart b/lib/logic/api_maps/server.dart index 7705dcc4..1792e2c1 100644 --- a/lib/logic/api_maps/server.dart +++ b/lib/logic/api_maps/server.dart @@ -74,6 +74,28 @@ class ServerApi extends ApiMap { return res; } + Future deleteUser(User user) async { + bool res; + Response response; + + var client = await getClient(); + try { + response = await client.delete( + '/users/${user.login}', + options: Options( + contentType: 'application/json', + ), + ); + res = response.statusCode == HttpStatus.ok; + } catch (e) { + print(e); + res = false; + } + + close(client); + return res; + } + String get rootAddress => throw UnimplementedError('not used in with implementation'); diff --git a/lib/logic/common_enum/common_enum.dart b/lib/logic/common_enum/common_enum.dart index e3d81309..9903f304 100644 --- a/lib/logic/common_enum/common_enum.dart +++ b/lib/logic/common_enum/common_enum.dart @@ -69,6 +69,46 @@ extension ServiceTypesExt on ServiceTypes { } } + String get loginInfo { + switch (this) { + case ServiceTypes.mail: + return 'services.mail.login_info'.tr(); + case ServiceTypes.messenger: + return 'services.messenger.login_info'.tr(); + case ServiceTypes.passwordManager: + return 'services.password_manager.login_info'.tr(); + case ServiceTypes.video: + return 'services.video.login_info'.tr(); + case ServiceTypes.cloud: + return 'services.cloud.login_info'.tr(); + case ServiceTypes.socialNetwork: + return 'services.social_network.login_info'.tr(); + case ServiceTypes.git: + return 'services.git.login_info'.tr(); + case ServiceTypes.vpn: + return ''; + } + } + + String get subdomain { + switch (this) { + case ServiceTypes.passwordManager: + return 'password'; + case ServiceTypes.video: + return 'meet'; + case ServiceTypes.cloud: + return 'cloud'; + case ServiceTypes.socialNetwork: + return 'social'; + case ServiceTypes.git: + return 'git'; + case ServiceTypes.vpn: + case ServiceTypes.messenger: + default: + return ''; + } + } + IconData get icon { switch (this) { case ServiceTypes.mail: diff --git a/lib/logic/cubit/backups/backups_cubit.dart b/lib/logic/cubit/backups/backups_cubit.dart index 7fb42ed3..4bee9ea8 100644 --- a/lib/logic/cubit/backups/backups_cubit.dart +++ b/lib/logic/cubit/backups/backups_cubit.dart @@ -6,6 +6,7 @@ import 'package:selfprivacy/logic/models/backblaze_bucket.dart'; import 'package:selfprivacy/logic/models/backup.dart'; import 'package:selfprivacy/logic/api_maps/server.dart'; import 'package:selfprivacy/logic/api_maps/backblaze.dart'; +import 'package:easy_localization/easy_localization.dart'; part 'backups_state.dart'; @@ -85,7 +86,7 @@ class BackupsCubit extends AppConfigDependendCubit { Future createBucket() async { emit(state.copyWith(preventActions: true)); final domain = - appConfigCubit.state.cloudFlareDomain!.domainName.replaceAll('.', '-'); + appConfigCubit.state.cloudFlareDomain!.domainName.replaceAll(RegExp(r'[^a-zA-Z0-9]'), '-'); final serverId = appConfigCubit.state.hetznerServer!.id; var bucketName = 'selfprivacy-$domain-$serverId'; // If bucket name is too long, shorten it @@ -151,7 +152,8 @@ class BackupsCubit extends AppConfigDependendCubit { Future forceUpdateBackups() async { emit(state.copyWith(preventActions: true)); await api.forceBackupListReload(); - getIt().showSnackBar('providers.backup.refetchingList'); + getIt() + .showSnackBar('providers.backup.refetchingList'.tr()); emit(state.copyWith(preventActions: false)); } diff --git a/lib/logic/cubit/jobs/jobs_cubit.dart b/lib/logic/cubit/jobs/jobs_cubit.dart index d48c6fdd..a2c1b414 100644 --- a/lib/logic/cubit/jobs/jobs_cubit.dart +++ b/lib/logic/cubit/jobs/jobs_cubit.dart @@ -69,15 +69,18 @@ class JobsCubit extends Cubit { } Future rebootServer() async { + emit(JobsStateLoading()); final isSuccessful = await api.reboot(); if (isSuccessful) { getIt().showSnackBar('jobs.rebootSuccess'.tr()); } else { getIt().showSnackBar('jobs.rebootFailed'.tr()); } + emit(JobsStateEmpty()); } Future upgradeServer() async { + emit(JobsStateLoading()); final isPullSuccessful = await api.pullConfigurationUpdate(); final isSuccessful = await api.upgrade(); if (isSuccessful) { @@ -89,6 +92,7 @@ class JobsCubit extends Cubit { } else { getIt().showSnackBar('jobs.upgradeFailed'.tr()); } + emit(JobsStateEmpty()); } Future applyAll() async { @@ -101,7 +105,12 @@ class JobsCubit extends Cubit { if (job is CreateUserJob) { newUsers.add(job.user); await api.createUser(job.user); - } else if (job is ServiceToggleJob) { + } + if (job is DeleteUserJob) { + final deleted = await api.deleteUser(job.user); + if (deleted) usersCubit.remove(job.user); + } + if (job is ServiceToggleJob) { hasServiceJobs = true; await api.switchService(job.type, job.needToTurnOn); } diff --git a/lib/logic/models/job.dart b/lib/logic/models/job.dart index c06d4ebd..e9767803 100644 --- a/lib/logic/models/job.dart +++ b/lib/logic/models/job.dart @@ -31,6 +31,17 @@ class CreateUserJob extends Job { List get props => [id, title, user]; } +class DeleteUserJob extends Job { + DeleteUserJob({ + required this.user, + }) : super(title: '${"jobs.deleteUser".tr()} ${user.login}'); + + final User user; + + @override + List get props => [id, title, user]; +} + class ServiceToggleJob extends Job { ServiceToggleJob({ required this.type, diff --git a/lib/ui/pages/initializing/initializing.dart b/lib/ui/pages/initializing/initializing.dart index 4ab27eec..958637e3 100644 --- a/lib/ui/pages/initializing/initializing.dart +++ b/lib/ui/pages/initializing/initializing.dart @@ -137,7 +137,8 @@ class InitializingPage extends StatelessWidget { ), SizedBox(height: 10), BrandButton.text( - onPressed: () => _showModal(context, _HowHetzner()), + onPressed: () => + _showModal(context, _HowTo(fileName: 'how_hetzner')), title: 'initializing.how'.tr(), ), ], @@ -192,7 +193,11 @@ class InitializingPage extends StatelessWidget { ), SizedBox(height: 10), BrandButton.text( - onPressed: () => _showModal(context, _HowHetzner()), + onPressed: () => _showModal( + context, + _HowTo( + fileName: 'how_cloudflare', + )), title: 'initializing.how'.tr(), ), ], @@ -243,7 +248,11 @@ class InitializingPage extends StatelessWidget { ), SizedBox(height: 10), BrandButton.text( - onPressed: () => _showModal(context, _HowHetzner()), + onPressed: () => _showModal( + context, + _HowTo( + fileName: 'how_backblaze', + )), title: 'initializing.how'.tr(), ), ], @@ -334,12 +343,9 @@ class InitializingPage extends StatelessWidget { text: 'initializing.10'.tr(), ), ], - SizedBox(height: 10), - Spacer(), - SizedBox(height: 10), - BrandButton.text( - onPressed: () => _showModal(context, _HowHetzner()), - title: 'initializing.how'.tr(), + SizedBox( + height: 10, + width: double.infinity, ), ], ); @@ -403,11 +409,6 @@ class InitializingPage extends StatelessWidget { : () => context.read().trySubmit(), text: 'basis.connect'.tr(), ), - SizedBox(height: 10), - BrandButton.text( - onPressed: () => _showModal(context, _HowHetzner()), - title: 'initializing.how'.tr(), - ), ], ); }), @@ -431,11 +432,6 @@ class InitializingPage extends StatelessWidget { : () => appConfigCubit.createServerAndSetDnsRecords(), text: isLoading ? 'basis.loading'.tr() : 'initializing.11'.tr(), ), - Spacer(flex: 2), - BrandButton.text( - onPressed: () => _showModal(context, _HowHetzner()), - title: 'initializing.what'.tr(), - ), ], ); }); @@ -482,13 +478,6 @@ class InitializingPage extends StatelessWidget { ], ), if (state.isLoading) BrandText.body2('initializing.17'.tr()), - Spacer( - flex: 2, - ), - BrandButton.text( - onPressed: () => _showModal(context, _HowHetzner()), - title: 'initializing.what'.tr(), - ), ], ); }); @@ -503,11 +492,14 @@ class InitializingPage extends StatelessWidget { } } -class _HowHetzner extends StatelessWidget { - const _HowHetzner({ +class _HowTo extends StatelessWidget { + const _HowTo({ Key? key, + required this.fileName, }) : super(key: key); + final String fileName; + @override Widget build(BuildContext context) { return BrandBottomSheet( @@ -515,7 +507,7 @@ class _HowHetzner extends StatelessWidget { child: Padding( padding: paddingH15V0, child: BrandMarkdown( - fileName: 'how_hetzner', + fileName: fileName, ), ), ); diff --git a/lib/ui/pages/services/services.dart b/lib/ui/pages/services/services.dart index e627a1b6..410d5048 100644 --- a/lib/ui/pages/services/services.dart +++ b/lib/ui/pages/services/services.dart @@ -38,6 +38,23 @@ class ServicesPage extends StatefulWidget { _ServicesPageState createState() => _ServicesPageState(); } +void _launchURL(url) async { + var _possible = await canLaunch(url); + + if (_possible) { + try { + await launch( + url, + enableJavaScript: true, + ); + } catch (e) { + print(e); + } + } else { + throw 'Could not launch $url'; + } +} + class _ServicesPageState extends State { @override Widget build(BuildContext context) { @@ -94,6 +111,9 @@ class _Card extends StatelessWidget { (!switchableServices.contains(serviceType) || serviceState.isEnableByType(serviceType)); + var config = context.watch().state; + var domainName = UiHelpers.getDomainName(config); + return GestureDetector( onTap: isSwithOn ? () => showDialog( @@ -163,6 +183,30 @@ class _Card extends StatelessWidget { SizedBox(height: 10), BrandText.h2(serviceType.title), SizedBox(height: 10), + if (serviceType.subdomain != '') + Column( + children: [ + GestureDetector( + onTap: () => _launchURL( + 'https://${serviceType.subdomain}.$domainName'), + child: Text( + '${serviceType.subdomain}.$domainName', + style: linkStyle, + ), + ), + SizedBox(height: 10), + ], + ), + if (serviceType == ServiceTypes.mail) + Column(children: [ + Text( + domainName, + style: linkStyle, + ), + SizedBox(height: 10), + ]), + BrandText.body2(serviceType.loginInfo), + SizedBox(height: 10), BrandText.body2(serviceType.subtitle), SizedBox(height: 10), ], @@ -438,21 +482,4 @@ class _ServiceDetails extends StatelessWidget { ), ); } - - void _launchURL(url) async { - var _possible = await canLaunch(url); - - if (_possible) { - try { - await launch( - url, - enableJavaScript: true, - ); - } catch (e) { - print(e); - } - } else { - throw 'Could not launch $url'; - } - } } diff --git a/lib/ui/pages/users/user_details.dart b/lib/ui/pages/users/user_details.dart index 869a9004..89d5d97f 100644 --- a/lib/ui/pages/users/user_details.dart +++ b/lib/ui/pages/users/user_details.dart @@ -75,9 +75,8 @@ class _UserDetails extends StatelessWidget { ), ), onPressed: () { - context - .read() - .remove(user); + context.read().addJob( + DeleteUserJob(user: user)); Navigator.of(context) ..pop() ..pop(); diff --git a/pubspec.yaml b/pubspec.yaml index ac38d851..88a6a95d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: selfprivacy description: selfprivacy.org publish_to: 'none' -version: 0.4.1+9 +version: 0.4.2+10 environment: sdk: '>=2.13.4 <3.0.0' @@ -61,6 +61,7 @@ flutter: - assets/images/ - assets/images/onboarding/ - assets/images/logos/ + - assets/images/gifs/ - assets/translations/ - assets/markdown/ fonts: