diff --git a/lib/logic/api_maps/graphql_maps/server_api/server.dart b/lib/logic/api_maps/graphql_maps/server_api/server.dart index 04f1d855..13ea67a7 100644 --- a/lib/logic/api_maps/graphql_maps/server_api/server.dart +++ b/lib/logic/api_maps/graphql_maps/server_api/server.dart @@ -37,7 +37,8 @@ class GenericJobMutationReturn extends GenericMutationResult { final ServerJob? job; } -class ServerApi extends ApiMap with VolumeApi, JobsApi, ServerActionsApi { +class ServerApi extends ApiMap + with VolumeApi, JobsApi, ServerActionsApi, ServicesApi { ServerApi({ this.hasLogger = false, this.isWithToken = true, diff --git a/lib/logic/api_maps/rest_maps/server.dart b/lib/logic/api_maps/rest_maps/server.dart index cd8e0a84..2563fa50 100644 --- a/lib/logic/api_maps/rest_maps/server.dart +++ b/lib/logic/api_maps/rest_maps/server.dart @@ -409,11 +409,11 @@ class ServerApi extends ApiMap { } return { - ServiceTypes.passwordManager: response.data['bitwarden'] == 0, - ServiceTypes.git: response.data['gitea'] == 0, - ServiceTypes.cloud: response.data['nextcloud'] == 0, - ServiceTypes.vpn: response.data['ocserv'] == 0, - ServiceTypes.socialNetwork: response.data['pleroma'] == 0, + ServiceTypes.bitwarden: response.data['bitwarden'] == 0, + ServiceTypes.gitea: response.data['gitea'] == 0, + ServiceTypes.nextcloud: response.data['nextcloud'] == 0, + ServiceTypes.ocserv: response.data['ocserv'] == 0, + ServiceTypes.pleroma: response.data['pleroma'] == 0, }; } @@ -907,15 +907,15 @@ extension UrlServerExt on ServiceTypes { // return ''; // external service // case ServiceTypes.video: // return ''; // jitsi meet not working - case ServiceTypes.passwordManager: + case ServiceTypes.bitwarden: return 'bitwarden'; - case ServiceTypes.cloud: + case ServiceTypes.nextcloud: return 'nextcloud'; - case ServiceTypes.socialNetwork: + case ServiceTypes.pleroma: return 'pleroma'; - case ServiceTypes.git: + case ServiceTypes.gitea: return 'gitea'; - case ServiceTypes.vpn: + case ServiceTypes.ocserv: return 'ocserv'; default: throw Exception('wrong state'); diff --git a/lib/logic/common_enum/common_enum.dart b/lib/logic/common_enum/common_enum.dart index 3a1a4d80..93334069 100644 --- a/lib/logic/common_enum/common_enum.dart +++ b/lib/logic/common_enum/common_enum.dart @@ -23,94 +23,86 @@ enum InitializingSteps { enum Period { hour, day, month } enum ServiceTypes { - mail, - messenger, - passwordManager, - video, - cloud, - socialNetwork, - git, - vpn, + mailserver, + bitwarden, + jitsi, + nextcloud, + pleroma, + gitea, + ocserv, } extension ServiceTypesExt on ServiceTypes { String get title { switch (this) { - case ServiceTypes.mail: + case ServiceTypes.mailserver: return 'services.mail.title'.tr(); - case ServiceTypes.messenger: - return 'services.messenger.title'.tr(); - case ServiceTypes.passwordManager: + case ServiceTypes.bitwarden: return 'services.password_manager.title'.tr(); - case ServiceTypes.video: + case ServiceTypes.jitsi: return 'services.video.title'.tr(); - case ServiceTypes.cloud: + case ServiceTypes.nextcloud: return 'services.cloud.title'.tr(); - case ServiceTypes.socialNetwork: + case ServiceTypes.pleroma: return 'services.social_network.title'.tr(); - case ServiceTypes.git: + case ServiceTypes.gitea: return 'services.git.title'.tr(); - case ServiceTypes.vpn: + case ServiceTypes.ocserv: return 'services.vpn.title'.tr(); } } String get subtitle { switch (this) { - case ServiceTypes.mail: + case ServiceTypes.mailserver: return 'services.mail.subtitle'.tr(); - case ServiceTypes.messenger: - return 'services.messenger.subtitle'.tr(); - case ServiceTypes.passwordManager: + case ServiceTypes.bitwarden: return 'services.password_manager.subtitle'.tr(); - case ServiceTypes.video: + case ServiceTypes.jitsi: return 'services.video.subtitle'.tr(); - case ServiceTypes.cloud: + case ServiceTypes.nextcloud: return 'services.cloud.subtitle'.tr(); - case ServiceTypes.socialNetwork: + case ServiceTypes.pleroma: return 'services.social_network.subtitle'.tr(); - case ServiceTypes.git: + case ServiceTypes.gitea: return 'services.git.subtitle'.tr(); - case ServiceTypes.vpn: + case ServiceTypes.ocserv: return 'services.vpn.subtitle'.tr(); } } String get loginInfo { switch (this) { - case ServiceTypes.mail: + case ServiceTypes.mailserver: return 'services.mail.login_info'.tr(); - case ServiceTypes.messenger: - return 'services.messenger.login_info'.tr(); - case ServiceTypes.passwordManager: + case ServiceTypes.bitwarden: return 'services.password_manager.login_info'.tr(); - case ServiceTypes.video: + case ServiceTypes.jitsi: return 'services.video.login_info'.tr(); - case ServiceTypes.cloud: + case ServiceTypes.nextcloud: return 'services.cloud.login_info'.tr(); - case ServiceTypes.socialNetwork: + case ServiceTypes.pleroma: return 'services.social_network.login_info'.tr(); - case ServiceTypes.git: + case ServiceTypes.gitea: return 'services.git.login_info'.tr(); - case ServiceTypes.vpn: + case ServiceTypes.ocserv: return ''; } } String get subdomain { switch (this) { - case ServiceTypes.passwordManager: + case ServiceTypes.bitwarden: return 'password'; - case ServiceTypes.video: + case ServiceTypes.jitsi: return 'meet'; - case ServiceTypes.cloud: + case ServiceTypes.nextcloud: return 'cloud'; - case ServiceTypes.socialNetwork: + case ServiceTypes.pleroma: return 'social'; - case ServiceTypes.git: + case ServiceTypes.gitea: return 'git'; - case ServiceTypes.vpn: - case ServiceTypes.messenger: + case ServiceTypes.ocserv: default: return ''; } @@ -118,21 +110,19 @@ extension ServiceTypesExt on ServiceTypes { IconData get icon { switch (this) { - case ServiceTypes.mail: + case ServiceTypes.mailserver: return BrandIcons.envelope; - case ServiceTypes.messenger: - return BrandIcons.messanger; - case ServiceTypes.passwordManager: + case ServiceTypes.bitwarden: return BrandIcons.key; - case ServiceTypes.video: + case ServiceTypes.jitsi: return BrandIcons.webcam; - case ServiceTypes.cloud: + case ServiceTypes.nextcloud: return BrandIcons.upload; - case ServiceTypes.socialNetwork: + case ServiceTypes.pleroma: return BrandIcons.social; - case ServiceTypes.git: + case ServiceTypes.gitea: return BrandIcons.git; - case ServiceTypes.vpn: + case ServiceTypes.ocserv: return Icons.vpn_lock_outlined; } } diff --git a/lib/logic/cubit/jobs/jobs_cubit.dart b/lib/logic/cubit/jobs/jobs_cubit.dart index baac08dd..4457fba5 100644 --- a/lib/logic/cubit/jobs/jobs_cubit.dart +++ b/lib/logic/cubit/jobs/jobs_cubit.dart @@ -114,7 +114,7 @@ class JobsCubit extends Cubit { } if (job is ServiceToggleJob) { hasServiceJobs = true; - await api.switchService(job.type, job.needToTurnOn); + await api.switchService(job.type.name, job.needToTurnOn); } if (job is CreateSSHKeyJob) { await usersCubit.addSshKey(job.user, job.publicKey); diff --git a/lib/logic/cubit/services/services_cubit.dart b/lib/logic/cubit/services/services_cubit.dart index 19160af1..089d9638 100644 --- a/lib/logic/cubit/services/services_cubit.dart +++ b/lib/logic/cubit/services/services_cubit.dart @@ -1,31 +1,48 @@ -import 'package:selfprivacy/logic/api_maps/rest_maps/server.dart'; +import 'dart:async'; + +import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server.dart'; import 'package:selfprivacy/logic/common_enum/common_enum.dart'; import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart'; +import 'package:selfprivacy/logic/models/service.dart'; part 'services_state.dart'; class ServicesCubit extends ServerInstallationDependendCubit { ServicesCubit(final ServerInstallationCubit serverInstallationCubit) - : super(serverInstallationCubit, ServicesState.allOff()); + : super(serverInstallationCubit, const ServicesState.empty()); final ServerApi api = ServerApi(); + Timer? timer; @override Future load() async { if (serverInstallationCubit.state is ServerInstallationFinished) { - final Map statuses = await api.servicesPowerCheck(); + final List services = await api.getAllServices(); emit( ServicesState( - isPasswordManagerEnable: statuses[ServiceTypes.passwordManager]!, - isCloudEnable: statuses[ServiceTypes.cloud]!, - isGitEnable: statuses[ServiceTypes.git]!, - isSocialNetworkEnable: statuses[ServiceTypes.socialNetwork]!, - isVpnEnable: statuses[ServiceTypes.vpn]!, + services: services, ), ); + timer = Timer(const Duration(seconds: 10), () => reload(useTimer: true)); + } + } + + Future reload({final bool useTimer = false}) async { + final List services = await api.getAllServices(); + emit( + ServicesState( + services: services, + ), + ); + if (useTimer) { + timer = Timer(const Duration(seconds: 60), () => reload(useTimer: true)); } } @override void clear() async { - emit(ServicesState.allOff()); + emit(const ServicesState.empty()); + if (timer != null && timer!.isActive) { + timer!.cancel(); + timer = null; + } } } diff --git a/lib/logic/cubit/services/services_state.dart b/lib/logic/cubit/services/services_state.dart index ffe90aee..353786c2 100644 --- a/lib/logic/cubit/services/services_state.dart +++ b/lib/logic/cubit/services/services_state.dart @@ -1,90 +1,43 @@ part of 'services_cubit.dart'; class ServicesState extends ServerInstallationDependendState { - factory ServicesState.allOn() => const ServicesState( - isPasswordManagerEnable: true, - isCloudEnable: true, - isGitEnable: true, - isSocialNetworkEnable: true, - isVpnEnable: true, - ); - - factory ServicesState.allOff() => const ServicesState( - isPasswordManagerEnable: false, - isCloudEnable: false, - isGitEnable: false, - isSocialNetworkEnable: false, - isVpnEnable: false, - ); const ServicesState({ - required this.isPasswordManagerEnable, - required this.isCloudEnable, - required this.isGitEnable, - required this.isSocialNetworkEnable, - required this.isVpnEnable, + required this.services, }); - final bool isPasswordManagerEnable; - final bool isCloudEnable; - final bool isGitEnable; - final bool isSocialNetworkEnable; - final bool isVpnEnable; + const ServicesState.empty() : this(services: const []); - ServicesState enableList( - final List list, - ) => - ServicesState( - isPasswordManagerEnable: list.contains(ServiceTypes.passwordManager) - ? true - : isPasswordManagerEnable, - isCloudEnable: list.contains(ServiceTypes.cloud) ? true : isCloudEnable, - isGitEnable: - list.contains(ServiceTypes.git) ? true : isPasswordManagerEnable, - isSocialNetworkEnable: list.contains(ServiceTypes.socialNetwork) - ? true - : isPasswordManagerEnable, - isVpnEnable: - list.contains(ServiceTypes.vpn) ? true : isPasswordManagerEnable, - ); + final List services; + bool get isPasswordManagerEnable => services.firstWhere((final service) => service.id == 'bitwarden', orElse: () => Service.empty).isEnabled; + bool get isCloudEnable => services.firstWhere((final service) => service.id == 'nextcloud', orElse: () => Service.empty).isEnabled; + bool get isGitEnable => services.firstWhere((final service) => service.id == 'gitea', orElse: () => Service.empty).isEnabled; + bool get isSocialNetworkEnable => services.firstWhere((final service) => service.id == 'pleroma', orElse: () => Service.empty).isEnabled; + bool get isVpnEnable => services.firstWhere((final service) => service.id == 'ocserv', orElse: () => Service.empty).isEnabled; - ServicesState disableList( - final List list, - ) => - ServicesState( - isPasswordManagerEnable: list.contains(ServiceTypes.passwordManager) - ? false - : isPasswordManagerEnable, - isCloudEnable: - list.contains(ServiceTypes.cloud) ? false : isCloudEnable, - isGitEnable: - list.contains(ServiceTypes.git) ? false : isPasswordManagerEnable, - isSocialNetworkEnable: list.contains(ServiceTypes.socialNetwork) - ? false - : isPasswordManagerEnable, - isVpnEnable: - list.contains(ServiceTypes.vpn) ? false : isPasswordManagerEnable, - ); + Service? getServiceById(final String id) { + final service = services.firstWhere((final service) => service.id == id, orElse: () => Service.empty); + if (service.id == 'empty') { + return null; + } + return service; + } @override List get props => [ - isPasswordManagerEnable, - isCloudEnable, - isGitEnable, - isSocialNetworkEnable, - isVpnEnable + services, ]; bool isEnableByType(final ServiceTypes type) { switch (type) { - case ServiceTypes.passwordManager: + case ServiceTypes.bitwarden: return isPasswordManagerEnable; - case ServiceTypes.cloud: + case ServiceTypes.nextcloud: return isCloudEnable; - case ServiceTypes.socialNetwork: + case ServiceTypes.pleroma: return isSocialNetworkEnable; - case ServiceTypes.git: + case ServiceTypes.gitea: return isGitEnable; - case ServiceTypes.vpn: + case ServiceTypes.ocserv: return isVpnEnable; default: throw Exception('wrong state'); diff --git a/lib/logic/models/job.dart b/lib/logic/models/job.dart index 9e694597..6ea31588 100644 --- a/lib/logic/models/job.dart +++ b/lib/logic/models/job.dart @@ -48,7 +48,7 @@ class ToggleJob extends ClientJob { required final super.title, }); - final dynamic type; + final ServiceTypes type; @override List get props => [...super.props, type]; @@ -56,7 +56,7 @@ class ToggleJob extends ClientJob { class ServiceToggleJob extends ToggleJob { ServiceToggleJob({ - required final ServiceTypes super.type, + required final super.type, required this.needToTurnOn, }) : super( title: diff --git a/lib/logic/models/service.dart b/lib/logic/models/service.dart index c0f54224..1085622a 100644 --- a/lib/logic/models/service.dart +++ b/lib/logic/models/service.dart @@ -12,6 +12,7 @@ class Service { required this.description, required this.isEnabled, required this.isRequired, + required this.isMovable, required this.status, required this.storageUsage, required this.svgIcon, @@ -26,6 +27,7 @@ class Service { description: service.description, isEnabled: service.isEnabled, isRequired: service.isRequired, + isMovable: service.isMovable, status: ServiceStatus.fromGraphQL(service.status), storageUsage: ServiceStorageUsage( used: DiskSize(byte: int.parse(service.storageUsage.usedSpace)), @@ -40,11 +42,29 @@ class Service { url: service.url, ); + static Service empty = Service( + id: 'empty', + displayName: '', + description: '', + isEnabled: false, + isRequired: false, + isMovable: false, + status: ServiceStatus.off, + storageUsage: ServiceStorageUsage( + used: DiskSize(byte: 0), + volume: '', + ), + svgIcon: '', + dnsRecords: [], + url: '', + ); + final String id; final String displayName; final String description; final bool isEnabled; final bool isRequired; + final bool isMovable; final ServiceStatus status; final ServiceStorageUsage storageUsage; final String svgIcon; diff --git a/lib/ui/components/brand_cards/brand_cards.dart b/lib/ui/components/brand_cards/brand_cards.dart index d8f48088..f3273e7a 100644 --- a/lib/ui/components/brand_cards/brand_cards.dart +++ b/lib/ui/components/brand_cards/brand_cards.dart @@ -25,9 +25,11 @@ class BrandCards { static Widget filled({ required final Widget child, final bool tertiary = false, + final bool error = false, }) => _FilledCard( tertiary: tertiary, + error: error, child: child, ); } @@ -81,10 +83,12 @@ class _FilledCard extends StatelessWidget { const _FilledCard({ required this.child, required this.tertiary, + required this.error, }); final Widget child; final bool tertiary; + final bool error; @override Widget build(final BuildContext context) => Card( elevation: 0.0, @@ -92,7 +96,8 @@ class _FilledCard extends StatelessWidget { borderRadius: BorderRadius.all(Radius.circular(12)), ), clipBehavior: Clip.antiAlias, - color: tertiary + color: error ? Theme.of(context).colorScheme.errorContainer + : tertiary ? Theme.of(context).colorScheme.tertiaryContainer : Theme.of(context).colorScheme.surfaceVariant, child: child, diff --git a/lib/ui/pages/services/service_page.dart b/lib/ui/pages/services/service_page.dart index afdd9718..75e7c7cf 100644 --- a/lib/ui/pages/services/service_page.dart +++ b/lib/ui/pages/services/service_page.dart @@ -1,9 +1,16 @@ import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; +import 'package:selfprivacy/logic/cubit/services/services_cubit.dart'; +import 'package:selfprivacy/logic/models/service.dart'; import 'package:selfprivacy/ui/components/brand_cards/brand_cards.dart'; import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; +import 'package:url_launcher/url_launcher.dart'; class ServicePage extends StatefulWidget { - const ServicePage({final super.key}); + const ServicePage({required this.serviceId, final super.key}); + + final String serviceId; @override State createState() => _ServicePageState(); @@ -11,73 +18,190 @@ class ServicePage extends StatefulWidget { class _ServicePageState extends State { @override - Widget build(final BuildContext context) => BrandHeroScreen( + Widget build(final BuildContext context) { + final Service? service = + context.watch().state.getServiceById(widget.serviceId); + + if (service == null) { + return const BrandHeroScreen( hasBackButton: true, children: [ - const SizedBox(height: 16), - Container( - alignment: Alignment.center, - child: const Icon( - Icons.question_mark_outlined, - size: 48, - ), + Center( + child: CircularProgressIndicator(), ), - const SizedBox(height: 16), - Text( - 'My Incredible Service', - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.headlineMedium!.copyWith( - color: Theme.of(context).colorScheme.onBackground, - ), - ), - const SizedBox(height: 16), - BrandCards.outlined( - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 24), - child: const Icon( - Icons.check_box_outlined, - size: 24, - ), - ), - const SizedBox(width: 16), - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 130), - child: const Text(''), - ), - ], - ), - ), - const SizedBox(height: 16), - const Divider(), - const SizedBox(height: 16), - ElevatedButton( - onPressed: null, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 24), - child: const Icon( - Icons.language_outlined, - size: 24, - ), - ), - const SizedBox(width: 16), - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 130), - child: const Text('Your Cool Domain'), - ), - ], - ), - ), - const SizedBox(height: 16), ], ); + } + return BrandHeroScreen( + hasBackButton: true, + children: [ + Container( + alignment: Alignment.center, + child: SvgPicture.string( + service.svgIcon, + width: 48.0, + height: 48.0, + ), + ), + const SizedBox(height: 16), + Text( + service.displayName, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.headlineMedium!.copyWith( + color: Theme.of(context).colorScheme.onBackground, + ), + ), + const SizedBox(height: 16), + ServiceStatusCard(status: service.status), + const SizedBox(height: 16), + if (service.url != null) + ListTile( + iconColor: Theme.of(context).colorScheme.onBackground, + onTap: () => _launchURL(service.url), + leading: const Icon(Icons.open_in_browser), + title: Text( + 'Open in browser', + style: Theme.of(context).textTheme.titleMedium, + ), + subtitle: Text( + service.url!.replaceAll('https://', ''), + style: Theme.of(context).textTheme.bodyMedium, + ), + ), + const SizedBox(height: 8), + const Divider(), + const SizedBox(height: 8), + ListTile( + iconColor: Theme.of(context).colorScheme.onBackground, + onTap: () => {}, + leading: const Icon(Icons.restart_alt_outlined), + title: Text( + 'Restart service', + style: Theme.of(context).textTheme.titleMedium, + ), + ), + ListTile( + iconColor: Theme.of(context).colorScheme.onBackground, + onTap: () => {}, + leading: const Icon(Icons.power_settings_new), + title: Text( + 'Disable service', + style: Theme.of(context).textTheme.titleMedium, + ), + ), + if (service.isMovable) + ListTile( + iconColor: Theme.of(context).colorScheme.onBackground, + onTap: () => {}, + leading: const Icon(Icons.drive_file_move_outlined), + title: Text( + 'Move to another volume', + style: Theme.of(context).textTheme.titleMedium, + ), + ), + ], + ); + } +} + +class ServiceStatusCard extends StatelessWidget { + const ServiceStatusCard({ + required this.status, + final super.key, + }); + final ServiceStatus status; + + @override + Widget build(final BuildContext context) { + switch (status) { + case ServiceStatus.active: + return BrandCards.filled( + child: const ListTile( + leading: Icon( + Icons.check_circle_outline, + size: 24, + ), + title: Text('Up and running'), + ), + ); + case ServiceStatus.inactive: + return BrandCards.filled( + child: const ListTile( + leading: Icon( + Icons.stop_circle_outlined, + size: 24, + ), + title: Text('Stopped'), + ), + tertiary: true, + ); + case ServiceStatus.failed: + return BrandCards.filled( + child: const ListTile( + leading: Icon( + Icons.error_outline, + size: 24, + ), + title: Text('Failed to start'), + ), + error: true, + ); + case ServiceStatus.off: + return BrandCards.filled( + child: const ListTile( + leading: Icon( + Icons.power_settings_new, + size: 24, + ), + title: Text('Disabled'), + ), + tertiary: true, + ); + case ServiceStatus.activating: + return BrandCards.filled( + child: const ListTile( + leading: Icon( + Icons.restart_alt_outlined, + size: 24, + ), + title: Text('Activating'), + ), + tertiary: true, + ); + case ServiceStatus.deactivating: + return BrandCards.filled( + child: const ListTile( + leading: Icon( + Icons.restart_alt_outlined, + size: 24, + ), + title: Text('Deactivating'), + ), + tertiary: true, + ); + case ServiceStatus.reloading: + return BrandCards.filled( + child: const ListTile( + leading: Icon( + Icons.restart_alt_outlined, + size: 24, + ), + title: Text('Restarting'), + ), + tertiary: true, + ); + } + } +} + +void _launchURL(final url) async { + try { + final Uri uri = Uri.parse(url); + await launchUrl( + uri, + mode: LaunchMode.externalApplication, + ); + } catch (e) { + print(e); + } } diff --git a/lib/ui/pages/services/services.dart b/lib/ui/pages/services/services.dart index 0a2b7db4..ba8465d2 100644 --- a/lib/ui/pages/services/services.dart +++ b/lib/ui/pages/services/services.dart @@ -1,16 +1,13 @@ import 'dart:ui'; 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/logic/common_enum/common_enum.dart'; import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; import 'package:selfprivacy/logic/cubit/jobs/jobs_cubit.dart'; import 'package:selfprivacy/logic/cubit/services/services_cubit.dart'; import 'package:selfprivacy/logic/models/job.dart'; import 'package:selfprivacy/logic/models/state_types.dart'; -import 'package:selfprivacy/ui/components/brand_button/brand_button.dart'; import 'package:selfprivacy/ui/components/brand_cards/brand_cards.dart'; import 'package:selfprivacy/ui/components/brand_header/brand_header.dart'; import 'package:selfprivacy/ui/components/brand_switch/brand_switch.dart'; @@ -18,17 +15,17 @@ import 'package:selfprivacy/ui/components/brand_text/brand_text.dart'; import 'package:selfprivacy/ui/components/icon_status_mask/icon_status_mask.dart'; import 'package:selfprivacy/ui/components/not_ready_card/not_ready_card.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:selfprivacy/ui/pages/services/service_page.dart'; +import 'package:selfprivacy/utils/route_transitions/basic.dart'; import 'package:selfprivacy/utils/ui_helpers.dart'; -import 'package:url_launcher/url_launcher_string.dart'; - -import 'package:selfprivacy/ui/pages/root_route.dart'; +import 'package:url_launcher/url_launcher.dart'; const switchableServices = [ - ServiceTypes.passwordManager, - ServiceTypes.cloud, - ServiceTypes.socialNetwork, - ServiceTypes.git, - ServiceTypes.vpn, + ServiceTypes.bitwarden, + ServiceTypes.nextcloud, + ServiceTypes.pleroma, + ServiceTypes.gitea, + ServiceTypes.ocserv, ]; class ServicesPage extends StatefulWidget { @@ -39,18 +36,14 @@ class ServicesPage extends StatefulWidget { } void _launchURL(final url) async { - final canLaunch = await canLaunchUrlString(url); - - if (canLaunch) { - try { - await launchUrlString( - url, - ); - } catch (e) { - print(e); - } - } else { - throw 'Could not launch $url'; + try { + final Uri uri = Uri.parse(url); + await launchUrl( + uri, + mode: LaunchMode.externalApplication, + ); + } catch (e) { + print(e); } } @@ -67,23 +60,28 @@ class _ServicesPageState extends State { title: 'basis.services'.tr(), ), ), - body: ListView( - padding: paddingH15V0, - children: [ - BrandText.body1('services.title'.tr()), - const SizedBox(height: 24), - if (!isReady) ...[const NotReadyCard(), const SizedBox(height: 24)], - ...ServiceTypes.values - .map( - (final t) => Padding( - padding: const EdgeInsets.only( - bottom: 30, + body: RefreshIndicator( + onRefresh: () async { + context.read().reload(); + }, + child: ListView( + padding: paddingH15V0, + children: [ + BrandText.body1('services.title'.tr()), + const SizedBox(height: 24), + if (!isReady) ...[const NotReadyCard(), const SizedBox(height: 24)], + ...ServiceTypes.values + .map( + (final t) => Padding( + padding: const EdgeInsets.only( + bottom: 30, + ), + child: _Card(serviceType: t), ), - child: _Card(serviceType: t), - ), - ) - .toList() - ], + ) + .toList() + ], + ), ), ); } @@ -97,7 +95,6 @@ class _Card extends StatelessWidget { Widget build(final BuildContext context) { final isReady = context.watch().state is ServerInstallationFinished; - final changeTab = context.read().onPress; final serviceState = context.watch().state; final jobsCubit = context.watch(); @@ -118,21 +115,11 @@ class _Card extends StatelessWidget { final domainName = UiHelpers.getDomainName(config); return GestureDetector( - onTap: isSwitchOn - ? () => showDialog( - context: context, - // isScrollControlled: true, - // backgroundColor: Colors.transparent, - builder: (final BuildContext context) => _ServiceDetails( - serviceType: serviceType, - status: - isSwitchOn ? StateType.stable : StateType.uninitialized, - title: serviceType.title, - icon: serviceType.icon, - changeTab: changeTab, - ), - ) - : null, + onTap: () => Navigator.of(context).push( + materialRoute( + ServicePage(serviceId: serviceType.name) + ) + ), child: BrandCards.big( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -202,7 +189,7 @@ class _Card extends StatelessWidget { const SizedBox(height: 10), ], ), - if (serviceType == ServiceTypes.mail) + if (serviceType == ServiceTypes.mailserver) Column( children: [ Text( @@ -246,257 +233,3 @@ class _Card extends StatelessWidget { ); } } - -class _ServiceDetails extends StatelessWidget { - const _ServiceDetails({ - required this.serviceType, - required this.icon, - required this.status, - required this.title, - required this.changeTab, - }); - - final ServiceTypes serviceType; - final IconData icon; - final StateType status; - final String title; - final ValueChanged changeTab; - - @override - Widget build(final BuildContext context) { - late Widget child; - - final config = context.watch().state; - final domainName = UiHelpers.getDomainName(config); - - final linksStyle = body1Style.copyWith( - fontSize: 15, - color: Theme.of(context).brightness == Brightness.dark - ? Colors.white - : BrandColors.black, - fontWeight: FontWeight.bold, - decoration: TextDecoration.underline, - ); - - final textStyle = body1Style.copyWith( - color: Theme.of(context).brightness == Brightness.dark - ? Colors.white - : BrandColors.black, - ); - switch (serviceType) { - case ServiceTypes.mail: - child = RichText( - text: TextSpan( - children: [ - TextSpan( - text: 'services.mail.bottom_sheet.1'.tr(args: [domainName]), - style: textStyle, - ), - const WidgetSpan(child: SizedBox(width: 5)), - WidgetSpan( - child: Padding( - padding: const EdgeInsets.only(bottom: 0.8), - child: GestureDetector( - child: Text( - 'services.mail.bottom_sheet.2'.tr(), - style: linksStyle, - ), - onTap: () { - Navigator.of(context).pop(); - changeTab(2); - }, - ), - ), - ), - ], - ), - ); - break; - case ServiceTypes.messenger: - child = RichText( - text: TextSpan( - children: [ - TextSpan( - text: - 'services.messenger.bottom_sheet.1'.tr(args: [domainName]), - style: textStyle, - ) - ], - ), - ); - break; - case ServiceTypes.passwordManager: - child = RichText( - text: TextSpan( - children: [ - TextSpan( - text: 'services.password_manager.bottom_sheet.1' - .tr(args: [domainName]), - style: textStyle, - ), - const WidgetSpan(child: SizedBox(width: 5)), - WidgetSpan( - child: Padding( - padding: const EdgeInsets.only(bottom: 0.8), - child: GestureDetector( - onTap: () => _launchURL('https://password.$domainName'), - child: Text( - 'password.$domainName', - style: linksStyle, - ), - ), - ), - ), - ], - ), - ); - break; - case ServiceTypes.video: - child = RichText( - text: TextSpan( - children: [ - TextSpan( - text: 'services.video.bottom_sheet.1'.tr(args: [domainName]), - style: textStyle, - ), - const WidgetSpan(child: SizedBox(width: 5)), - WidgetSpan( - child: Padding( - padding: const EdgeInsets.only(bottom: 0.8), - child: GestureDetector( - onTap: () => _launchURL('https://meet.$domainName'), - child: Text( - 'meet.$domainName', - style: linksStyle, - ), - ), - ), - ), - ], - ), - ); - break; - case ServiceTypes.cloud: - child = RichText( - text: TextSpan( - children: [ - TextSpan( - text: 'services.cloud.bottom_sheet.1'.tr(args: [domainName]), - style: textStyle, - ), - const WidgetSpan(child: SizedBox(width: 5)), - WidgetSpan( - child: Padding( - padding: const EdgeInsets.only(bottom: 0.8), - child: GestureDetector( - onTap: () => _launchURL('https://cloud.$domainName'), - child: Text( - 'cloud.$domainName', - style: linksStyle, - ), - ), - ), - ), - ], - ), - ); - break; - case ServiceTypes.socialNetwork: - child = RichText( - text: TextSpan( - children: [ - TextSpan( - text: 'services.social_network.bottom_sheet.1' - .tr(args: [domainName]), - style: textStyle, - ), - const WidgetSpan(child: SizedBox(width: 5)), - WidgetSpan( - child: Padding( - padding: const EdgeInsets.only(bottom: 0.8), - child: GestureDetector( - onTap: () => _launchURL('https://social.$domainName'), - child: Text( - 'social.$domainName', - style: linksStyle, - ), - ), - ), - ), - ], - ), - ); - break; - case ServiceTypes.git: - child = RichText( - text: TextSpan( - children: [ - TextSpan( - text: 'services.git.bottom_sheet.1'.tr(args: [domainName]), - style: textStyle, - ), - const WidgetSpan(child: SizedBox(width: 5)), - WidgetSpan( - child: Padding( - padding: const EdgeInsets.only(bottom: 0.8), - child: GestureDetector( - onTap: () => _launchURL('https://git.$domainName'), - child: Text( - 'git.$domainName', - style: linksStyle, - ), - ), - ), - ), - ], - ), - ); - break; - case ServiceTypes.vpn: - child = Text( - 'services.vpn.bottom_sheet.1'.tr(), - ); - } - - return Dialog( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), - ), - child: SingleChildScrollView( - child: SizedBox( - width: 350, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: paddingH15V30, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - IconStatusMask( - status: status, - child: Icon(icon, size: 40, color: Colors.white), - ), - const SizedBox(height: 10), - BrandText.h2(title), - const SizedBox(height: 10), - child, - const SizedBox(height: 40), - Center( - child: Container( - child: BrandButton.rised( - onPressed: () => Navigator.of(context).pop(), - text: 'basis.close'.tr(), - ), - ), - ), - ], - ), - ) - ], - ), - ), - ), - ); - } -}