diff --git a/.editorconfig b/.editorconfig index 80a3e35b..206f5ceb 100644 --- a/.editorconfig +++ b/.editorconfig @@ -14,3 +14,6 @@ max_line_length = 150 [*.md] trim_trailing_whitespace = false + +[*.json] +indent_size = 4 diff --git a/analysis_options.yaml b/analysis_options.yaml index 72343f80..ff0f9356 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -29,16 +29,16 @@ linter: # producing the lint. rules: avoid_print: false # Uncomment to disable the `avoid_print` rule - prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule - always_use_package_imports: true - no_adjacent_strings_in_list: true - unnecessary_statements: true always_declare_return_types: true - always_put_required_named_parameters_first: true always_put_control_body_on_new_line: true + always_put_required_named_parameters_first: true + always_use_package_imports: true avoid_escaping_inner_quotes: true avoid_setters_without_getters: true + collection_methods_unrelated_type: true + combinators_ordering: true eol_at_end_of_file: true + no_adjacent_strings_in_list: true prefer_constructors_over_static_methods: true prefer_expression_function_bodies: true prefer_final_in_for_each: true @@ -48,12 +48,18 @@ linter: prefer_if_elements_to_conditional_expressions: true prefer_mixin: true prefer_null_aware_method_calls: true + prefer_single_quotes: true require_trailing_commas: true sized_box_shrink_expand: true sort_constructors_first: true + unawaited_futures: true unnecessary_await_in_return: true + unnecessary_null_aware_operator_on_extension_on_nullable: true unnecessary_null_checks: true unnecessary_parenthesis: true + unnecessary_statements: true + unnecessary_to_list_in_spreads: true + unreachable_from_main: true use_enums: true use_if_null_to_convert_nulls_to_bools: true use_is_even_rather_than_modulo: true @@ -61,6 +67,7 @@ linter: use_named_constants: true use_setters_to_change_properties: true use_string_buffers: true + use_string_in_part_of_directives: true use_super_parameters: true use_to_and_as_if_applicable: true diff --git a/assets/translations/en.json b/assets/translations/en.json index a2cb16dc..f20d4bc4 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -2,6 +2,7 @@ "test": "en-test", "locale": "en", "basis": { + "app_name": "SelfPrivacy", "providers": "Providers", "providers_title": "Your Data Center", "select": "Select", @@ -46,7 +47,8 @@ }, "console_page": { "title": "Console", - "waiting": "Waiting for initialization…" + "waiting": "Waiting for initialization…", + "copy": "Copy" }, "about_us_page": { "title": "About us" @@ -59,8 +61,11 @@ }, "application_settings": { "title": "Application settings", + "system_dark_theme_title": "System default theme", + "system_dark_theme_description": "Use light or dark theme depending on system settings", "dark_theme_title": "Dark theme", "dark_theme_description": "Switch your application theme", + "dangerous_settings": "Dangerous settings", "reset_config_title": "Reset application config", "reset_config_description": "Reset api keys and root user", "delete_server_title": "Delete server", @@ -251,6 +256,7 @@ "subtitle": "Private VPN server" }, "users": { + "details_title": "User details", "add_new_user": "Add a first user", "new_user": "New user", "delete_user": "Delete user", @@ -335,7 +341,20 @@ "create_master_account": "Create master account", "enter_username_and_password": "Enter username and strong password", "finish": "Everything is initialized", - "checks": "Checks have been completed \n{} out of {}" + "checks": "Checks have been completed \n{} out of {}", + "steps": { + "hosting": "Hosting", + "server_type": "Server type", + "dns_provider": "DNS provider", + "backups_provider": "Backups", + "domain": "Domain", + "master_account": "Master account", + "server": "Server", + "dns_setup": "DNS setup", + "nixos_installation": "NixOS installation", + "server_reboot": "Server reboot", + "final_checks": "Final checks" + } }, "recovering": { "generic_error": "Operation failed, please try again.", @@ -478,5 +497,19 @@ "root_name": "Cannot be 'root'", "length_not_equal": "Length is [], should be {}", "length_longer": "Length is [], should be shorter than or equal to {}" + }, + "support": { + "title": "SelfPrivacy Support" + }, + "developer_settings": { + "title": "Developer settings", + "subtitle": "These settings are for debugging only. Don't change them unless you know what you're doing.", + "server_setup": "Server setup", + "use_staging_acme": "Use staging ACME server", + "use_staging_acme_description": "Rebuild your app to change this value.", + "routing": "App routing", + "reset_onboarding": "Reset onboarding switch", + "reset_onboarding_description": "Reset onboarding switch to show onboarding screen again", + "cubit_statuses": "Cubit loading statuses" } } diff --git a/lib/config/bloc_config.dart b/lib/config/bloc_config.dart index 6c870f9e..381261fe 100644 --- a/lib/config/bloc_config.dart +++ b/lib/config/bloc_config.dart @@ -12,6 +12,7 @@ import 'package:selfprivacy/logic/cubit/providers/providers_cubit.dart'; import 'package:selfprivacy/logic/cubit/server_jobs/server_jobs_cubit.dart'; import 'package:selfprivacy/logic/cubit/server_volumes/server_volume_cubit.dart'; import 'package:selfprivacy/logic/cubit/services/services_cubit.dart'; +import 'package:selfprivacy/logic/cubit/support_system/support_system_cubit.dart'; import 'package:selfprivacy/logic/cubit/users/users_cubit.dart'; import 'package:selfprivacy/logic/cubit/provider_volumes/provider_volume_cubit.dart'; @@ -23,7 +24,9 @@ class BlocAndProviderConfig extends StatelessWidget { @override Widget build(final BuildContext context) { const isDark = false; + const isAutoDark = true; final serverInstallationCubit = ServerInstallationCubit()..load(); + final supportSystemCubit = SupportSystemCubit(); final usersCubit = UsersCubit(serverInstallationCubit); final servicesCubit = ServicesCubit(serverInstallationCubit); final backupsCubit = BackupsCubit(serverInstallationCubit); @@ -41,9 +44,13 @@ class BlocAndProviderConfig extends StatelessWidget { BlocProvider( create: (final _) => AppSettingsCubit( isDarkModeOn: isDark, + isAutoDarkModeOn: isAutoDark, isOnboardingShowing: true, )..load(), ), + BlocProvider( + create: (final _) => supportSystemCubit, + ), BlocProvider( create: (final _) => serverInstallationCubit, lazy: false, diff --git a/lib/config/brand_colors.dart b/lib/config/brand_colors.dart index 15d1433a..335e652e 100644 --- a/lib/config/brand_colors.dart +++ b/lib/config/brand_colors.dart @@ -2,53 +2,16 @@ import 'package:flutter/material.dart'; class BrandColors { static const Color blue = Color(0xFF093CEF); - static const Color white = Colors.white; - static const Color black = Colors.black; - - static const Color gray1 = Color(0xFF555555); - static const Color gray2 = Color(0xFF7C7C7C); - static const Color gray3 = Color(0xFFFAFAFA); - static const Color gray4 = Color(0xFFDDDDDD); - static const Color gray5 = Color(0xFFEDEEF1); - static Color gray6 = const Color(0xFF181818).withOpacity(0.7); - static const Color grey7 = Color(0xFFABABAB); - - static const Color red1 = Color(0xFFFA0E0E); - static const Color red2 = Color(0xFFE65527); - - static const Color green1 = Color(0xFF00AF54); - - static const Color green2 = Color(0xFF0F8849); - - static Color get navBackgroundLight => white.withOpacity(0.8); - static Color get navBackgroundDark => black.withOpacity(0.8); static const List uninitializedGradientColors = [ Color(0xFF555555), Color(0xFFABABAB), ]; - static const List stableGradientColors = [ - Color(0xFF093CEF), - Color(0xFF14A1CB), - ]; - static const List progressGradientColors = [ - Color(0xFF093CEF), - Color(0xFF14A1CB), - ]; static const List warningGradientColors = [ Color(0xFFEF4E09), Color(0xFFEFD135), ]; static const Color primary = blue; - static const Color headlineColor = black; - static const Color inactive = gray2; - static const Color scaffoldBackground = gray3; - static const Color inputInactive = gray4; - - static const Color textColor1 = black; - static const Color textColor2 = gray1; - static const Color dividerColor = gray5; - static const Color warning = red1; } diff --git a/lib/config/hive_config.dart b/lib/config/hive_config.dart index b300e247..ca703da5 100644 --- a/lib/config/hive_config.dart +++ b/lib/config/hive_config.dart @@ -35,8 +35,8 @@ class HiveConfig { final Box deprecatedUsers = Hive.box(BNames.usersDeprecated); if (deprecatedUsers.isNotEmpty) { final Box users = Hive.box(BNames.usersBox); - users.addAll(deprecatedUsers.values.toList()); - deprecatedUsers.clear(); + await users.addAll(deprecatedUsers.values.toList()); + await deprecatedUsers.clear(); } await Hive.openBox(BNames.serverInstallationBox, encryptionCipher: cipher); @@ -63,6 +63,9 @@ class BNames { /// A boolean field of [appSettingsBox] box. static String isDarkModeOn = 'isDarkModeOn'; + /// A boolean field of [appSettingsBox] box. + static String isAutoDarkModeOn = 'isAutoDarkModeOn'; + /// A boolean field of [appSettingsBox] box. static String isOnboardingShowing = 'isOnboardingShowing'; diff --git a/lib/config/text_themes.dart b/lib/config/text_themes.dart deleted file mode 100644 index 63b4b99c..00000000 --- a/lib/config/text_themes.dart +++ /dev/null @@ -1,80 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:selfprivacy/utils/named_font_weight.dart'; - -import 'package:selfprivacy/config/brand_colors.dart'; - -const TextStyle defaultTextStyle = TextStyle( - fontSize: 15, - color: BrandColors.textColor1, -); - -final TextStyle headline1Style = defaultTextStyle.copyWith( - fontSize: 40, - fontWeight: NamedFontWeight.extraBold, - color: BrandColors.headlineColor, -); - -final TextStyle headline2Style = defaultTextStyle.copyWith( - fontSize: 24, - fontWeight: NamedFontWeight.extraBold, - color: BrandColors.headlineColor, -); - -final TextStyle onboardingTitle = defaultTextStyle.copyWith( - fontSize: 30, - fontWeight: NamedFontWeight.extraBold, - color: BrandColors.headlineColor, -); - -final TextStyle headline3Style = defaultTextStyle.copyWith( - fontSize: 20, - fontWeight: NamedFontWeight.extraBold, - color: BrandColors.headlineColor, -); - -final TextStyle headline4Style = defaultTextStyle.copyWith( - fontSize: 18, - fontWeight: NamedFontWeight.medium, - color: BrandColors.headlineColor, -); - -final TextStyle headline4UnderlinedStyle = defaultTextStyle.copyWith( - fontSize: 18, - fontWeight: NamedFontWeight.medium, - color: BrandColors.headlineColor, - decoration: TextDecoration.underline, -); - -final TextStyle headline5Style = defaultTextStyle.copyWith( - fontSize: 15, - fontWeight: NamedFontWeight.medium, - color: BrandColors.headlineColor.withOpacity(0.8), -); - -const TextStyle body1Style = defaultTextStyle; -final TextStyle body2Style = defaultTextStyle.copyWith( - color: BrandColors.textColor2, -); - -final TextStyle buttonTitleText = defaultTextStyle.copyWith( - color: BrandColors.white, - fontSize: 16, - fontWeight: FontWeight.bold, - height: 1, -); - -final TextStyle mediumStyle = - defaultTextStyle.copyWith(fontSize: 13, height: 1.53); - -final TextStyle smallStyle = - defaultTextStyle.copyWith(fontSize: 11, height: 1.45); - -const TextStyle progressTextStyleLight = TextStyle( - fontSize: 11, - color: BrandColors.textColor1, - height: 1.7, -); - -final TextStyle progressTextStyleDark = progressTextStyleLight.copyWith( - color: BrandColors.white, -); diff --git a/lib/logic/api_maps/graphql_maps/api_map.dart b/lib/logic/api_maps/graphql_maps/api_map.dart index 02b9bc04..a633866e 100644 --- a/lib/logic/api_maps/graphql_maps/api_map.dart +++ b/lib/logic/api_maps/graphql_maps/api_map.dart @@ -20,7 +20,13 @@ class RequestLoggingLink extends Link { final Request request, [ final NextLink? forward, ]) async* { - _logToAppConsole(request); + getIt.get().addMessage( + GraphQlRequestMessage( + operation: request.operation, + variables: request.variables, + context: request.context, + ), + ); yield* forward!(request); } } @@ -29,7 +35,13 @@ class ResponseLoggingParser extends ResponseParser { @override Response parseResponse(final Map body) { final response = super.parseResponse(body); - _logToAppConsole(response); + getIt.get().addMessage( + GraphQlResponseMessage( + data: response.data, + errors: response.errors, + context: response.context, + ), + ); return response; } diff --git a/lib/logic/api_maps/rest_maps/api_map.dart b/lib/logic/api_maps/rest_maps/api_map.dart index 6fd0bdda..299837fa 100644 --- a/lib/logic/api_maps/rest_maps/api_map.dart +++ b/lib/logic/api_maps/rest_maps/api_map.dart @@ -65,9 +65,11 @@ class ConsoleInterceptor extends InterceptorsWrapper { final RequestInterceptorHandler handler, ) async { addMessage( - Message( - text: - 'request-uri: ${options.uri}\nheaders: ${options.headers}\ndata: ${options.data}', + RestApiRequestMessage( + method: options.method, + data: options.data.toString(), + headers: options.headers, + uri: options.uri, ), ); return super.onRequest(options, handler); @@ -79,9 +81,11 @@ class ConsoleInterceptor extends InterceptorsWrapper { final ResponseInterceptorHandler handler, ) async { addMessage( - Message( - text: - 'response-uri: ${response.realUri}\ncode: ${response.statusCode}\ndata: ${response.toString()}\n', + RestApiResponseMessage( + method: response.requestOptions.method, + statusCode: response.statusCode, + data: response.data.toString(), + uri: response.realUri, ), ); return super.onResponse( diff --git a/lib/logic/cubit/app_settings/app_settings_cubit.dart b/lib/logic/cubit/app_settings/app_settings_cubit.dart index d013d418..af1ab3e0 100644 --- a/lib/logic/cubit/app_settings/app_settings_cubit.dart +++ b/lib/logic/cubit/app_settings/app_settings_cubit.dart @@ -15,10 +15,12 @@ part 'app_settings_state.dart'; class AppSettingsCubit extends Cubit { AppSettingsCubit({ required final bool isDarkModeOn, + required final bool isAutoDarkModeOn, required final bool isOnboardingShowing, }) : super( AppSettingsState( isDarkModeOn: isDarkModeOn, + isAutoDarkModeOn: isAutoDarkModeOn, isOnboardingShowing: isOnboardingShowing, ), ); @@ -27,10 +29,12 @@ class AppSettingsCubit extends Cubit { void load() async { final bool? isDarkModeOn = box.get(BNames.isDarkModeOn); + final bool? isAutoDarkModeOn = box.get(BNames.isAutoDarkModeOn); final bool? isOnboardingShowing = box.get(BNames.isOnboardingShowing); emit( state.copyWith( isDarkModeOn: isDarkModeOn, + isAutoDarkModeOn: isAutoDarkModeOn, isOnboardingShowing: isOnboardingShowing, ), ); @@ -49,9 +53,14 @@ class AppSettingsCubit extends Cubit { emit(state.copyWith(isDarkModeOn: isDarkModeOn)); } - void turnOffOnboarding() { - box.put(BNames.isOnboardingShowing, false); + void updateAutoDarkMode({required final bool isAutoDarkModeOn}) { + box.put(BNames.isAutoDarkModeOn, isAutoDarkModeOn); + emit(state.copyWith(isAutoDarkModeOn: isAutoDarkModeOn)); + } - emit(state.copyWith(isOnboardingShowing: false)); + void turnOffOnboarding({final bool isOnboardingShowing = false}) { + box.put(BNames.isOnboardingShowing, isOnboardingShowing); + + emit(state.copyWith(isOnboardingShowing: isOnboardingShowing)); } } diff --git a/lib/logic/cubit/app_settings/app_settings_state.dart b/lib/logic/cubit/app_settings/app_settings_state.dart index 8b29f6e9..ad364d66 100644 --- a/lib/logic/cubit/app_settings/app_settings_state.dart +++ b/lib/logic/cubit/app_settings/app_settings_state.dart @@ -3,21 +3,25 @@ part of 'app_settings_cubit.dart'; class AppSettingsState extends Equatable { const AppSettingsState({ required this.isDarkModeOn, + required this.isAutoDarkModeOn, required this.isOnboardingShowing, this.corePalette, }); final bool isDarkModeOn; + final bool isAutoDarkModeOn; final bool isOnboardingShowing; final color_utils.CorePalette? corePalette; AppSettingsState copyWith({ final bool? isDarkModeOn, + final bool? isAutoDarkModeOn, final bool? isOnboardingShowing, final color_utils.CorePalette? corePalette, }) => AppSettingsState( isDarkModeOn: isDarkModeOn ?? this.isDarkModeOn, + isAutoDarkModeOn: isAutoDarkModeOn ?? this.isAutoDarkModeOn, isOnboardingShowing: isOnboardingShowing ?? this.isOnboardingShowing, corePalette: corePalette ?? this.corePalette, ); @@ -26,5 +30,6 @@ class AppSettingsState extends Equatable { corePalette ?? color_utils.CorePalette.of(BrandColors.primary.value); @override - List get props => [isDarkModeOn, isOnboardingShowing, corePalette]; + List get props => + [isDarkModeOn, isAutoDarkModeOn, isOnboardingShowing, corePalette]; } diff --git a/lib/logic/cubit/devices/devices_cubit.dart b/lib/logic/cubit/devices/devices_cubit.dart index 5e5c145c..d76e3651 100644 --- a/lib/logic/cubit/devices/devices_cubit.dart +++ b/lib/logic/cubit/devices/devices_cubit.dart @@ -15,9 +15,9 @@ class ApiDevicesCubit @override void load() async { - if (serverInstallationCubit.state is ServerInstallationFinished) { - _refetch(); - } + // if (serverInstallationCubit.state is ServerInstallationFinished) { + _refetch(); + // } } Future refresh() async { diff --git a/lib/logic/cubit/forms/setup/initializing/domain_setup_cubit.dart b/lib/logic/cubit/forms/setup/initializing/domain_setup_cubit.dart index 62fc1050..b1dedcd0 100644 --- a/lib/logic/cubit/forms/setup/initializing/domain_setup_cubit.dart +++ b/lib/logic/cubit/forms/setup/initializing/domain_setup_cubit.dart @@ -22,9 +22,6 @@ class DomainSetupCubit extends Cubit { } } - @override - Future close() => super.close(); - Future saveDomain() async { assert(state is Loaded, 'wrong state'); final String domainName = (state as Loaded).domain; diff --git a/lib/logic/cubit/forms/user/user_form_cubit.dart b/lib/logic/cubit/forms/user/user_form_cubit.dart index c3712382..5524417d 100644 --- a/lib/logic/cubit/forms/user/user_form_cubit.dart +++ b/lib/logic/cubit/forms/user/user_form_cubit.dart @@ -36,10 +36,6 @@ class UserFormCubit extends FormCubit { @override FutureOr onSubmit() { - print('onSubmit'); - print('initialUser: $initialUser'); - print('login: ${login.state.value}'); - print('password: ${password.state.value}'); if (initialUser == null) { final User user = User( login: login.state.value, diff --git a/lib/logic/cubit/provider_volumes/provider_volume_cubit.dart b/lib/logic/cubit/provider_volumes/provider_volume_cubit.dart index ff8eb797..48617545 100644 --- a/lib/logic/cubit/provider_volumes/provider_volume_cubit.dart +++ b/lib/logic/cubit/provider_volumes/provider_volume_cubit.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:easy_localization/easy_localization.dart'; import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server_api.dart'; @@ -20,7 +22,7 @@ class ApiProviderVolumeCubit @override Future load() async { if (serverInstallationCubit.state is ServerInstallationFinished) { - _refetch(); + unawaited(_refetch()); } } @@ -31,7 +33,7 @@ class ApiProviderVolumeCubit Future refresh() async { emit(const ApiProviderVolumeState([], LoadingStatus.refreshing, false)); - _refetch(); + unawaited(_refetch()); } Future _refetch() async { @@ -56,14 +58,14 @@ class ApiProviderVolumeCubit await ApiController.currentVolumeProviderApiFactory! .getVolumeProvider() .attachVolume(volume.providerVolume!, server.id); - refresh(); + unawaited(refresh()); } Future detachVolume(final DiskVolume volume) async { await ApiController.currentVolumeProviderApiFactory! .getVolumeProvider() .detachVolume(volume.providerVolume!); - refresh(); + unawaited(refresh()); } Future resizeVolume( @@ -125,14 +127,14 @@ class ApiProviderVolumeCubit await Future.delayed(const Duration(seconds: 10)); await ServerApi().mountVolume(volume!.name); - refresh(); + unawaited(refresh()); } Future deleteVolume(final DiskVolume volume) async { await ApiController.currentVolumeProviderApiFactory! .getVolumeProvider() .deleteVolume(volume.providerVolume!); - refresh(); + unawaited(refresh()); } @override diff --git a/lib/logic/cubit/recovery_key/recovery_key_cubit.dart b/lib/logic/cubit/recovery_key/recovery_key_cubit.dart index 56800be3..87309654 100644 --- a/lib/logic/cubit/recovery_key/recovery_key_cubit.dart +++ b/lib/logic/cubit/recovery_key/recovery_key_cubit.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server_api.dart'; import 'package:selfprivacy/logic/common_enum/common_enum.dart'; import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart'; @@ -14,21 +16,21 @@ class RecoveryKeyCubit @override void load() async { - if (serverInstallationCubit.state is ServerInstallationFinished) { - final RecoveryKeyStatus? status = await _getRecoveryKeyStatus(); - if (status == null) { - emit(state.copyWith(loadingStatus: LoadingStatus.error)); - } else { - emit( - state.copyWith( - status: status, - loadingStatus: LoadingStatus.success, - ), - ); - } + // if (serverInstallationCubit.state is ServerInstallationFinished) { + final RecoveryKeyStatus? status = await _getRecoveryKeyStatus(); + if (status == null) { + emit(state.copyWith(loadingStatus: LoadingStatus.error)); } else { - emit(state.copyWith(loadingStatus: LoadingStatus.uninitialized)); + emit( + state.copyWith( + status: status, + loadingStatus: LoadingStatus.success, + ), + ); } + // } else { + // emit(state.copyWith(loadingStatus: LoadingStatus.uninitialized)); + // } } Future _getRecoveryKeyStatus() async { @@ -60,7 +62,7 @@ class RecoveryKeyCubit final APIGenericResult response = await api.generateRecoveryToken(expirationDate, numberOfUses); if (response.success) { - refresh(); + unawaited(refresh()); return response.data; } else { throw GenerationError(response.message ?? 'Unknown error'); diff --git a/lib/logic/cubit/server_installation/server_installation_cubit.dart b/lib/logic/cubit/server_installation/server_installation_cubit.dart index 60a801fd..e387ddc5 100644 --- a/lib/logic/cubit/server_installation/server_installation_cubit.dart +++ b/lib/logic/cubit/server_installation/server_installation_cubit.dart @@ -215,7 +215,7 @@ class ServerInstallationCubit extends Cubit { void setDnsApiToken(final String dnsApiToken) async { if (state is ServerInstallationRecovery) { - setAndValidateCloudflareToken(dnsApiToken); + await setAndValidateCloudflareToken(dnsApiToken); return; } await repository.setDnsApiToken(dnsApiToken); diff --git a/lib/logic/cubit/server_installation/server_installation_repository.dart b/lib/logic/cubit/server_installation/server_installation_repository.dart index 3b787a70..c5b38868 100644 --- a/lib/logic/cubit/server_installation/server_installation_repository.dart +++ b/lib/logic/cubit/server_installation/server_installation_repository.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:io'; import 'package:basic_utils/basic_utils.dart'; @@ -276,7 +277,7 @@ class ServerInstallationRepository { return; } await saveServerDetails(serverDetails); - onSuccess(serverDetails); + unawaited(onSuccess(serverDetails)); }, cancelButtonOnPressed: onCancel, ); @@ -326,15 +327,15 @@ class ServerInstallationRepository { return; } await saveServerDetails(serverDetails); - onSuccess(serverDetails); + unawaited(onSuccess(serverDetails)); }, cancelButtonOnPressed: onCancel, ); return; } - saveServerDetails(createServerResult.data!); - onSuccess(createServerResult.data!); + await saveServerDetails(createServerResult.data!); + unawaited(onSuccess(createServerResult.data!)); } catch (e) { print(e); showInstallationErrorPopUp(); diff --git a/lib/logic/cubit/server_volumes/server_volume_cubit.dart b/lib/logic/cubit/server_volumes/server_volume_cubit.dart index c10bc377..bf30c8c0 100644 --- a/lib/logic/cubit/server_volumes/server_volume_cubit.dart +++ b/lib/logic/cubit/server_volumes/server_volume_cubit.dart @@ -24,7 +24,7 @@ class ApiServerVolumeCubit @override Future load() async { if (serverInstallationCubit.state is ServerInstallationFinished) { - reload(); + unawaited(reload()); } } diff --git a/lib/logic/cubit/services/services_cubit.dart b/lib/logic/cubit/services/services_cubit.dart index 54e22b3d..60476c2d 100644 --- a/lib/logic/cubit/services/services_cubit.dart +++ b/lib/logic/cubit/services/services_cubit.dart @@ -53,7 +53,7 @@ class ServicesCubit extends ServerInstallationDependendCubit { } await Future.delayed(const Duration(seconds: 2)); - reload(); + unawaited(reload()); await Future.delayed(const Duration(seconds: 10)); emit( state.copyWith( @@ -62,7 +62,7 @@ class ServicesCubit extends ServerInstallationDependendCubit { .toList(), ), ); - reload(); + unawaited(reload()); } Future moveService( diff --git a/lib/logic/cubit/support_system/support_system_cubit.dart b/lib/logic/cubit/support_system/support_system_cubit.dart new file mode 100644 index 00000000..b6250740 --- /dev/null +++ b/lib/logic/cubit/support_system/support_system_cubit.dart @@ -0,0 +1,19 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; + +part 'support_system_state.dart'; + +class SupportSystemCubit extends Cubit { + SupportSystemCubit() : super(const SupportSystemState('about')); + + void showArticle({ + required final String article, + final BuildContext? context, + }) { + emit(SupportSystemState(article)); + if (context != null) { + Scaffold.of(context).openEndDrawer(); + } + } +} diff --git a/lib/logic/cubit/support_system/support_system_state.dart b/lib/logic/cubit/support_system/support_system_state.dart new file mode 100644 index 00000000..0c3c3087 --- /dev/null +++ b/lib/logic/cubit/support_system/support_system_state.dart @@ -0,0 +1,12 @@ +part of 'support_system_cubit.dart'; + +class SupportSystemState extends Equatable { + const SupportSystemState( + this.currentArticle, + ); + + final String currentArticle; + + @override + List get props => [currentArticle]; +} diff --git a/lib/logic/cubit/users/users_cubit.dart b/lib/logic/cubit/users/users_cubit.dart index 001ce8d0..2108cf93 100644 --- a/lib/logic/cubit/users/users_cubit.dart +++ b/lib/logic/cubit/users/users_cubit.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:easy_localization/easy_localization.dart'; import 'package:hive/hive.dart'; import 'package:selfprivacy/config/get_it_config.dart'; @@ -39,7 +41,7 @@ class UsersCubit extends ServerInstallationDependendCubit { ); } - refresh(); + unawaited(refresh()); } Future refresh() async { diff --git a/lib/logic/get_it/navigation.dart b/lib/logic/get_it/navigation.dart index 15adc982..90f67203 100644 --- a/lib/logic/get_it/navigation.dart +++ b/lib/logic/get_it/navigation.dart @@ -1,16 +1,19 @@ import 'package:flutter/material.dart'; -import 'package:selfprivacy/config/brand_colors.dart'; -import 'package:selfprivacy/config/text_themes.dart'; class NavigationService { final GlobalKey scaffoldMessengerKey = GlobalKey(); final GlobalKey navigatorKey = GlobalKey(); - NavigatorState? get navigator => navigatorKey.currentState; - void showPopUpDialog(final AlertDialog dialog) { - final BuildContext context = navigatorKey.currentState!.overlay!.context; + final BuildContext? context = navigatorKey.currentContext; + + if (context == null) { + showSnackBar( + 'Could not show dialog. This should not happen, please report this.', + ); + return; + } showDialog( context: context, @@ -21,8 +24,7 @@ class NavigationService { void showSnackBar(final String text) { final ScaffoldMessengerState state = scaffoldMessengerKey.currentState!; final SnackBar snack = SnackBar( - backgroundColor: BrandColors.black.withOpacity(0.8), - content: Text(text, style: buttonTitleText), + content: Text(text), duration: const Duration(seconds: 2), ); state.showSnackBar(snack); diff --git a/lib/logic/models/message.dart b/lib/logic/models/message.dart index 8bbc6dfd..aaaf0930 100644 --- a/lib/logic/models/message.dart +++ b/lib/logic/models/message.dart @@ -1,20 +1,74 @@ +import 'package:graphql/client.dart'; import 'package:intl/intl.dart'; final DateFormat formatter = DateFormat('hh:mm'); class Message { - Message({this.text, this.type = MessageType.normal}) : time = DateTime.now(); + Message({this.text, this.severity = MessageSeverity.normal}) + : time = DateTime.now(); Message.warn({this.text}) - : type = MessageType.warning, + : severity = MessageSeverity.warning, time = DateTime.now(); final String? text; final DateTime time; - final MessageType type; + final MessageSeverity severity; String get timeString => formatter.format(time); } -enum MessageType { +enum MessageSeverity { normal, warning, } + +class RestApiRequestMessage extends Message { + RestApiRequestMessage({ + this.method, + this.uri, + this.data, + this.headers, + }) : super(text: 'request-uri: $uri\nheaders: $headers\ndata: $data'); + + final String? method; + final Uri? uri; + final String? data; + final Map? headers; +} + +class RestApiResponseMessage extends Message { + RestApiResponseMessage({ + this.method, + this.uri, + this.statusCode, + this.data, + }) : super(text: 'response-uri: $uri\ncode: $statusCode\ndata: $data'); + + final String? method; + final Uri? uri; + final int? statusCode; + final String? data; +} + +class GraphQlResponseMessage extends Message { + GraphQlResponseMessage({ + this.data, + this.errors, + this.context, + }) : super(text: 'GraphQL Response\ndata: $data'); + + final Map? data; + final List? errors; + final Context? context; +} + +class GraphQlRequestMessage extends Message { + GraphQlRequestMessage({ + this.operation, + this.variables, + this.context, + }) : super(text: 'GraphQL Request\noperation: $operation'); + + final Operation? operation; + final Map? variables; + final Context? context; +} diff --git a/lib/main.dart b/lib/main.dart index 3bab9f0d..b6e1dc31 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -5,9 +5,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:selfprivacy/config/brand_colors.dart'; import 'package:selfprivacy/config/hive_config.dart'; import 'package:selfprivacy/theming/factory/app_theme_factory.dart'; -import 'package:selfprivacy/ui/pages/setup/initializing/initializing.dart'; -import 'package:selfprivacy/ui/pages/onboarding/onboarding.dart'; -import 'package:selfprivacy/ui/pages/root_route.dart'; +import 'package:selfprivacy/ui/router/router.dart'; import 'package:wakelock/wakelock.dart'; import 'package:timezone/data/latest.dart' as tz; @@ -20,7 +18,7 @@ import 'package:selfprivacy/logic/cubit/app_settings/app_settings_cubit.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); await HiveConfig.init(); - await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); + // await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); try { /// Wakelock support for Linux @@ -43,21 +41,20 @@ void main() async { fallbackColor: BrandColors.primary, ); - BlocOverrides.runZoned( - () => runApp( - Localization( - child: MyApp( - lightThemeData: lightThemeData, - darkThemeData: darkThemeData, - ), + Bloc.observer = SimpleBlocObserver(); + + runApp( + Localization( + child: SelfprivacyApp( + lightThemeData: lightThemeData, + darkThemeData: darkThemeData, ), ), - blocObserver: SimpleBlocObserver(), ); } -class MyApp extends StatelessWidget { - const MyApp({ +class SelfprivacyApp extends StatelessWidget { + SelfprivacyApp({ required this.lightThemeData, required this.darkThemeData, super.key, @@ -66,42 +63,42 @@ class MyApp extends StatelessWidget { final ThemeData lightThemeData; final ThemeData darkThemeData; + final _appRouter = RootRouter(getIt.get().navigatorKey); + @override Widget build(final BuildContext context) => Localization( - child: AnnotatedRegion( - value: SystemUiOverlayStyle.light, // Manually changing appbar color - child: BlocAndProviderConfig( - child: BlocBuilder( - builder: ( - final BuildContext context, - final AppSettingsState appSettings, - ) => - MaterialApp( - scaffoldMessengerKey: - getIt.get().scaffoldMessengerKey, - navigatorKey: getIt.get().navigatorKey, - localizationsDelegates: context.localizationDelegates, - supportedLocales: context.supportedLocales, - locale: context.locale, - debugShowCheckedModeBanner: false, - title: 'SelfPrivacy', - theme: lightThemeData, - darkTheme: darkThemeData, - themeMode: - appSettings.isDarkModeOn ? ThemeMode.dark : ThemeMode.light, - home: appSettings.isOnboardingShowing - ? const OnboardingPage(nextPage: InitializingPage()) - : const RootPage(), - builder: (final BuildContext context, final Widget? widget) { - Widget error = const Text('...rendering error...'); - if (widget is Scaffold || widget is Navigator) { - error = Scaffold(body: Center(child: error)); - } - ErrorWidget.builder = - (final FlutterErrorDetails errorDetails) => error; - return widget!; - }, - ), + child: BlocAndProviderConfig( + child: BlocBuilder( + builder: ( + final BuildContext context, + final AppSettingsState appSettings, + ) => + MaterialApp.router( + routeInformationParser: _appRouter.defaultRouteParser(), + routerDelegate: _appRouter.delegate(), + scaffoldMessengerKey: + getIt.get().scaffoldMessengerKey, + localizationsDelegates: context.localizationDelegates, + supportedLocales: context.supportedLocales, + locale: context.locale, + debugShowCheckedModeBanner: false, + title: 'SelfPrivacy', + theme: lightThemeData, + darkTheme: darkThemeData, + themeMode: appSettings.isAutoDarkModeOn + ? ThemeMode.system + : appSettings.isDarkModeOn + ? ThemeMode.dark + : ThemeMode.light, + builder: (final BuildContext context, final Widget? widget) { + Widget error = const Text('...rendering error...'); + if (widget is Scaffold || widget is Navigator) { + error = Scaffold(body: Center(child: error)); + } + ErrorWidget.builder = + (final FlutterErrorDetails errorDetails) => error; + return widget!; + }, ), ), ), diff --git a/lib/ui/components/brand_alert/brand_alert.dart b/lib/ui/components/brand_alert/brand_alert.dart deleted file mode 100644 index 352b091c..00000000 --- a/lib/ui/components/brand_alert/brand_alert.dart +++ /dev/null @@ -1,13 +0,0 @@ -import 'package:flutter/material.dart'; - -class BrandAlert extends AlertDialog { - BrandAlert({ - super.key, - final String? title, - final String? contentText, - super.actions, - }) : super( - title: title != null ? Text(title) : null, - content: title != null ? Text(contentText!) : null, - ); -} diff --git a/lib/ui/components/brand_bottom_sheet/brand_bottom_sheet.dart b/lib/ui/components/brand_bottom_sheet/brand_bottom_sheet.dart deleted file mode 100644 index d53b5ced..00000000 --- a/lib/ui/components/brand_bottom_sheet/brand_bottom_sheet.dart +++ /dev/null @@ -1,56 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:selfprivacy/config/brand_colors.dart'; - -class BrandBottomSheet extends StatelessWidget { - const BrandBottomSheet({ - required this.child, - super.key, - this.isExpended = false, - }); - - final Widget child; - final bool isExpended; - - @override - Widget build(final BuildContext context) { - final double mainHeight = MediaQuery.of(context).size.height - - MediaQuery.of(context).padding.top - - 300; - late Widget innerWidget; - if (isExpended) { - innerWidget = Scaffold( - body: child, - ); - } else { - final ThemeData themeData = Theme.of(context); - - innerWidget = Material( - color: themeData.scaffoldBackgroundColor, - child: IntrinsicHeight(child: child), - ); - } - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - Center( - child: Container( - height: 4, - width: 30, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(2), - color: BrandColors.gray4, - ), - ), - ), - const SizedBox(height: 6), - ClipRRect( - borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), - child: ConstrainedBox( - constraints: BoxConstraints(maxHeight: mainHeight), - child: innerWidget, - ), - ), - ], - ); - } -} diff --git a/lib/ui/components/brand_cards/brand_cards.dart b/lib/ui/components/brand_cards/brand_cards.dart deleted file mode 100644 index 67e7f725..00000000 --- a/lib/ui/components/brand_cards/brand_cards.dart +++ /dev/null @@ -1,55 +0,0 @@ -import 'package:flutter/material.dart'; - -class BrandCards { - static Widget big({required final Widget child}) => _BrandCard( - padding: const EdgeInsets.symmetric( - horizontal: 20, - vertical: 15, - ), - shadow: bigShadow, - borderRadius: BorderRadius.circular(20), - child: child, - ); - static Widget small({required final Widget child}) => _BrandCard( - padding: const EdgeInsets.symmetric( - horizontal: 15, - vertical: 10, - ), - shadow: bigShadow, - borderRadius: BorderRadius.circular(10), - child: child, - ); -} - -class _BrandCard extends StatelessWidget { - const _BrandCard({ - required this.child, - required this.padding, - required this.shadow, - required this.borderRadius, - }); - - final Widget child; - final EdgeInsets padding; - final List shadow; - final BorderRadius borderRadius; - - @override - Widget build(final BuildContext context) => Container( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - borderRadius: borderRadius, - boxShadow: shadow, - ), - padding: padding, - child: child, - ); -} - -final List bigShadow = [ - BoxShadow( - offset: const Offset(0, 4), - blurRadius: 8, - color: Colors.black.withOpacity(.08), - ) -]; diff --git a/lib/ui/components/brand_header/brand_header.dart b/lib/ui/components/brand_header/brand_header.dart index abdabc6f..3151aff7 100644 --- a/lib/ui/components/brand_header/brand_header.dart +++ b/lib/ui/components/brand_header/brand_header.dart @@ -25,5 +25,8 @@ class BrandHeader extends StatelessWidget { onBackButtonPressed ?? () => Navigator.of(context).pop(), ) : null, + actions: const [ + SizedBox.shrink(), + ], ); } diff --git a/lib/ui/components/brand_hero_screen/brand_hero_screen.dart b/lib/ui/components/brand_hero_screen/brand_hero_screen.dart deleted file mode 100644 index 68f5d772..00000000 --- a/lib/ui/components/brand_hero_screen/brand_hero_screen.dart +++ /dev/null @@ -1,147 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:selfprivacy/ui/components/pre_styled_buttons/flash_fab.dart'; -import 'package:selfprivacy/ui/helpers/widget_size.dart'; - -class BrandHeroScreen extends StatelessWidget { - const BrandHeroScreen({ - required this.children, - super.key, - this.hasBackButton = true, - this.hasFlashButton = true, - this.heroIcon, - this.heroIconWidget, - this.heroTitle = '', - this.heroSubtitle, - this.onBackButtonPressed, - }); - - final List children; - final bool hasBackButton; - final bool hasFlashButton; - final IconData? heroIcon; - final Widget? heroIconWidget; - final String heroTitle; - final String? heroSubtitle; - final VoidCallback? onBackButtonPressed; - - @override - Widget build(final BuildContext context) { - final Widget heroIconWidget = this.heroIconWidget ?? - Icon( - heroIcon ?? Icons.help, - size: 48.0, - color: Theme.of(context).colorScheme.onBackground, - ); - final bool hasHeroIcon = heroIcon != null || this.heroIconWidget != null; - - return Scaffold( - floatingActionButton: hasFlashButton ? const BrandFab() : null, - body: CustomScrollView( - slivers: [ - HeroSliverAppBar( - heroTitle: heroTitle, - hasHeroIcon: hasHeroIcon, - hasBackButton: hasBackButton, - onBackButtonPressed: onBackButtonPressed, - heroIconWidget: heroIconWidget, - ), - if (heroSubtitle != null) - SliverPadding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 4.0, - ), - sliver: SliverList( - delegate: SliverChildListDelegate([ - Text( - heroSubtitle!, - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - color: Theme.of(context).colorScheme.onBackground, - ), - textAlign: hasHeroIcon ? TextAlign.center : TextAlign.start, - ), - ]), - ), - ), - SliverPadding( - padding: const EdgeInsets.all(16.0), - sliver: SliverList( - delegate: SliverChildListDelegate(children), - ), - ), - ], - ), - ); - } -} - -class HeroSliverAppBar extends StatefulWidget { - const HeroSliverAppBar({ - required this.heroTitle, - required this.hasHeroIcon, - required this.hasBackButton, - required this.onBackButtonPressed, - required this.heroIconWidget, - super.key, - }); - - final String heroTitle; - final bool hasHeroIcon; - final bool hasBackButton; - final VoidCallback? onBackButtonPressed; - final Widget heroIconWidget; - - @override - State createState() => _HeroSliverAppBarState(); -} - -class _HeroSliverAppBarState extends State { - Size _size = Size.zero; - @override - Widget build(final BuildContext context) => SliverAppBar( - expandedHeight: - widget.hasHeroIcon ? 148.0 + _size.height : 72.0 + _size.height, - primary: true, - pinned: true, - stretch: true, - leading: widget.hasBackButton - ? IconButton( - icon: const Icon(Icons.arrow_back), - onPressed: widget.onBackButtonPressed ?? - () => Navigator.of(context).pop(), - ) - : null, - flexibleSpace: FlexibleSpaceBar( - title: LayoutBuilder( - builder: (final context, final constraints) => SizedBox( - width: constraints.maxWidth - 72.0, - child: WidgetSize( - onChange: (final Size size) => setState(() => _size = size), - child: Text( - widget.heroTitle, - style: Theme.of(context).textTheme.titleLarge?.copyWith( - color: Theme.of(context).colorScheme.onSurface, - ), - overflow: TextOverflow.fade, - textAlign: TextAlign.center, - ), - ), - ), - ), - expandedTitleScale: 1.2, - centerTitle: true, - collapseMode: CollapseMode.pin, - titlePadding: const EdgeInsets.only( - bottom: 12.0, - top: 16.0, - ), - background: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - const SizedBox(height: 72.0), - if (widget.hasHeroIcon) widget.heroIconWidget, - ], - ), - ), - ); -} diff --git a/lib/ui/components/brand_linear_indicator/brand_linear_indicator.dart b/lib/ui/components/brand_linear_indicator/brand_linear_indicator.dart index 335387f5..34fb9e10 100644 --- a/lib/ui/components/brand_linear_indicator/brand_linear_indicator.dart +++ b/lib/ui/components/brand_linear_indicator/brand_linear_indicator.dart @@ -27,14 +27,14 @@ class BrandLinearIndicator extends StatelessWidget { alignment: Alignment.centerLeft, child: AnimatedSlide( duration: const Duration(milliseconds: 400), - curve: Curves.easeInOut, + curve: Curves.easeInOutCubicEmphasized, offset: Offset( -(1 - value), 0, ), child: AnimatedContainer( duration: const Duration(milliseconds: 400), - curve: Curves.easeInOut, + curve: Curves.easeInOutCubicEmphasized, width: constraints.maxWidth, decoration: BoxDecoration( color: color, diff --git a/lib/ui/components/brand_md/brand_md.dart b/lib/ui/components/brand_md/brand_md.dart index 457bb5c3..67c3e2ea 100644 --- a/lib/ui/components/brand_md/brand_md.dart +++ b/lib/ui/components/brand_md/brand_md.dart @@ -2,8 +2,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:flutter/services.dart' show rootBundle; import 'package:easy_localization/easy_localization.dart'; -import 'package:selfprivacy/config/brand_colors.dart'; -import 'package:selfprivacy/config/text_themes.dart'; import 'package:url_launcher/url_launcher_string.dart'; class BrandMarkdown extends StatefulWidget { @@ -37,24 +35,7 @@ class _BrandMarkdownState extends State { @override Widget build(final BuildContext context) { - final bool isDark = Theme.of(context).brightness == Brightness.dark; - final MarkdownStyleSheet markdown = MarkdownStyleSheet( - p: defaultTextStyle.copyWith( - color: isDark ? BrandColors.white : null, - ), - h1: headline1Style.copyWith( - color: isDark ? BrandColors.white : null, - ), - h2: headline2Style.copyWith( - color: isDark ? BrandColors.white : null, - ), - h3: headline3Style.copyWith( - color: isDark ? BrandColors.white : null, - ), - h4: headline4Style.copyWith( - color: isDark ? BrandColors.white : null, - ), - ); + final MarkdownStyleSheet markdown = MarkdownStyleSheet(); return MarkdownBody( shrinkWrap: true, styleSheet: markdown, diff --git a/lib/ui/components/brand_radio/brand_radio.dart b/lib/ui/components/brand_radio/brand_radio.dart deleted file mode 100644 index 494c3e81..00000000 --- a/lib/ui/components/brand_radio/brand_radio.dart +++ /dev/null @@ -1,40 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:selfprivacy/config/brand_colors.dart'; - -// TODO: Delete this file. - -class BrandRadio extends StatelessWidget { - const BrandRadio({ - required this.isChecked, - super.key, - }); - - final bool isChecked; - - @override - Widget build(final BuildContext context) => Container( - height: 20, - width: 20, - alignment: Alignment.center, - padding: const EdgeInsets.all(2), - decoration: BoxDecoration( - shape: BoxShape.circle, - border: _getBorder(), - ), - child: isChecked - ? Container( - height: 10, - width: 10, - decoration: const BoxDecoration( - shape: BoxShape.circle, - color: BrandColors.primary, - ), - ) - : null, - ); - - BoxBorder? _getBorder() => Border.all( - color: isChecked ? BrandColors.primary : BrandColors.gray1, - width: 2, - ); -} diff --git a/lib/ui/components/brand_switch/brand_switch.dart b/lib/ui/components/brand_switch/brand_switch.dart deleted file mode 100644 index 4ded47dd..00000000 --- a/lib/ui/components/brand_switch/brand_switch.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:flutter/material.dart'; - -class BrandSwitch extends StatelessWidget { - const BrandSwitch({ - required this.onChanged, - required this.value, - super.key, - }); - - final ValueChanged onChanged; - final bool value; - - @override - Widget build(final BuildContext context) => Switch( - activeColor: Theme.of(context).colorScheme.primary, - value: value, - onChanged: onChanged, - ); -} diff --git a/lib/ui/components/brand_tab_bar/brand_tab_bar.dart b/lib/ui/components/brand_tab_bar/brand_tab_bar.dart deleted file mode 100644 index 8362bedb..00000000 --- a/lib/ui/components/brand_tab_bar/brand_tab_bar.dart +++ /dev/null @@ -1,60 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart'; - -class BrandTabBar extends StatefulWidget { - const BrandTabBar({super.key, this.controller}); - - final TabController? controller; - @override - State createState() => _BrandTabBarState(); -} - -class _BrandTabBarState extends State { - int? currentIndex; - @override - void initState() { - currentIndex = widget.controller!.index; - widget.controller!.addListener(_listener); - super.initState(); - } - - void _listener() { - if (currentIndex != widget.controller!.index) { - setState(() { - currentIndex = widget.controller!.index; - }); - } - } - - @override - void dispose() { - widget.controller ?? widget.controller!.removeListener(_listener); - super.dispose(); - } - - @override - Widget build(final BuildContext context) => NavigationBar( - destinations: [ - _getIconButton('basis.providers'.tr(), BrandIcons.server, 0), - _getIconButton('basis.services'.tr(), BrandIcons.box, 1), - _getIconButton('basis.users'.tr(), BrandIcons.users, 2), - _getIconButton('basis.more'.tr(), Icons.menu_rounded, 3), - ], - onDestinationSelected: (final index) { - widget.controller!.animateTo(index); - }, - selectedIndex: currentIndex ?? 0, - labelBehavior: NavigationDestinationLabelBehavior.onlyShowSelected, - ); - - NavigationDestination _getIconButton( - final String label, - final IconData iconData, - final int index, - ) => - NavigationDestination( - icon: Icon(iconData), - label: label, - ); -} diff --git a/lib/ui/components/brand_text/brand_text.dart b/lib/ui/components/brand_text/brand_text.dart deleted file mode 100644 index 15307577..00000000 --- a/lib/ui/components/brand_text/brand_text.dart +++ /dev/null @@ -1,238 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:selfprivacy/config/text_themes.dart'; -export 'package:selfprivacy/utils/extensions/text_extensions.dart'; - -// TODO: Delete this file - -enum TextType { - h1, // right now only at onboarding and opened providers - h2, // cards titles - h3, // titles in about page - h4, // caption - h5, // Table data - body1, // normal - body2, // with opacity - medium, - small, - onboardingTitle, - buttonTitleText, // risen button title text, - h4Underlined, -} - -class BrandText extends StatelessWidget { - factory BrandText.h4( - final String? text, { - final TextStyle? style, - final TextAlign? textAlign, - }) => - BrandText( - text, - type: TextType.h4, - style: style, - softWrap: true, - overflow: TextOverflow.ellipsis, - maxLines: 2, - textAlign: textAlign, - ); - - factory BrandText.onboardingTitle( - final String text, { - final TextStyle? style, - }) => - BrandText( - text, - type: TextType.onboardingTitle, - style: style, - ); - factory BrandText.h3( - final String text, { - final TextStyle? style, - final TextAlign? textAlign, - }) => - BrandText( - text, - type: TextType.h3, - style: style, - textAlign: textAlign, - overflow: TextOverflow.ellipsis, - ); - - factory BrandText.h4Underlined( - final String? text, { - final TextStyle? style, - final TextAlign? textAlign, - }) => - BrandText( - text, - type: TextType.h4Underlined, - style: style, - softWrap: true, - overflow: TextOverflow.ellipsis, - maxLines: 2, - textAlign: textAlign, - ); - - factory BrandText.h1( - final String? text, { - final TextStyle? style, - final TextOverflow? overflow, - final bool? softWrap, - }) => - BrandText( - text, - type: TextType.h1, - style: style, - ); - factory BrandText.h2( - final String? text, { - final TextStyle? style, - final TextAlign? textAlign, - }) => - BrandText( - text, - type: TextType.h2, - style: style, - textAlign: textAlign, - ); - factory BrandText.body1(final String? text, {final TextStyle? style}) => - BrandText( - text, - type: TextType.body1, - style: style, - ); - factory BrandText.small(final String text, {final TextStyle? style}) => - BrandText( - text, - type: TextType.small, - style: style, - ); - factory BrandText.body2(final String? text, {final TextStyle? style}) => - BrandText( - text, - type: TextType.body2, - style: style, - ); - factory BrandText.buttonTitleText( - final String? text, { - final TextStyle? style, - }) => - BrandText( - text, - type: TextType.buttonTitleText, - style: style, - ); - - factory BrandText.h5( - final String? text, { - final TextStyle? style, - final TextAlign? textAlign, - }) => - BrandText( - text, - type: TextType.h5, - style: style, - textAlign: textAlign, - ); - factory BrandText.medium( - final String? text, { - final TextStyle? style, - final TextAlign? textAlign, - }) => - BrandText( - text, - type: TextType.medium, - style: style, - textAlign: textAlign, - ); - const BrandText( - this.text, { - required this.type, - super.key, - this.style, - this.overflow, - this.softWrap, - this.textAlign, - this.maxLines, - }); - - final String? text; - final TextStyle? style; - final TextType type; - final TextOverflow? overflow; - final bool? softWrap; - final TextAlign? textAlign; - final int? maxLines; - @override - Text build(final BuildContext context) { - TextStyle style; - final bool isDark = Theme.of(context).brightness == Brightness.dark; - switch (type) { - case TextType.h1: - style = isDark - ? headline1Style.copyWith(color: Colors.white) - : headline1Style; - break; - case TextType.h2: - style = isDark - ? headline2Style.copyWith(color: Colors.white) - : headline2Style; - break; - case TextType.h3: - style = isDark - ? headline3Style.copyWith(color: Colors.white) - : headline3Style; - break; - case TextType.h4: - style = isDark - ? headline4Style.copyWith(color: Colors.white) - : headline4Style; - break; - case TextType.h4Underlined: - style = isDark - ? headline4UnderlinedStyle.copyWith(color: Colors.white) - : headline4UnderlinedStyle; - break; - case TextType.h5: - style = isDark - ? headline5Style.copyWith(color: Colors.white) - : headline5Style; - break; - case TextType.body1: - style = isDark ? body1Style.copyWith(color: Colors.white) : body1Style; - break; - case TextType.body2: - style = isDark - ? body2Style.copyWith(color: Colors.white.withOpacity(0.6)) - : body2Style; - break; - case TextType.small: - style = isDark ? smallStyle.copyWith(color: Colors.white) : smallStyle; - break; - case TextType.onboardingTitle: - style = isDark - ? onboardingTitle.copyWith(color: Colors.white) - : onboardingTitle; - break; - case TextType.medium: - style = - isDark ? mediumStyle.copyWith(color: Colors.white) : mediumStyle; - break; - case TextType.buttonTitleText: - style = !isDark - ? buttonTitleText.copyWith(color: Colors.white) - : buttonTitleText; - break; - } - if (this.style != null) { - style = style.merge(this.style); - } - return Text( - text!, - style: style, - maxLines: maxLines, - overflow: overflow, - softWrap: softWrap, - textAlign: textAlign, - ); - } -} diff --git a/lib/ui/components/brand_timer/brand_timer.dart b/lib/ui/components/brand_timer/brand_timer.dart index 3ccf9a63..4d744cfc 100644 --- a/lib/ui/components/brand_timer/brand_timer.dart +++ b/lib/ui/components/brand_timer/brand_timer.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:selfprivacy/ui/components/brand_text/brand_text.dart'; import 'package:selfprivacy/utils/named_font_weight.dart'; class BrandTimer extends StatefulWidget { @@ -52,11 +51,12 @@ class _BrandTimerState extends State { } @override - Widget build(final BuildContext context) => BrandText.medium( - _timeString, - style: const TextStyle( - fontWeight: NamedFontWeight.demiBold, - ), + Widget build(final BuildContext context) => Text( + _timeString ?? '', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + fontWeight: NamedFontWeight.demiBold, + color: Theme.of(context).colorScheme.onSurface, + ), ); void _getTime() { diff --git a/lib/ui/components/brand_button/brand_button.dart b/lib/ui/components/buttons/brand_button.dart similarity index 54% rename from lib/ui/components/brand_button/brand_button.dart rename to lib/ui/components/buttons/brand_button.dart index 28edd6b8..12c7c132 100644 --- a/lib/ui/components/brand_button/brand_button.dart +++ b/lib/ui/components/buttons/brand_button.dart @@ -1,7 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:selfprivacy/ui/components/brand_text/brand_text.dart'; - -enum BrandButtonTypes { rised, text, iconText } class BrandButton { static ConstrainedBox rised({ @@ -58,53 +55,4 @@ class BrandButton { ), child: TextButton(onPressed: onPressed, child: Text(title)), ); - - static IconTextButton emptyWithIconText({ - required final VoidCallback onPressed, - required final String title, - required final Icon icon, - final Key? key, - }) => - IconTextButton( - key: key, - title: title, - onPressed: onPressed, - icon: icon, - ); -} - -class IconTextButton extends StatelessWidget { - const IconTextButton({ - super.key, - this.onPressed, - this.title, - this.icon, - }); - - final VoidCallback? onPressed; - final String? title; - final Icon? icon; - - @override - Widget build(final BuildContext context) => Material( - color: Colors.transparent, - child: InkWell( - onTap: onPressed, - child: Container( - height: 48, - width: double.infinity, - alignment: Alignment.center, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - BrandText.body1(title), - Padding( - padding: const EdgeInsets.all(12.0), - child: icon, - ) - ], - ), - ), - ), - ); } diff --git a/lib/ui/components/action_button/action_button.dart b/lib/ui/components/buttons/dialog_action_button.dart similarity index 81% rename from lib/ui/components/action_button/action_button.dart rename to lib/ui/components/buttons/dialog_action_button.dart index 4073393f..260cd493 100644 --- a/lib/ui/components/action_button/action_button.dart +++ b/lib/ui/components/buttons/dialog_action_button.dart @@ -1,7 +1,8 @@ import 'package:flutter/material.dart'; -class ActionButton extends StatelessWidget { - const ActionButton({ +/// Basically a [TextButton] to be used in dialogs +class DialogActionButton extends StatelessWidget { + const DialogActionButton({ super.key, this.text, this.onPressed, diff --git a/lib/ui/components/brand_button/outlined_button.dart b/lib/ui/components/buttons/outlined_button.dart similarity index 100% rename from lib/ui/components/brand_button/outlined_button.dart rename to lib/ui/components/buttons/outlined_button.dart diff --git a/lib/ui/components/brand_button/segmented_buttons.dart b/lib/ui/components/buttons/segmented_buttons.dart similarity index 67% rename from lib/ui/components/brand_button/segmented_buttons.dart rename to lib/ui/components/buttons/segmented_buttons.dart index 7632dc20..b876f71d 100644 --- a/lib/ui/components/brand_button/segmented_buttons.dart +++ b/lib/ui/components/buttons/segmented_buttons.dart @@ -1,6 +1,16 @@ import 'package:flutter/material.dart'; +/// For some reason original [SegmentedButton] does not have animations. +/// +/// The [SegmentedButtons] was written for SelfPrivacy before [SegmentedButton] was introduced. +/// While it doesn't have that much options to pass, it has cute little animation. +/// It is based on [ToggleButtons]. class SegmentedButtons extends StatelessWidget { + /// Creates a segmented buttons widget. This is a SelfPrivacy implementation. + /// + /// Provide the button titles in [titles] as a [List]. + /// Current selection is provided in [isSelected] as a [List]. + /// This widget will call [onPressed] with the index of the button that was pressed. const SegmentedButtons({ required this.isSelected, required this.onPressed, @@ -8,15 +18,24 @@ class SegmentedButtons extends StatelessWidget { super.key, }); + /// The current selection state of the buttons. + /// + /// The length of this list must be equal to the length of [titles]. + /// Several buttons can be selected at the same time. final List isSelected; + + /// The callback that is called when a button is pressed. + /// It will be called with the index of the button that was pressed. final Function(int)? onPressed; + + /// The titles of the buttons. final List titles; @override Widget build(final BuildContext context) => LayoutBuilder( builder: (final context, final constraints) => ToggleButtons( constraints: BoxConstraints( - minWidth: (constraints.maxWidth - 8) / 3, + minWidth: (constraints.maxWidth - 8) / titles.length, minHeight: 40 + Theme.of(context).visualDensity.vertical * 4, ), borderRadius: BorderRadius.circular(48), @@ -38,7 +57,7 @@ class SegmentedButtons extends StatelessWidget { opacity: isSelected[index] ? 1 : 0, child: AnimatedScale( duration: const Duration(milliseconds: 200), - curve: Curves.easeInOut, + curve: Curves.easeInOutCubicEmphasized, alignment: Alignment.centerLeft, scale: isSelected[index] ? 1 : 0, child: Icon( @@ -53,7 +72,7 @@ class SegmentedButtons extends StatelessWidget { ? const EdgeInsets.only(left: 24) : EdgeInsets.zero, duration: const Duration(milliseconds: 200), - curve: Curves.easeInOut, + curve: Curves.easeInOutCubicEmphasized, child: Text( title, style: Theme.of(context).textTheme.labelLarge, diff --git a/lib/ui/components/brand_cards/filled_card.dart b/lib/ui/components/cards/filled_card.dart similarity index 100% rename from lib/ui/components/brand_cards/filled_card.dart rename to lib/ui/components/cards/filled_card.dart diff --git a/lib/ui/components/brand_cards/outlined_card.dart b/lib/ui/components/cards/outlined_card.dart similarity index 100% rename from lib/ui/components/brand_cards/outlined_card.dart rename to lib/ui/components/cards/outlined_card.dart diff --git a/lib/ui/components/drawers/progress_drawer.dart b/lib/ui/components/drawers/progress_drawer.dart new file mode 100644 index 00000000..d886da02 --- /dev/null +++ b/lib/ui/components/drawers/progress_drawer.dart @@ -0,0 +1,113 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; + +class ProgressDrawer extends StatelessWidget { + /// A [Drawer] that displays a list of steps and the current step. + /// Used in setup wizards. The [trailing] widget is displayed at the bottom. + /// The [steps] are translated using [EasyLocalization]. + const ProgressDrawer({ + required this.steps, + required this.currentStep, + required this.constraints, + required this.trailing, + required this.title, + super.key, + }); + + final List steps; + final int currentStep; + final Widget trailing; + final BoxConstraints constraints; + final String title; + + @override + Widget build(final BuildContext context) => SizedBox( + width: 300, + height: constraints.maxHeight, + child: Drawer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: Text( + title, + style: Theme.of(context).textTheme.titleLarge, + ), + ), + Flexible( + fit: FlexFit.tight, + child: SingleChildScrollView( + child: Column( + children: [ + ...steps.map((final step) { + final index = steps.indexOf(step); + return _StepIndicator( + title: step.tr(), + isCurrent: index == currentStep, + isCompleted: index < currentStep, + ); + }), + ], + ), + ), + ), + // const Spacer(), + Padding( + padding: const EdgeInsets.all(16.0), + child: trailing, + ), + ], + ), + ), + ); +} + +class _StepIndicator extends StatelessWidget { + const _StepIndicator({ + required this.title, + required this.isCompleted, + required this.isCurrent, + }); + + final String title; + final bool isCompleted; + final bool isCurrent; + + @override + Widget build(final BuildContext context) => ListTile( + selected: isCurrent, + leading: isCurrent + ? const _StepCurrentIcon() + : isCompleted + ? const _StepCompletedIcon() + : const _StepPendingIcon(), + title: Text( + title, + ), + textColor: Theme.of(context).colorScheme.onSurfaceVariant, + iconColor: Theme.of(context).colorScheme.onSurfaceVariant, + ); +} + +class _StepCompletedIcon extends StatelessWidget { + const _StepCompletedIcon(); + + @override + Widget build(final BuildContext context) => const Icon(Icons.check_circle); +} + +class _StepPendingIcon extends StatelessWidget { + const _StepPendingIcon(); + + @override + Widget build(final BuildContext context) => const Icon(Icons.circle_outlined); +} + +class _StepCurrentIcon extends StatelessWidget { + const _StepCurrentIcon(); + + @override + Widget build(final BuildContext context) => + const Icon(Icons.build_circle_outlined); +} diff --git a/lib/ui/components/drawers/support_drawer.dart b/lib/ui/components/drawers/support_drawer.dart new file mode 100644 index 00000000..7b4c5c2b --- /dev/null +++ b/lib/ui/components/drawers/support_drawer.dart @@ -0,0 +1,52 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:selfprivacy/logic/cubit/support_system/support_system_cubit.dart'; +import 'package:selfprivacy/ui/components/brand_md/brand_md.dart'; + +class SupportDrawer extends StatelessWidget { + const SupportDrawer({ + super.key, + }); + + @override + Widget build(final BuildContext context) { + final currentArticle = + context.watch().state.currentArticle; + return Drawer( + width: 440, + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + Row( + children: [ + const SizedBox(width: 8), + const Icon(Icons.help_outline), + const SizedBox(width: 16), + Text( + 'support.title'.tr(), + style: Theme.of(context).textTheme.titleLarge, + ), + const Spacer(), + IconButton( + onPressed: () => Scaffold.of(context).closeEndDrawer(), + icon: const Icon(Icons.chevron_right_outlined), + ), + ], + ), + const SizedBox(height: 8), + Padding( + padding: const EdgeInsets.all(8.0), + child: BrandMarkdown( + fileName: currentArticle, + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/ui/components/error/error.dart b/lib/ui/components/error/error.dart deleted file mode 100644 index 402ce512..00000000 --- a/lib/ui/components/error/error.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'package:flutter/material.dart'; - -class BrandError extends StatelessWidget { - const BrandError({super.key, this.error, this.stackTrace}); - - final Object? error; - final StackTrace? stackTrace; - - @override - Widget build(final BuildContext context) => SafeArea( - child: Scaffold( - body: Center( - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text(error.toString()), - const Text('stackTrace: '), - Text(stackTrace.toString()), - ], - ), - ), - ), - ), - ); -} diff --git a/lib/ui/components/jobs_content/jobs_content.dart b/lib/ui/components/jobs_content/jobs_content.dart index bafe94a7..5ba00f89 100644 --- a/lib/ui/components/jobs_content/jobs_content.dart +++ b/lib/ui/components/jobs_content/jobs_content.dart @@ -6,10 +6,8 @@ import 'package:selfprivacy/logic/cubit/client_jobs/client_jobs_cubit.dart'; import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; import 'package:selfprivacy/logic/cubit/server_jobs/server_jobs_cubit.dart'; import 'package:selfprivacy/logic/models/json/server_job.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/buttons/brand_button.dart'; import 'package:selfprivacy/ui/components/brand_loader/brand_loader.dart'; -import 'package:selfprivacy/ui/components/brand_text/brand_text.dart'; import 'package:selfprivacy/ui/components/jobs_content/server_job_card.dart'; import 'package:selfprivacy/ui/helpers/modals.dart'; @@ -32,7 +30,12 @@ class JobsContent extends StatelessWidget { if (state is JobsStateEmpty) { widgets = [ const SizedBox(height: 80), - Center(child: BrandText.body1('jobs.empty'.tr())), + Center( + child: Text( + 'jobs.empty'.tr(), + style: Theme.of(context).textTheme.bodyLarge, + ), + ), ]; if (installationState is ServerInstallationFinished) { @@ -65,38 +68,49 @@ class JobsContent extends StatelessWidget { ]; } else if (state is JobsStateWithJobs) { widgets = [ - ...state.clientJobList - .map( - (final j) => Row( - children: [ - Expanded( - child: BrandCards.small( - child: Text(j.title), + ...state.clientJobList.map( + (final j) => Row( + children: [ + Expanded( + child: Card( + color: Theme.of(context).colorScheme.surfaceVariant, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 15, + vertical: 10, ), - ), - const SizedBox(width: 10), - ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: - Theme.of(context).colorScheme.errorContainer, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - ), - onPressed: () => - context.read().removeJob(j.id), child: Text( - 'basis.remove'.tr(), - style: TextStyle( - color: - Theme.of(context).colorScheme.onErrorContainer, - ), + j.title, + style: + Theme.of(context).textTheme.labelLarge?.copyWith( + color: Theme.of(context) + .colorScheme + .onSurfaceVariant, + ), ), ), - ], + ), ), - ) - .toList(), + const SizedBox(width: 10), + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: + Theme.of(context).colorScheme.errorContainer, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + onPressed: () => context.read().removeJob(j.id), + child: Text( + 'basis.remove'.tr(), + style: TextStyle( + color: Theme.of(context).colorScheme.onErrorContainer, + ), + ), + ), + ], + ), + ), const SizedBox(height: 20), BrandButton.rised( onPressed: () => context.read().applyAll(), @@ -109,8 +123,9 @@ class JobsContent extends StatelessWidget { children: [ const SizedBox(height: 15), Center( - child: BrandText.h2( + child: Text( 'jobs.title'.tr(), + style: Theme.of(context).textTheme.headlineSmall, ), ), const SizedBox(height: 20), diff --git a/lib/ui/components/list_tiles/log_list_tile.dart b/lib/ui/components/list_tiles/log_list_tile.dart new file mode 100644 index 00000000..88505d8f --- /dev/null +++ b/lib/ui/components/list_tiles/log_list_tile.dart @@ -0,0 +1,300 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:selfprivacy/logic/models/message.dart'; + +class LogListItem extends StatelessWidget { + const LogListItem({ + required this.message, + super.key, + }); + + final Message message; + + @override + Widget build(final BuildContext context) { + final messageItem = message; + if (messageItem is RestApiRequestMessage) { + return _RestApiRequestMessageItem(message: messageItem); + } else if (messageItem is RestApiResponseMessage) { + return _RestApiResponseMessageItem(message: messageItem); + } else if (messageItem is GraphQlResponseMessage) { + return _GraphQlResponseMessageItem(message: messageItem); + } else if (messageItem is GraphQlRequestMessage) { + return _GraphQlRequestMessageItem(message: messageItem); + } else { + return _DefaultMessageItem(message: messageItem); + } + } +} + +class _RestApiRequestMessageItem extends StatelessWidget { + const _RestApiRequestMessageItem({required this.message}); + + final RestApiRequestMessage message; + + @override + Widget build(final BuildContext context) => ListTile( + title: Text( + '${message.method}\n${message.uri}', + ), + subtitle: Text(message.timeString), + leading: const Icon(Icons.upload_outlined), + iconColor: Theme.of(context).colorScheme.secondary, + onTap: () => showDialog( + context: context, + builder: (final BuildContext context) => AlertDialog( + scrollable: true, + title: Text( + '${message.method}\n${message.uri}', + ), + content: Column( + children: [ + Text(message.timeString), + const SizedBox(height: 16), + // Headers is a map of key-value pairs + if (message.headers != null) const Text('Headers'), + if (message.headers != null) + Text( + message.headers!.entries + .map((final entry) => '${entry.key}: ${entry.value}') + .join('\n'), + ), + if (message.data != null && message.data != 'null') + const Text('Data'), + if (message.data != null && message.data != 'null') + Text(message.data!), + ], + ), + actions: [ + // A button to copy the request to the clipboard + TextButton( + onPressed: () { + Clipboard.setData(ClipboardData(text: message.text)); + }, + child: Text('console_page.copy'.tr()), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text('basis.close'.tr()), + ), + ], + ), + ), + ); +} + +class _RestApiResponseMessageItem extends StatelessWidget { + const _RestApiResponseMessageItem({required this.message}); + + final RestApiResponseMessage message; + + @override + Widget build(final BuildContext context) => ListTile( + title: Text( + '${message.statusCode} ${message.method}\n${message.uri}', + ), + subtitle: Text(message.timeString), + leading: const Icon(Icons.download_outlined), + iconColor: Theme.of(context).colorScheme.primary, + onTap: () => showDialog( + context: context, + builder: (final BuildContext context) => AlertDialog( + scrollable: true, + title: Text( + '${message.statusCode} ${message.method}\n${message.uri}', + ), + content: Column( + children: [ + Text(message.timeString), + const SizedBox(height: 16), + // Headers is a map of key-value pairs + if (message.data != null && message.data != 'null') + const Text('Data'), + if (message.data != null && message.data != 'null') + Text(message.data!), + ], + ), + actions: [ + // A button to copy the request to the clipboard + TextButton( + onPressed: () { + Clipboard.setData(ClipboardData(text: message.text)); + }, + child: Text('console_page.copy'.tr()), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text('basis.close'.tr()), + ), + ], + ), + ), + ); +} + +class _GraphQlResponseMessageItem extends StatelessWidget { + const _GraphQlResponseMessageItem({required this.message}); + + final GraphQlResponseMessage message; + + @override + Widget build(final BuildContext context) => ListTile( + title: Text( + 'GraphQL Response at ${message.timeString}', + ), + subtitle: Text( + message.data.toString(), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + leading: const Icon(Icons.arrow_circle_down_outlined), + iconColor: Theme.of(context).colorScheme.tertiary, + onTap: () => showDialog( + context: context, + builder: (final BuildContext context) => AlertDialog( + scrollable: true, + title: Text( + 'GraphQL Response at ${message.timeString}', + ), + content: Column( + children: [ + Text(message.timeString), + const Divider(), + if (message.data != null) const Text('Data'), + // Data is a map of key-value pairs + if (message.data != null) + Text( + message.data!.entries + .map((final entry) => '${entry.key}: ${entry.value}') + .join('\n'), + ), + const Divider(), + if (message.errors != null) const Text('Errors'), + if (message.errors != null) + Text( + message.errors! + .map( + (final entry) => + '${entry.message} at ${entry.locations}', + ) + .join('\n'), + ), + const Divider(), + if (message.context != null) const Text('Context'), + if (message.context != null) + Text( + message.context!.toString(), + ), + ], + ), + actions: [ + // A button to copy the request to the clipboard + TextButton( + onPressed: () { + Clipboard.setData(ClipboardData(text: message.text)); + }, + child: Text('console_page.copy'.tr()), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text('basis.close'.tr()), + ), + ], + ), + ), + ); +} + +class _GraphQlRequestMessageItem extends StatelessWidget { + const _GraphQlRequestMessageItem({required this.message}); + + final GraphQlRequestMessage message; + + @override + Widget build(final BuildContext context) => ListTile( + title: Text( + 'GraphQL Request at ${message.timeString}', + ), + subtitle: Text( + message.operation.toString(), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + leading: const Icon(Icons.arrow_circle_up_outlined), + iconColor: Theme.of(context).colorScheme.secondary, + onTap: () => showDialog( + context: context, + builder: (final BuildContext context) => AlertDialog( + scrollable: true, + title: Text( + 'GraphQL Response at ${message.timeString}', + ), + content: Column( + children: [ + Text(message.timeString), + const Divider(), + if (message.operation != null) const Text('Operation'), + // Data is a map of key-value pairs + if (message.operation != null) + Text( + message.operation!.toString(), + ), + const Divider(), + if (message.variables != null) const Text('Variables'), + if (message.variables != null) + Text( + message.variables!.entries + .map((final entry) => '${entry.key}: ${entry.value}') + .join('\n'), + ), + const Divider(), + if (message.context != null) const Text('Context'), + if (message.context != null) + Text( + message.context!.toString(), + ), + ], + ), + actions: [ + // A button to copy the request to the clipboard + TextButton( + onPressed: () { + Clipboard.setData(ClipboardData(text: message.text)); + }, + child: Text('console_page.copy'.tr()), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text('basis.close'.tr()), + ), + ], + ), + ), + ); +} + +class _DefaultMessageItem extends StatelessWidget { + const _DefaultMessageItem({required this.message}); + + final Message message; + + @override + Widget build(final BuildContext context) => Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: RichText( + text: TextSpan( + style: DefaultTextStyle.of(context).style, + children: [ + TextSpan( + text: '${message.timeString}: \n', + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), + TextSpan(text: message.text), + ], + ), + ), + ); +} diff --git a/lib/ui/components/not_ready_card/not_ready_card.dart b/lib/ui/components/not_ready_card/not_ready_card.dart index 379abf27..4b174a41 100644 --- a/lib/ui/components/not_ready_card/not_ready_card.dart +++ b/lib/ui/components/not_ready_card/not_ready_card.dart @@ -1,7 +1,7 @@ +import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; -import 'package:selfprivacy/ui/components/brand_cards/filled_card.dart'; -import 'package:selfprivacy/ui/pages/setup/initializing/initializing.dart'; -import 'package:selfprivacy/utils/route_transitions/basic.dart'; +import 'package:selfprivacy/ui/components/cards/filled_card.dart'; +import 'package:selfprivacy/ui/router/router.dart'; import 'package:easy_localization/easy_localization.dart'; class NotReadyCard extends StatelessWidget { @@ -13,11 +13,7 @@ class NotReadyCard extends StatelessWidget { child: ListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - onTap: () => Navigator.of(context).push( - materialRoute( - const InitializingPage(), - ), - ), + onTap: () => context.pushRoute(const InitializingRoute()), title: Text( 'not_ready_card.in_menu'.tr(), style: Theme.of(context).textTheme.titleSmall?.copyWith( diff --git a/lib/ui/components/pre_styled_buttons/flash_fab.dart b/lib/ui/components/pre_styled_buttons/flash_fab.dart index f0087b9f..2098f79e 100644 --- a/lib/ui/components/pre_styled_buttons/flash_fab.dart +++ b/lib/ui/components/pre_styled_buttons/flash_fab.dart @@ -1,13 +1,17 @@ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:ionicons/ionicons.dart'; import 'package:selfprivacy/logic/cubit/client_jobs/client_jobs_cubit.dart'; -import 'package:selfprivacy/ui/components/brand_bottom_sheet/brand_bottom_sheet.dart'; import 'package:selfprivacy/ui/components/jobs_content/jobs_content.dart'; -import 'package:selfprivacy/ui/helpers/modals.dart'; class BrandFab extends StatefulWidget { - const BrandFab({super.key}); + const BrandFab({ + this.extended = false, + super.key, + }); + + final bool extended; @override State createState() => _BrandFabState(); @@ -56,28 +60,40 @@ class _BrandFabState extends State child: FloatingActionButton( onPressed: () { // TODO: Make a hero animation to the screen - showBrandBottomSheet( + showModalBottomSheet( context: context, - builder: (final BuildContext context) => const BrandBottomSheet( - isExpended: true, - child: JobsContent(), - ), + builder: (final BuildContext context) => const JobsContent(), ); }, - child: AnimatedBuilder( - animation: _colorTween, - builder: (final BuildContext context, final Widget? child) { - final double v = _animationController.value; - final IconData icon = - v > 0.5 ? Ionicons.flash : Ionicons.flash_outline; - return Transform.scale( - scale: 1 + (v < 0.5 ? v : 1 - v) * 2, - child: Icon( - icon, - color: _colorTween.value, + isExtended: widget.extended, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + AnimatedBuilder( + animation: _colorTween, + builder: (final BuildContext context, final Widget? child) { + final double v = _animationController.value; + final IconData icon = + v > 0.5 ? Ionicons.flash : Ionicons.flash_outline; + return Transform.scale( + scale: 1 + (v < 0.5 ? v : 1 - v) * 2, + child: Icon( + icon, + color: _colorTween.value, + ), + ); + }, + ), + if (widget.extended) + const SizedBox( + width: 8, ), - ); - }, + if (widget.extended) + Text( + 'jobs.title'.tr(), + ), + ], ), ), ); diff --git a/lib/ui/components/progress_bar/progress_bar.dart b/lib/ui/components/progress_bar/progress_bar.dart index 00f1388e..7f743f44 100644 --- a/lib/ui/components/progress_bar/progress_bar.dart +++ b/lib/ui/components/progress_bar/progress_bar.dart @@ -1,6 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:selfprivacy/config/brand_colors.dart'; -import 'package:selfprivacy/config/text_themes.dart'; import 'package:selfprivacy/logic/cubit/app_settings/app_settings_cubit.dart'; class ProgressBar extends StatefulWidget { @@ -65,7 +63,7 @@ class _ProgressBarState extends State { Container( alignment: Alignment.centerLeft, decoration: BoxDecoration( - color: BrandColors.gray4, + color: const Color(0xFFDDDDDD), borderRadius: BorderRadius.circular(5), ), child: LayoutBuilder( @@ -119,3 +117,13 @@ class _ProgressBarState extends State { ); } } + +const TextStyle progressTextStyleLight = TextStyle( + fontSize: 11, + color: Colors.black, + height: 1.7, +); + +final TextStyle progressTextStyleDark = progressTextStyleLight.copyWith( + color: Colors.white, +); diff --git a/lib/ui/components/storage_list_items/service_migration_list_item.dart b/lib/ui/components/storage_list_items/service_migration_list_item.dart index 8eee284c..5229658d 100644 --- a/lib/ui/components/storage_list_items/service_migration_list_item.dart +++ b/lib/ui/components/storage_list_items/service_migration_list_item.dart @@ -72,7 +72,10 @@ class ServiceConsumptionTitle extends StatelessWidget { service.svgIcon, width: 24.0, height: 24.0, - color: Theme.of(context).colorScheme.onBackground, + colorFilter: ColorFilter.mode( + Theme.of(context).colorScheme.onBackground, + BlendMode.srcIn, + ), ), ), const SizedBox(width: 16), diff --git a/lib/ui/components/switch_block/switch_bloc.dart b/lib/ui/components/switch_block/switch_bloc.dart deleted file mode 100644 index 85ac2c68..00000000 --- a/lib/ui/components/switch_block/switch_bloc.dart +++ /dev/null @@ -1,36 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:selfprivacy/config/brand_colors.dart'; - -// TODO: Delete this file. - -class SwitcherBlock extends StatelessWidget { - const SwitcherBlock({ - required this.child, - required this.isActive, - required this.onChange, - super.key, - }); - - final Widget child; - final bool isActive; - final ValueChanged onChange; - - @override - Widget build(final BuildContext context) => Container( - padding: const EdgeInsets.symmetric(vertical: 16), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible(child: child), - const SizedBox(width: 5), - Switch( - activeColor: BrandColors.green1, - activeTrackColor: BrandColors.green2, - onChanged: onChange, - value: isActive, - ), - ], - ), - ); -} diff --git a/lib/ui/helpers/modals.dart b/lib/ui/helpers/modals.dart index 1750c2aa..b744e323 100644 --- a/lib/ui/helpers/modals.dart +++ b/lib/ui/helpers/modals.dart @@ -1,21 +1,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; import 'package:selfprivacy/config/get_it_config.dart'; -import 'package:selfprivacy/ui/components/action_button/action_button.dart'; -import 'package:selfprivacy/ui/components/brand_alert/brand_alert.dart'; - -Future showBrandBottomSheet({ - required final BuildContext context, - required final WidgetBuilder builder, -}) => - showCupertinoModalBottomSheet( - builder: builder, - barrierColor: Colors.black45, - context: context, - shadow: const BoxShadow(color: Colors.transparent), - backgroundColor: Colors.transparent, - ); +import 'package:selfprivacy/ui/components/buttons/dialog_action_button.dart'; void showPopUpAlert({ required final String description, @@ -26,16 +12,16 @@ void showPopUpAlert({ final String? cancelButtonTitle, }) { getIt.get().showPopUpDialog( - BrandAlert( - title: alertTitle ?? 'basis.alert'.tr(), - contentText: description, + AlertDialog( + title: Text(alertTitle ?? 'basis.alert'.tr()), + content: Text(description), actions: [ - ActionButton( + DialogActionButton( text: actionButtonTitle, isRed: true, onPressed: actionButtonOnPressed, ), - ActionButton( + DialogActionButton( text: cancelButtonTitle ?? 'basis.cancel'.tr(), onPressed: cancelButtonOnPressed, ), diff --git a/lib/ui/helpers/widget_size.dart b/lib/ui/helpers/widget_size.dart index 2b9eb962..bbd1529f 100644 --- a/lib/ui/helpers/widget_size.dart +++ b/lib/ui/helpers/widget_size.dart @@ -1,14 +1,26 @@ import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; +/// A helper widget that calls a callback when its size changes. +/// +/// This is useful when you want to know the size of a widget, and use it in +/// another leaf of the tree. +/// +/// The [onChange] callback is called after the widget is rendered, and the +/// size of the widget is different from the previous render. class WidgetSize extends StatefulWidget { + /// Creates a helper widget that calls a callback when its size changes. const WidgetSize({ required this.onChange, required this.child, super.key, }); + + /// The child widget, the size of which is to be measured. final Widget child; - final Function onChange; + + /// The callback to be called when the size of the widget changes. + final Function(Size) onChange; @override State createState() => _WidgetSizeState(); @@ -34,6 +46,11 @@ class _WidgetSizeState extends State { } final newSize = context.size; + + if (newSize == null) { + return; + } + if (oldSize == newSize) { return; } diff --git a/lib/ui/layouts/brand_hero_screen.dart b/lib/ui/layouts/brand_hero_screen.dart new file mode 100644 index 00000000..adb94902 --- /dev/null +++ b/lib/ui/layouts/brand_hero_screen.dart @@ -0,0 +1,195 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:ionicons/ionicons.dart'; +import 'package:selfprivacy/logic/cubit/client_jobs/client_jobs_cubit.dart'; +import 'package:selfprivacy/ui/components/jobs_content/jobs_content.dart'; +import 'package:selfprivacy/ui/components/drawers/support_drawer.dart'; +import 'package:selfprivacy/ui/helpers/widget_size.dart'; +import 'package:selfprivacy/utils/breakpoints.dart'; + +class BrandHeroScreen extends StatelessWidget { + const BrandHeroScreen({ + required this.children, + super.key, + this.hasBackButton = true, + this.hasFlashButton = false, + this.heroIcon, + this.heroIconWidget, + this.heroTitle = '', + this.heroSubtitle, + this.onBackButtonPressed, + this.bodyPadding = const EdgeInsets.all(16.0), + this.ignoreBreakpoints = false, + this.hasSupportDrawer = false, + }); + + final List children; + final bool hasBackButton; + final bool hasFlashButton; + final IconData? heroIcon; + final Widget? heroIconWidget; + final String heroTitle; + final String? heroSubtitle; + final VoidCallback? onBackButtonPressed; + final EdgeInsetsGeometry bodyPadding; + + /// On non-mobile screens the buttons of the app bar are hidden. + /// This is because this widget implies that it is nested inside a bigger layout. + /// If it is not nested, set this to true. + final bool ignoreBreakpoints; + + /// Usually support drawer is provided by the parent layout. + /// If it is not provided, set this to true. + final bool hasSupportDrawer; + + @override + Widget build(final BuildContext context) { + final Widget heroIconWidget = this.heroIconWidget ?? + Icon( + heroIcon ?? Icons.help, + size: 48.0, + color: Theme.of(context).colorScheme.onBackground, + ); + final bool hasHeroIcon = heroIcon != null || this.heroIconWidget != null; + + return Scaffold( + endDrawerEnableOpenDragGesture: false, + endDrawer: hasSupportDrawer ? const SupportDrawer() : null, + body: CustomScrollView( + slivers: [ + HeroSliverAppBar( + heroTitle: heroTitle, + hasHeroIcon: hasHeroIcon, + hasBackButton: hasBackButton, + onBackButtonPressed: onBackButtonPressed, + heroIconWidget: heroIconWidget, + hasFlashButton: hasFlashButton, + ignoreBreakpoints: ignoreBreakpoints, + ), + if (heroSubtitle != null) + SliverPadding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 4.0, + ), + sliver: SliverList( + delegate: SliverChildListDelegate([ + Text( + heroSubtitle!, + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: Theme.of(context).colorScheme.onBackground, + ), + textAlign: hasHeroIcon ? TextAlign.center : TextAlign.start, + ), + ]), + ), + ), + SliverPadding( + padding: bodyPadding, + sliver: SliverList( + delegate: SliverChildListDelegate(children), + ), + ), + ], + ), + ); + } +} + +class HeroSliverAppBar extends StatefulWidget { + const HeroSliverAppBar({ + required this.heroTitle, + required this.hasHeroIcon, + required this.hasBackButton, + required this.onBackButtonPressed, + required this.heroIconWidget, + required this.hasFlashButton, + required this.ignoreBreakpoints, + super.key, + }); + + final String heroTitle; + final bool hasHeroIcon; + final bool hasBackButton; + final bool hasFlashButton; + final VoidCallback? onBackButtonPressed; + final Widget heroIconWidget; + final bool ignoreBreakpoints; + + @override + State createState() => _HeroSliverAppBarState(); +} + +class _HeroSliverAppBarState extends State { + Size _size = Size.zero; + @override + Widget build(final BuildContext context) { + final isMobile = + widget.ignoreBreakpoints ? true : Breakpoints.small.isActive(context); + final isJobsListEmpty = context.watch().state is JobsStateEmpty; + return SliverAppBar( + expandedHeight: + widget.hasHeroIcon ? 148.0 + _size.height : 72.0 + _size.height, + primary: true, + pinned: isMobile, + stretch: true, + surfaceTintColor: isMobile ? null : Colors.transparent, + leading: (widget.hasBackButton && isMobile) + ? const AutoLeadingButton() + : const SizedBox.shrink(), + actions: [ + if (widget.hasFlashButton && isMobile) + AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: IconButton( + onPressed: () { + showModalBottomSheet( + context: context, + builder: (final BuildContext context) => const JobsContent(), + ); + }, + icon: Icon( + isJobsListEmpty ? Ionicons.flash_outline : Ionicons.flash, + ), + color: isJobsListEmpty + ? Theme.of(context).colorScheme.onBackground + : Theme.of(context).colorScheme.primary, + ), + ), + const SizedBox.shrink(), + ], + flexibleSpace: FlexibleSpaceBar( + title: LayoutBuilder( + builder: (final context, final constraints) => SizedBox( + width: constraints.maxWidth - 72.0, + child: WidgetSize( + onChange: (final Size size) => setState(() => _size = size), + child: Text( + widget.heroTitle, + style: Theme.of(context).textTheme.titleLarge?.copyWith( + color: Theme.of(context).colorScheme.onSurface, + ), + overflow: TextOverflow.fade, + textAlign: TextAlign.center, + ), + ), + ), + ), + expandedTitleScale: 1.2, + centerTitle: true, + collapseMode: CollapseMode.pin, + titlePadding: const EdgeInsets.only( + bottom: 12.0, + top: 16.0, + ), + background: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + const SizedBox(height: 72.0), + if (widget.hasHeroIcon) widget.heroIconWidget, + ], + ), + ), + ); + } +} diff --git a/lib/ui/layouts/responsive_layout_with_infobox.dart b/lib/ui/layouts/responsive_layout_with_infobox.dart new file mode 100644 index 00000000..4ef467ab --- /dev/null +++ b/lib/ui/layouts/responsive_layout_with_infobox.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; +import 'package:selfprivacy/utils/breakpoints.dart'; + +class ResponsiveLayoutWithInfobox extends StatelessWidget { + const ResponsiveLayoutWithInfobox({ + required this.primaryColumn, + this.topChild, + this.secondaryColumn, + super.key, + }); + + final Widget? topChild; + final Widget primaryColumn; + final Widget? secondaryColumn; + + @override + Widget build(final BuildContext context) { + final hasSecondaryColumn = secondaryColumn != null; + final hasTopChild = topChild != null; + + if (Breakpoints.large.isActive(context)) { + return LayoutBuilder( + builder: (final context, final constraints) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (hasTopChild) + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width: constraints.maxWidth * 0.9, + child: topChild, + ), + ], + ), + if (hasTopChild) const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: hasSecondaryColumn + ? constraints.maxWidth * 0.7 + : constraints.maxWidth * 0.9, + child: primaryColumn, + ), + if (hasSecondaryColumn) const SizedBox(width: 16), + if (hasSecondaryColumn) + SizedBox( + width: constraints.maxWidth * 0.2, + child: secondaryColumn, + ), + ], + ), + ], + ), + ); + } + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (hasTopChild) topChild!, + const SizedBox(height: 16), + primaryColumn, + const SizedBox(height: 32), + if (hasSecondaryColumn) secondaryColumn!, + ], + ); + } +} diff --git a/lib/ui/layouts/root_scaffold_with_navigation.dart b/lib/ui/layouts/root_scaffold_with_navigation.dart new file mode 100644 index 00000000..fcc58515 --- /dev/null +++ b/lib/ui/layouts/root_scaffold_with_navigation.dart @@ -0,0 +1,277 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:selfprivacy/ui/components/pre_styled_buttons/flash_fab.dart'; +import 'package:selfprivacy/ui/components/drawers/support_drawer.dart'; +import 'package:selfprivacy/ui/router/root_destinations.dart'; +import 'package:selfprivacy/utils/breakpoints.dart'; + +class RootScaffoldWithNavigation extends StatelessWidget { + const RootScaffoldWithNavigation({ + required this.child, + required this.title, + required this.destinations, + this.showBottomBar = true, + this.showFab = true, + super.key, + }); + + final Widget child; + final String title; + final bool showBottomBar; + final List destinations; + final bool showFab; + + @override + // ignore: prefer_expression_function_bodies + Widget build(final BuildContext context) { + return Scaffold( + appBar: Breakpoints.mediumAndUp.isActive(context) + ? PreferredSize( + preferredSize: const Size.fromHeight(52), + child: _RootAppBar(title: title), + ) + : null, + endDrawer: const SupportDrawer(), + endDrawerEnableOpenDragGesture: false, + body: Row( + children: [ + if (Breakpoints.medium.isActive(context)) + _MainScreenNavigationRail( + destinations: destinations, + showFab: showFab, + ), + if (Breakpoints.large.isActive(context)) + _MainScreenNavigationDrawer( + destinations: destinations, + showFab: showFab, + ), + Expanded(child: child), + ], + ), + bottomNavigationBar: _BottomBar( + destinations: destinations, + hidden: !(Breakpoints.small.isActive(context) && showBottomBar), + key: const Key('bottomBar'), + ), + floatingActionButton: + showFab && Breakpoints.small.isActive(context) && showBottomBar + ? const BrandFab() + : null, + ); + } +} + +class _RootAppBar extends StatelessWidget { + const _RootAppBar({ + required this.title, + }); + + final String title; + + @override + Widget build(final BuildContext context) => AppBar( + title: AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + transitionBuilder: + (final Widget child, final Animation animation) => + SlideTransition( + position: animation.drive( + Tween( + begin: const Offset(0.0, 0.2), + end: Offset.zero, + ), + ), + child: FadeTransition( + opacity: animation, + child: child, + ), + ), + child: SizedBox( + key: ValueKey(title), + width: double.infinity, + child: Text( + title, + ), + ), + ), + leading: context.router.pageCount > 1 + ? IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () => context.router.pop(), + ) + : null, + actions: const [ + SizedBox.shrink(), + ], + ); +} + +class _MainScreenNavigationRail extends StatelessWidget { + const _MainScreenNavigationRail({ + required this.destinations, + this.showFab = true, + }); + + final List destinations; + final bool showFab; + + @override + Widget build(final BuildContext context) { + int? activeIndex = destinations.indexWhere( + (final destination) => + context.router.isRouteActive(destination.route.routeName), + ); + + final prevActiveIndex = destinations.indexWhere( + (final destination) => context.router.stack + .any((final route) => route.name == destination.route.routeName), + ); + + if (activeIndex == -1) { + if (prevActiveIndex != -1) { + activeIndex = prevActiveIndex; + } else { + activeIndex = 0; + } + } + + final isExtended = Breakpoints.large.isActive(context); + + return LayoutBuilder( + builder: (final context, final constraints) => SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints(minHeight: constraints.maxHeight), + child: IntrinsicHeight( + child: NavigationRail( + backgroundColor: Colors.transparent, + labelType: isExtended + ? NavigationRailLabelType.none + : NavigationRailLabelType.all, + extended: isExtended, + leading: showFab + ? const BrandFab( + extended: false, + ) + : null, + groupAlignment: 0.0, + destinations: destinations + .map( + (final destination) => NavigationRailDestination( + icon: Icon(destination.icon), + label: Text(destination.label), + ), + ) + .toList(), + selectedIndex: activeIndex, + onDestinationSelected: (final index) { + context.router.replaceAll([destinations[index].route]); + }, + ), + ), + ), + ), + ); + } +} + +class _BottomBar extends StatelessWidget { + const _BottomBar({ + required this.destinations, + required this.hidden, + super.key, + }); + + final List destinations; + final bool hidden; + + @override + Widget build(final BuildContext context) { + final prevActiveIndex = destinations.indexWhere( + (final destination) => context.router.stack + .any((final route) => route.name == destination.route.routeName), + ); + + return AnimatedContainer( + duration: const Duration(milliseconds: 500), + height: hidden ? 0 : 80, + curve: Curves.easeInOutCubicEmphasized, + clipBehavior: Clip.antiAlias, + decoration: BoxDecoration( + color: Theme.of(context).scaffoldBackgroundColor, + ), + child: NavigationBar( + selectedIndex: prevActiveIndex == -1 ? 0 : prevActiveIndex, + labelBehavior: NavigationDestinationLabelBehavior.onlyShowSelected, + onDestinationSelected: (final index) { + context.router.replaceAll([destinations[index].route]); + }, + destinations: destinations + .map( + (final destination) => NavigationDestination( + icon: Icon(destination.icon), + label: destination.label, + ), + ) + .toList(), + ), + ); + } +} + +class _MainScreenNavigationDrawer extends StatelessWidget { + const _MainScreenNavigationDrawer({ + required this.destinations, + this.showFab = true, + }); + + final List destinations; + final bool showFab; + + @override + Widget build(final BuildContext context) { + int? activeIndex = destinations.indexWhere( + (final destination) => + context.router.isRouteActive(destination.route.routeName), + ); + + final prevActiveIndex = destinations.indexWhere( + (final destination) => context.router.stack + .any((final route) => route.name == destination.route.routeName), + ); + + if (activeIndex == -1) { + if (prevActiveIndex != -1) { + activeIndex = prevActiveIndex; + } else { + activeIndex = 0; + } + } + + return SizedBox( + height: MediaQuery.of(context).size.height, + width: 296, + child: NavigationDrawer( + key: const Key('PrimaryNavigationDrawer'), + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + surfaceTintColor: Colors.transparent, + selectedIndex: activeIndex, + onDestinationSelected: (final index) { + context.router.replaceAll([destinations[index].route]); + }, + children: [ + const Padding( + padding: EdgeInsets.all(16.0), + child: BrandFab(extended: true), + ), + const SizedBox(height: 16), + ...destinations.map( + (final destination) => NavigationDrawerDestination( + icon: Icon(destination.icon), + label: Text(destination.label), + ), + ), + ], + ), + ); + } +} diff --git a/lib/ui/pages/backup_details/backup_details.dart b/lib/ui/pages/backup_details/backup_details.dart index 93cb0139..f909e481 100644 --- a/lib/ui/pages/backup_details/backup_details.dart +++ b/lib/ui/pages/backup_details/backup_details.dart @@ -1,26 +1,27 @@ +import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; import 'package:selfprivacy/logic/cubit/backups/backups_cubit.dart'; import 'package:selfprivacy/logic/models/json/backup.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/outlined_card.dart'; -import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; +import 'package:selfprivacy/ui/components/buttons/brand_button.dart'; +import 'package:selfprivacy/ui/components/cards/outlined_card.dart'; +import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart'; import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart'; -import 'package:selfprivacy/ui/components/brand_text/brand_text.dart'; import 'package:selfprivacy/ui/helpers/modals.dart'; GlobalKey navigatorKey = GlobalKey(); -class BackupDetails extends StatefulWidget { - const BackupDetails({super.key}); +@RoutePage() +class BackupDetailsPage extends StatefulWidget { + const BackupDetailsPage({super.key}); @override - State createState() => _BackupDetailsState(); + State createState() => _BackupDetailsPageState(); } -class _BackupDetailsState extends State +class _BackupDetailsPageState extends State with SingleTickerProviderStateMixin { @override Widget build(final BuildContext context) { @@ -57,7 +58,10 @@ class _BackupDetailsState extends State text: 'backup.initialize'.tr(), ), if (backupStatus == BackupStatusEnum.initializing) - BrandText.body1('backup.waiting_for_rebuild'.tr()), + Text( + 'backup.waiting_for_rebuild'.tr(), + style: Theme.of(context).textTheme.bodyMedium, + ), if (backupStatus != BackupStatusEnum.initializing && backupStatus != BackupStatusEnum.noKey) OutlinedCard( @@ -227,7 +231,10 @@ class _BackupDetailsState extends State ), ), if (backupStatus == BackupStatusEnum.error) - BrandText.body1(backupError.toString()), + Text( + backupError.toString(), + style: Theme.of(context).textTheme.bodyMedium, + ), ], ); } diff --git a/lib/ui/pages/devices/devices.dart b/lib/ui/pages/devices/devices.dart index 31010fd4..52fffdbe 100644 --- a/lib/ui/pages/devices/devices.dart +++ b/lib/ui/pages/devices/devices.dart @@ -1,3 +1,4 @@ +import 'package:auto_route/auto_route.dart'; import 'package:cubit_form/cubit_form.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; @@ -5,11 +6,12 @@ import 'package:selfprivacy/logic/common_enum/common_enum.dart'; import 'package:selfprivacy/logic/cubit/devices/devices_cubit.dart'; import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; import 'package:selfprivacy/logic/models/json/api_token.dart'; -import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; +import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart'; import 'package:selfprivacy/ui/components/info_box/info_box.dart'; import 'package:selfprivacy/ui/pages/devices/new_device.dart'; import 'package:selfprivacy/utils/route_transitions/basic.dart'; +@RoutePage() class DevicesScreen extends StatefulWidget { const DevicesScreen({super.key}); @@ -25,7 +27,7 @@ class _DevicesScreenState extends State { return RefreshIndicator( onRefresh: () async { - context.read().refresh(); + await context.read().refresh(); }, child: BrandHeroScreen( heroTitle: 'devices.main_screen.header'.tr(), @@ -90,8 +92,7 @@ class _DevicesInfo extends StatelessWidget { ), ), ...devicesStatus.otherDevices - .map((final device) => _DeviceTile(device: device)) - .toList(), + .map((final device) => _DeviceTile(device: device)), ], ); } diff --git a/lib/ui/pages/devices/new_device.dart b/lib/ui/pages/devices/new_device.dart index 8310b127..9a64fa72 100644 --- a/lib/ui/pages/devices/new_device.dart +++ b/lib/ui/pages/devices/new_device.dart @@ -3,8 +3,8 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:selfprivacy/logic/cubit/devices/devices_cubit.dart'; import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; -import 'package:selfprivacy/ui/components/brand_button/brand_button.dart'; -import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; +import 'package:selfprivacy/ui/components/buttons/brand_button.dart'; +import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart'; class NewDeviceScreen extends StatelessWidget { const NewDeviceScreen({super.key}); diff --git a/lib/ui/pages/dns_details/dns_details.dart b/lib/ui/pages/dns_details/dns_details.dart index 98f23979..98e567a9 100644 --- a/lib/ui/pages/dns_details/dns_details.dart +++ b/lib/ui/pages/dns_details/dns_details.dart @@ -1,13 +1,15 @@ +import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/desired_dns_record.dart'; import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; import 'package:selfprivacy/logic/cubit/dns_records/dns_records_cubit.dart'; -import 'package:selfprivacy/ui/components/brand_cards/filled_card.dart'; -import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; +import 'package:selfprivacy/ui/components/cards/filled_card.dart'; +import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart'; import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart'; +@RoutePage() class DnsDetailsPage extends StatefulWidget { const DnsDetailsPage({super.key}); @@ -158,8 +160,7 @@ class _DnsDetailsPageState extends State { ), ], ), - ) - .toList(), + ), const SizedBox(height: 16.0), ListTile( title: Text( @@ -200,8 +201,7 @@ class _DnsDetailsPageState extends State { ), ], ), - ) - .toList(), + ), ], ); } diff --git a/lib/ui/pages/more/about_application.dart b/lib/ui/pages/more/about_application.dart index 7a9dc11e..54e493de 100644 --- a/lib/ui/pages/more/about_application.dart +++ b/lib/ui/pages/more/about_application.dart @@ -1,67 +1,73 @@ +import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; -import 'package:selfprivacy/config/brand_theme.dart'; import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server_api.dart'; -import 'package:selfprivacy/ui/components/brand_header/brand_header.dart'; -import 'package:selfprivacy/ui/components/brand_text/brand_text.dart'; +import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; +import 'package:selfprivacy/ui/components/brand_md/brand_md.dart'; import 'package:package_info/package_info.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart'; import 'package:url_launcher/url_launcher.dart'; +@RoutePage() class AboutApplicationPage extends StatelessWidget { const AboutApplicationPage({super.key}); @override - Widget build(final BuildContext context) => SafeArea( - child: Scaffold( - appBar: PreferredSize( - preferredSize: const Size.fromHeight(52), - child: BrandHeader( - title: 'about_application_page.title'.tr(), - hasBackButton: true, + Widget build(final BuildContext context) { + final bool isReady = context.watch().state + is ServerInstallationFinished; + + return BrandHeroScreen( + hasBackButton: true, + hasFlashButton: false, + heroTitle: 'about_application_page.title'.tr(), + children: [ + FutureBuilder( + future: _packageVersion(), + builder: (final context, final snapshot) => Text( + 'about_application_page.application_version_text' + .tr(args: [snapshot.data.toString()]), + style: Theme.of(context).textTheme.bodyLarge, + ), + ), + if (isReady) + FutureBuilder( + future: _apiVersion(), + builder: (final context, final snapshot) => Text( + 'about_application_page.api_version_text' + .tr(args: [snapshot.data.toString()]), + style: Theme.of(context).textTheme.bodyLarge, ), ), - body: ListView( - padding: paddingH15V0, + const SizedBox(height: 10), + // Button to call showAboutDialog + TextButton( + onPressed: () => showAboutDialog( + context: context, + applicationName: 'SelfPrivacy', + applicationLegalese: '© 2022 SelfPrivacy', + // Link to privacy policy children: [ - const SizedBox(height: 10), - FutureBuilder( - future: _packageVersion(), - builder: (final context, final snapshot) => BrandText.body1( - 'about_application_page.application_version_text' - .tr(args: [snapshot.data.toString()]), - ), - ), - FutureBuilder( - future: _apiVersion(), - builder: (final context, final snapshot) => BrandText.body1( - 'about_application_page.api_version_text' - .tr(args: [snapshot.data.toString()]), - ), - ), - const SizedBox(height: 10), - // Button to call showAboutDialog TextButton( - onPressed: () => showAboutDialog( - context: context, - applicationName: 'SelfPrivacy', - applicationLegalese: '© 2022 SelfPrivacy', - // Link to privacy policy - children: [ - TextButton( - onPressed: () => launchUrl( - Uri.parse('https://selfprivacy.ru/privacy-policy'), - mode: LaunchMode.externalApplication, - ), - child: Text('about_application_page.privacy_policy'.tr()), - ), - ], + onPressed: () => launchUrl( + Uri.parse('https://selfprivacy.ru/privacy-policy'), + mode: LaunchMode.externalApplication, ), - child: const Text('Show about dialog'), + child: Text('about_application_page.privacy_policy'.tr()), ), ], ), + child: const Text('Show about dialog'), ), - ); + const SizedBox(height: 8), + const Divider(height: 0), + const SizedBox(height: 8), + const BrandMarkdown( + fileName: 'about', + ), + ], + ); + } Future _packageVersion() async { String packageVersion = 'unknown'; diff --git a/lib/ui/pages/more/app_settings/app_setting.dart b/lib/ui/pages/more/app_settings/app_setting.dart deleted file mode 100644 index 4287e985..00000000 --- a/lib/ui/pages/more/app_settings/app_setting.dart +++ /dev/null @@ -1,229 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:selfprivacy/config/brand_colors.dart'; -import 'package:selfprivacy/config/brand_theme.dart'; -import 'package:selfprivacy/logic/cubit/app_settings/app_settings_cubit.dart'; -import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; -import 'package:selfprivacy/ui/components/action_button/action_button.dart'; -import 'package:selfprivacy/ui/components/brand_alert/brand_alert.dart'; -import 'package:selfprivacy/ui/components/brand_header/brand_header.dart'; -import 'package:selfprivacy/ui/components/brand_switch/brand_switch.dart'; -import 'package:selfprivacy/ui/components/brand_text/brand_text.dart'; -import 'package:selfprivacy/utils/named_font_weight.dart'; -import 'package:easy_localization/easy_localization.dart'; - -class AppSettingsPage extends StatefulWidget { - const AppSettingsPage({super.key}); - - @override - State createState() => _AppSettingsPageState(); -} - -class _AppSettingsPageState extends State { - @override - Widget build(final BuildContext context) { - final bool isDarkModeOn = - context.watch().state.isDarkModeOn; - - return SafeArea( - child: Builder( - builder: (final context) => Scaffold( - appBar: PreferredSize( - preferredSize: const Size.fromHeight(52), - child: BrandHeader( - title: 'application_settings.title'.tr(), - hasBackButton: true, - ), - ), - body: ListView( - padding: paddingH15V0, - children: [ - const Divider(height: 1), - Container( - padding: const EdgeInsets.symmetric(vertical: 16), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible( - child: _TextColumn( - title: 'application_settings.dark_theme_title'.tr(), - value: - 'application_settings.dark_theme_description'.tr(), - hasWarning: false, - ), - ), - const SizedBox(width: 5), - BrandSwitch( - value: Theme.of(context).brightness == Brightness.dark, - onChanged: (final value) => context - .read() - .updateDarkMode(isDarkModeOn: !isDarkModeOn), - ), - ], - ), - ), - const Divider(height: 0), - Container( - padding: const EdgeInsets.symmetric(vertical: 16), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible( - child: _TextColumn( - title: 'application_settings.reset_config_title'.tr(), - value: 'application_settings.reset_config_description' - .tr(), - hasWarning: false, - ), - ), - const SizedBox(width: 5), - ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: BrandColors.red1, - ), - child: Text( - 'basis.reset'.tr(), - style: const TextStyle( - color: BrandColors.white, - fontWeight: NamedFontWeight.demiBold, - ), - ), - onPressed: () { - showDialog( - context: context, - builder: (final _) => BrandAlert( - title: 'modals.are_you_sure'.tr(), - contentText: 'modals.purge_all_keys'.tr(), - actions: [ - ActionButton( - text: 'modals.purge_all_keys_confirm'.tr(), - isRed: true, - onPressed: () { - context - .read() - .clearAppConfig(); - Navigator.of(context).pop(); - }, - ), - ActionButton( - text: 'basis.cancel'.tr(), - ), - ], - ), - ); - }, - ), - ], - ), - ), - const Divider(height: 0), - _deleteServer(context) - ], - ), - ), - ), - ); - } - - Widget _deleteServer(final BuildContext context) { - final bool isDisabled = - context.watch().state.serverDetails == null; - return Container( - padding: const EdgeInsets.symmetric(vertical: 16), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible( - child: _TextColumn( - title: 'application_settings.delete_server_title'.tr(), - value: 'application_settings.delete_server_description'.tr(), - hasWarning: false, - ), - ), - const SizedBox(width: 5), - ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: BrandColors.red1, - ), - onPressed: isDisabled - ? null - : () { - showDialog( - context: context, - builder: (final _) => BrandAlert( - title: 'modals.are_you_sure'.tr(), - contentText: 'modals.delete_server_volume'.tr(), - actions: [ - ActionButton( - text: 'modals.yes'.tr(), - isRed: true, - onPressed: () async { - showDialog( - context: context, - builder: (final context) => Container( - alignment: Alignment.center, - child: const CircularProgressIndicator(), - ), - ); - await context - .read() - .serverDelete(); - if (!mounted) { - return; - } - Navigator.of(context).pop(); - }, - ), - ActionButton( - text: 'basis.cancel'.tr(), - ), - ], - ), - ); - }, - child: Text( - 'basis.delete'.tr(), - style: const TextStyle( - color: BrandColors.white, - fontWeight: NamedFontWeight.demiBold, - ), - ), - ), - ], - ), - ); - } -} - -class _TextColumn extends StatelessWidget { - const _TextColumn({ - required this.title, - required this.value, - this.hasWarning = false, - }); - - final String title; - final String value; - final bool hasWarning; - @override - Widget build(final BuildContext context) => Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - BrandText.body1( - title, - style: TextStyle(color: hasWarning ? BrandColors.warning : null), - ), - const SizedBox(height: 5), - BrandText.body1( - value, - style: const TextStyle( - fontSize: 13, - height: 1.53, - color: BrandColors.gray1, - ).merge(TextStyle(color: hasWarning ? BrandColors.warning : null)), - ), - ], - ); -} diff --git a/lib/ui/pages/more/app_settings/app_settings.dart b/lib/ui/pages/more/app_settings/app_settings.dart new file mode 100644 index 00000000..6ab69f40 --- /dev/null +++ b/lib/ui/pages/more/app_settings/app_settings.dart @@ -0,0 +1,151 @@ +import 'dart:async'; + +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:selfprivacy/logic/cubit/app_settings/app_settings_cubit.dart'; +import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; +import 'package:selfprivacy/ui/components/buttons/dialog_action_button.dart'; +import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart'; +import 'package:easy_localization/easy_localization.dart'; + +@RoutePage() +class AppSettingsPage extends StatefulWidget { + const AppSettingsPage({super.key}); + + @override + State createState() => _AppSettingsPageState(); +} + +class _AppSettingsPageState extends State { + @override + Widget build(final BuildContext context) { + final bool isDarkModeOn = + context.watch().state.isDarkModeOn; + + final bool isSystemDarkModeOn = + context.watch().state.isAutoDarkModeOn; + + return BrandHeroScreen( + hasBackButton: true, + hasFlashButton: false, + bodyPadding: const EdgeInsets.symmetric(vertical: 16), + heroTitle: 'application_settings.title'.tr(), + children: [ + SwitchListTile( + title: Text('application_settings.system_dark_theme_title'.tr()), + subtitle: + Text('application_settings.system_dark_theme_description'.tr()), + value: isSystemDarkModeOn, + onChanged: (final value) => context + .read() + .updateAutoDarkMode(isAutoDarkModeOn: !isSystemDarkModeOn), + ), + SwitchListTile( + title: Text('application_settings.dark_theme_title'.tr()), + subtitle: Text('application_settings.dark_theme_description'.tr()), + value: Theme.of(context).brightness == Brightness.dark, + onChanged: isSystemDarkModeOn + ? null + : (final value) => context + .read() + .updateDarkMode(isDarkModeOn: !isDarkModeOn), + ), + const Divider(height: 0), + Padding( + padding: const EdgeInsets.all(16), + child: Text( + 'application_settings.dangerous_settings'.tr(), + style: Theme.of(context).textTheme.labelLarge!.copyWith( + color: Theme.of(context).colorScheme.error, + ), + ), + ), + const _ResetAppTile(), + // const Divider(height: 0), + _deleteServer(context) + ], + ); + } + + Widget _deleteServer(final BuildContext context) { + final bool isDisabled = + context.watch().state.serverDetails == null; + return ListTile( + title: Text('application_settings.delete_server_title'.tr()), + subtitle: Text('application_settings.delete_server_description'.tr()), + textColor: isDisabled + ? Theme.of(context).colorScheme.onBackground.withOpacity(0.5) + : Theme.of(context).colorScheme.onBackground, + onTap: isDisabled + ? null + : () { + showDialog( + context: context, + builder: (final _) => AlertDialog( + title: Text('modals.are_you_sure'.tr()), + content: Text('modals.delete_server_volume'.tr()), + actions: [ + DialogActionButton( + text: 'modals.yes'.tr(), + isRed: true, + onPressed: () async { + unawaited( + showDialog( + context: context, + builder: (final context) => Container( + alignment: Alignment.center, + child: const CircularProgressIndicator(), + ), + ), + ); + await context + .read() + .serverDelete(); + if (!mounted) { + return; + } + Navigator.of(context).pop(); + }, + ), + DialogActionButton( + text: 'basis.cancel'.tr(), + ), + ], + ), + ); + }, + ); + } +} + +class _ResetAppTile extends StatelessWidget { + const _ResetAppTile(); + + @override + Widget build(final BuildContext context) => ListTile( + title: Text('application_settings.reset_config_title'.tr()), + subtitle: Text('application_settings.reset_config_description'.tr()), + onTap: () { + showDialog( + context: context, + builder: (final _) => AlertDialog( + title: Text('modals.are_you_sure'.tr()), + content: Text('modals.purge_all_keys'.tr()), + actions: [ + DialogActionButton( + text: 'modals.purge_all_keys_confirm'.tr(), + isRed: true, + onPressed: () { + context.read().clearAppConfig(); + Navigator.of(context).pop(); + }, + ), + DialogActionButton( + text: 'basis.cancel'.tr(), + ), + ], + ), + ); + }, + ); +} diff --git a/lib/ui/pages/more/app_settings/developer_settings.dart b/lib/ui/pages/more/app_settings/developer_settings.dart new file mode 100644 index 00000000..220cb791 --- /dev/null +++ b/lib/ui/pages/more/app_settings/developer_settings.dart @@ -0,0 +1,85 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:selfprivacy/logic/api_maps/staging_options.dart'; +import 'package:selfprivacy/logic/cubit/app_settings/app_settings_cubit.dart'; +import 'package:selfprivacy/logic/cubit/devices/devices_cubit.dart'; +import 'package:selfprivacy/logic/cubit/recovery_key/recovery_key_cubit.dart'; +import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart'; +import 'package:easy_localization/easy_localization.dart'; + +@RoutePage() +class DeveloperSettingsPage extends StatefulWidget { + const DeveloperSettingsPage({super.key}); + + @override + State createState() => _DeveloperSettingsPageState(); +} + +class _DeveloperSettingsPageState extends State { + @override + Widget build(final BuildContext context) => BrandHeroScreen( + hasBackButton: true, + hasFlashButton: false, + bodyPadding: const EdgeInsets.symmetric(vertical: 16), + heroTitle: 'developer_settings.title'.tr(), + heroSubtitle: 'developer_settings.subtitle'.tr(), + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Text( + 'developer_settings.server_setup'.tr(), + style: Theme.of(context).textTheme.labelLarge!.copyWith( + color: Theme.of(context).colorScheme.secondary, + ), + ), + ), + SwitchListTile( + title: Text('developer_settings.use_staging_acme'.tr()), + subtitle: + Text('developer_settings.use_staging_acme_description'.tr()), + value: StagingOptions.stagingAcme, + onChanged: null, + ), + Padding( + padding: const EdgeInsets.all(16), + child: Text( + 'developer_settings.routing'.tr(), + style: Theme.of(context).textTheme.labelLarge!.copyWith( + color: Theme.of(context).colorScheme.secondary, + ), + ), + ), + ListTile( + title: Text('developer_settings.reset_onboarding'.tr()), + subtitle: + Text('developer_settings.reset_onboarding_description'.tr()), + enabled: + !context.watch().state.isOnboardingShowing, + onTap: () => context + .read() + .turnOffOnboarding(isOnboardingShowing: true), + ), + Padding( + padding: const EdgeInsets.all(16), + child: Text( + 'developer_settings.cubit_statuses'.tr(), + style: Theme.of(context).textTheme.labelLarge!.copyWith( + color: Theme.of(context).colorScheme.secondary, + ), + ), + ), + ListTile( + title: const Text('ApiDevicesCubit'), + subtitle: Text( + context.watch().state.status.toString(), + ), + ), + ListTile( + title: const Text('RecoveryKeyCubit'), + subtitle: Text( + context.watch().state.loadingStatus.toString(), + ), + ), + ], + ); +} diff --git a/lib/ui/pages/more/console.dart b/lib/ui/pages/more/console.dart index 6ad31ea5..f338b580 100644 --- a/lib/ui/pages/more/console.dart +++ b/lib/ui/pages/more/console.dart @@ -1,20 +1,21 @@ +import 'package:auto_route/auto_route.dart'; import 'dart:collection'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:selfprivacy/config/brand_colors.dart'; import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/logic/models/message.dart'; -import 'package:selfprivacy/ui/components/brand_header/brand_header.dart'; +import 'package:selfprivacy/ui/components/list_tiles/log_list_tile.dart'; -class Console extends StatefulWidget { - const Console({super.key}); +@RoutePage() +class ConsolePage extends StatefulWidget { + const ConsolePage({super.key}); @override - State createState() => _ConsoleState(); + State createState() => _ConsolePageState(); } -class _ConsoleState extends State { +class _ConsolePageState extends State { @override void initState() { getIt.get().addListener(update); @@ -28,21 +29,31 @@ class _ConsoleState extends State { super.dispose(); } - void update() => setState(() => {}); + bool paused = false; + + void update() { + if (!paused) { + setState(() => {}); + } + } @override Widget build(final BuildContext context) => SafeArea( child: Scaffold( - appBar: PreferredSize( - preferredSize: const Size.fromHeight(53), - child: Column( - children: [ - BrandHeader( - title: 'console_page.title'.tr(), - hasBackButton: true, - ), - ], + appBar: AppBar( + title: Text('console_page.title'.tr()), + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () => Navigator.of(context).pop(), ), + actions: [ + IconButton( + icon: Icon( + paused ? Icons.play_arrow_outlined : Icons.pause_outlined, + ), + onPressed: () => setState(() => paused = !paused), + ), + ], ), body: FutureBuilder( future: getIt.allReady(), @@ -61,30 +72,7 @@ class _ConsoleState extends State { const SizedBox(height: 20), ...UnmodifiableListView( messages - .map((final message) { - final bool isError = - message.type == MessageType.warning; - return Padding( - padding: const EdgeInsets.symmetric(vertical: 4), - child: RichText( - text: TextSpan( - style: DefaultTextStyle.of(context).style, - children: [ - TextSpan( - text: - '${message.timeString}${isError ? '(Error)' : ''}: \n', - style: TextStyle( - fontWeight: FontWeight.bold, - color: - isError ? BrandColors.red1 : null, - ), - ), - TextSpan(text: message.text), - ], - ), - ), - ); - }) + .map((final message) => LogListItem(message: message)) .toList() .reversed, ), diff --git a/lib/ui/pages/more/more.dart b/lib/ui/pages/more/more.dart index 77b1d34a..3d85b093 100644 --- a/lib/ui/pages/more/more.dart +++ b/lib/ui/pages/more/more.dart @@ -1,3 +1,4 @@ +import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:ionicons/ionicons.dart'; @@ -5,23 +6,13 @@ import 'package:selfprivacy/config/brand_theme.dart'; import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; import 'package:selfprivacy/logic/cubit/server_volumes/server_volume_cubit.dart'; import 'package:selfprivacy/logic/cubit/services/services_cubit.dart'; -import 'package:selfprivacy/ui/components/brand_cards/filled_card.dart'; +import 'package:selfprivacy/ui/components/cards/filled_card.dart'; import 'package:selfprivacy/ui/components/brand_header/brand_header.dart'; import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart'; -import 'package:selfprivacy/ui/pages/devices/devices.dart'; -import 'package:selfprivacy/ui/pages/recovery_key/recovery_key.dart'; -import 'package:selfprivacy/ui/pages/server_storage/binds_migration/services_migration.dart'; -import 'package:selfprivacy/ui/pages/setup/initializing/initializing.dart'; -import 'package:selfprivacy/ui/pages/onboarding/onboarding.dart'; -import 'package:selfprivacy/ui/pages/root_route.dart'; -import 'package:selfprivacy/ui/pages/users/users.dart'; -import 'package:selfprivacy/utils/route_transitions/basic.dart'; - -import 'package:selfprivacy/ui/pages/more/about_us.dart'; -import 'package:selfprivacy/ui/pages/more/app_settings/app_setting.dart'; -import 'package:selfprivacy/ui/pages/more/console.dart'; -import 'package:selfprivacy/ui/pages/more/about_application.dart'; +import 'package:selfprivacy/utils/breakpoints.dart'; +import 'package:selfprivacy/ui/router/router.dart'; +@RoutePage() class MorePage extends StatelessWidget { const MorePage({super.key}); @@ -34,12 +25,14 @@ class MorePage extends StatelessWidget { context.watch().state.usesBinds; return Scaffold( - appBar: PreferredSize( - preferredSize: const Size.fromHeight(52), - child: BrandHeader( - title: 'basis.more'.tr(), - ), - ), + appBar: Breakpoints.small.isActive(context) + ? PreferredSize( + preferredSize: const Size.fromHeight(52), + child: BrandHeader( + title: 'basis.more'.tr(), + ), + ) + : null, body: ListView( children: [ Padding( @@ -50,7 +43,7 @@ class MorePage extends StatelessWidget { _MoreMenuItem( title: 'storage.start_migration_button'.tr(), iconData: Icons.drive_file_move_outline, - goTo: ServicesMigrationPage( + goTo: () => ServicesMigrationRoute( diskStatus: context .watch() .state @@ -77,7 +70,7 @@ class MorePage extends StatelessWidget { _MoreMenuItem( title: 'more_page.configuration_wizard'.tr(), iconData: Icons.change_history_outlined, - goTo: const InitializingPage(), + goTo: () => const InitializingRoute(), subtitle: 'not_ready_card.in_menu'.tr(), accent: true, ), @@ -85,47 +78,43 @@ class MorePage extends StatelessWidget { _MoreMenuItem( title: 'more_page.create_ssh_key'.tr(), iconData: Ionicons.key_outline, - goTo: const UserDetails( + goTo: () => UserDetailsRoute( login: 'root', ), ), if (isReady) _MoreMenuItem( iconData: Icons.password_outlined, - goTo: const RecoveryKey(), + goTo: () => const RecoveryKeyRoute(), title: 'recovery_key.key_main_header'.tr(), ), if (isReady) _MoreMenuItem( iconData: Icons.devices_outlined, - goTo: const DevicesScreen(), + goTo: () => const DevicesRoute(), title: 'devices.main_screen.header'.tr(), ), _MoreMenuItem( title: 'more_page.application_settings'.tr(), iconData: Icons.settings_outlined, - goTo: const AppSettingsPage(), - ), - _MoreMenuItem( - title: 'more_page.about_project'.tr(), - iconData: BrandIcons.engineer, - goTo: const AboutUsPage(), + goTo: () => const AppSettingsRoute(), ), _MoreMenuItem( title: 'more_page.about_application'.tr(), iconData: BrandIcons.fire, - goTo: const AboutApplicationPage(), + goTo: () => const AboutApplicationRoute(), + longGoTo: const DeveloperSettingsRoute(), ), if (!isReady) _MoreMenuItem( title: 'more_page.onboarding'.tr(), iconData: BrandIcons.start, - goTo: const OnboardingPage(nextPage: RootPage()), + goTo: () => const OnboardingRoute(), ), _MoreMenuItem( title: 'more_page.console'.tr(), iconData: BrandIcons.terminal, - goTo: const Console(), + goTo: () => const ConsoleRoute(), ), ], ), @@ -140,14 +129,16 @@ class _MoreMenuItem extends StatelessWidget { const _MoreMenuItem({ required this.iconData, required this.title, + required this.goTo, this.subtitle, - this.goTo, + this.longGoTo, this.accent = false, }); final IconData iconData; final String title; - final Widget? goTo; + final PageRouteInfo Function() goTo; + final PageRouteInfo? longGoTo; final String? subtitle; final bool accent; @@ -160,9 +151,9 @@ class _MoreMenuItem extends StatelessWidget { tertiary: accent, child: ListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - onTap: goTo != null - ? () => Navigator.of(context).push(materialRoute(goTo!)) - : null, + onTap: () => context.pushRoute(goTo()), + onLongPress: + longGoTo != null ? () => context.pushRoute(longGoTo!) : null, leading: Icon( iconData, size: 24, diff --git a/lib/ui/pages/onboarding/onboarding.dart b/lib/ui/pages/onboarding/onboarding.dart index ac865b11..2e035d53 100644 --- a/lib/ui/pages/onboarding/onboarding.dart +++ b/lib/ui/pages/onboarding/onboarding.dart @@ -1,13 +1,14 @@ +import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:selfprivacy/logic/cubit/app_settings/app_settings_cubit.dart'; -import 'package:selfprivacy/ui/components/brand_button/brand_button.dart'; -import 'package:selfprivacy/utils/route_transitions/basic.dart'; +import 'package:selfprivacy/ui/components/buttons/brand_button.dart'; +import 'package:selfprivacy/ui/router/router.dart'; import 'package:easy_localization/easy_localization.dart'; +@RoutePage() class OnboardingPage extends StatefulWidget { - const OnboardingPage({required this.nextPage, super.key}); + const OnboardingPage({super.key}); - final Widget nextPage; @override State createState() => _OnboardingPageState(); } @@ -22,14 +23,14 @@ class _OnboardingPageState extends State { @override Widget build(final BuildContext context) => Scaffold( - body: PageView( - controller: pageController, - children: [ - _withPadding(firstPage()), - _withPadding(secondPage()), - ], - ), - ); + body: PageView( + controller: pageController, + children: [ + _withPadding(firstPage()), + _withPadding(secondPage()), + ], + ), + ); Widget _withPadding(final Widget child) => Padding( padding: const EdgeInsets.symmetric( @@ -76,7 +77,7 @@ class _OnboardingPageState extends State { pageController.animateToPage( 1, duration: const Duration(milliseconds: 300), - curve: Curves.easeIn, + curve: Curves.easeInOutCubicEmphasized, ); }, text: 'basis.next'.tr(), @@ -142,10 +143,10 @@ class _OnboardingPageState extends State { BrandButton.rised( onPressed: () { context.read().turnOffOnboarding(); - Navigator.of(context).pushAndRemoveUntil( - materialRoute(widget.nextPage), - (final route) => false, - ); + context.router.replaceAll([ + const RootRoute(), + const InitializingRoute(), + ]); }, text: 'basis.got_it'.tr(), ), diff --git a/lib/ui/pages/providers/providers.dart b/lib/ui/pages/providers/providers.dart index d234c984..25533a43 100644 --- a/lib/ui/pages/providers/providers.dart +++ b/lib/ui/pages/providers/providers.dart @@ -1,3 +1,4 @@ +import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:selfprivacy/config/brand_theme.dart'; @@ -10,13 +11,12 @@ import 'package:selfprivacy/ui/components/brand_header/brand_header.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/ui/components/not_ready_card/not_ready_card.dart'; -import 'package:selfprivacy/ui/pages/backup_details/backup_details.dart'; -import 'package:selfprivacy/ui/pages/dns_details/dns_details.dart'; -import 'package:selfprivacy/ui/pages/server_details/server_details_screen.dart'; -import 'package:selfprivacy/utils/route_transitions/basic.dart'; +import 'package:selfprivacy/ui/router/router.dart'; +import 'package:selfprivacy/utils/breakpoints.dart'; GlobalKey navigatorKey = GlobalKey(); +@RoutePage() class ProvidersPage extends StatefulWidget { const ProvidersPage({super.key}); @@ -61,12 +61,14 @@ class _ProvidersPageState extends State { } return Scaffold( - appBar: PreferredSize( - preferredSize: const Size.fromHeight(52), - child: BrandHeader( - title: 'basis.providers_title'.tr(), - ), - ), + appBar: Breakpoints.small.isActive(context) + ? PreferredSize( + preferredSize: const Size.fromHeight(52), + child: BrandHeader( + title: 'basis.providers_title'.tr(), + ), + ) + : null, body: ListView( padding: paddingH15V0, children: [ @@ -81,8 +83,7 @@ class _ProvidersPageState extends State { subtitle: diskStatus.isDiskOkay ? 'storage.status_ok'.tr() : 'storage.status_error'.tr(), - onTap: () => Navigator.of(context) - .push(materialRoute(const ServerDetailsScreen())), + onTap: () => context.pushRoute(const ServerDetailsRoute()), ), const SizedBox(height: 16), _Card( @@ -92,11 +93,7 @@ class _ProvidersPageState extends State { subtitle: appConfig.isDomainSelected ? appConfig.serverDomain!.domainName : '', - onTap: () => Navigator.of(context).push( - materialRoute( - const DnsDetailsPage(), - ), - ), + onTap: () => context.pushRoute(const DnsDetailsRoute()), ), const SizedBox(height: 16), // TODO: When backups are fixed, show this card @@ -108,8 +105,7 @@ class _ProvidersPageState extends State { icon: BrandIcons.save, title: 'backup.card_title'.tr(), subtitle: isBackupInitialized ? 'backup.card_subtitle'.tr() : '', - onTap: () => Navigator.of(context) - .push(materialRoute(const BackupDetails())), + onTap: () => context.pushRoute(const BackupDetailsRoute()), ), ], ), diff --git a/lib/ui/pages/recovery_key/recovery_key.dart b/lib/ui/pages/recovery_key/recovery_key.dart index 02a7ed9a..86d678bb 100644 --- a/lib/ui/pages/recovery_key/recovery_key.dart +++ b/lib/ui/pages/recovery_key/recovery_key.dart @@ -1,4 +1,4 @@ -import 'package:cubit_form/cubit_form.dart'; +import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -6,20 +6,21 @@ import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/logic/common_enum/common_enum.dart'; import 'package:selfprivacy/logic/cubit/recovery_key/recovery_key_cubit.dart'; import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; -import 'package:selfprivacy/ui/components/brand_button/brand_button.dart'; -import 'package:selfprivacy/ui/components/brand_cards/filled_card.dart'; -import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; +import 'package:selfprivacy/ui/components/buttons/brand_button.dart'; +import 'package:selfprivacy/ui/components/cards/filled_card.dart'; +import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart'; import 'package:selfprivacy/ui/pages/recovery_key/recovery_key_receiving.dart'; import 'package:selfprivacy/utils/route_transitions/basic.dart'; -class RecoveryKey extends StatefulWidget { - const RecoveryKey({super.key}); +@RoutePage() +class RecoveryKeyPage extends StatefulWidget { + const RecoveryKeyPage({super.key}); @override - State createState() => _RecoveryKeyState(); + State createState() => _RecoveryKeyPageState(); } -class _RecoveryKeyState extends State { +class _RecoveryKeyPageState extends State { @override void initState() { super.initState(); @@ -250,7 +251,7 @@ class _RecoveryKeyConfigurationState extends State { setState(() { _isLoading = false; }); - Navigator.of(context).push( + await Navigator.of(context).push( materialRoute( RecoveryKeyReceiving(recoveryKey: token), // TO DO ), diff --git a/lib/ui/pages/recovery_key/recovery_key_receiving.dart b/lib/ui/pages/recovery_key/recovery_key_receiving.dart index afca6d20..10f2e7c0 100644 --- a/lib/ui/pages/recovery_key/recovery_key_receiving.dart +++ b/lib/ui/pages/recovery_key/recovery_key_receiving.dart @@ -1,7 +1,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:selfprivacy/ui/components/brand_button/brand_button.dart'; -import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; +import 'package:selfprivacy/ui/components/buttons/brand_button.dart'; +import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart'; import 'package:selfprivacy/ui/components/info_box/info_box.dart'; class RecoveryKeyReceiving extends StatelessWidget { diff --git a/lib/ui/pages/root_route.dart b/lib/ui/pages/root_route.dart index ce1f344c..65d4cd8d 100644 --- a/lib/ui/pages/root_route.dart +++ b/lib/ui/pages/root_route.dart @@ -1,89 +1,153 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:selfprivacy/logic/cubit/app_settings/app_settings_cubit.dart'; import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; -import 'package:selfprivacy/ui/components/brand_tab_bar/brand_tab_bar.dart'; -import 'package:selfprivacy/ui/pages/more/more.dart'; -import 'package:selfprivacy/ui/pages/providers/providers.dart'; -import 'package:selfprivacy/ui/pages/services/services.dart'; -import 'package:selfprivacy/ui/pages/users/users.dart'; +import 'package:selfprivacy/ui/layouts/root_scaffold_with_navigation.dart'; +import 'package:selfprivacy/ui/router/root_destinations.dart'; -import 'package:selfprivacy/ui/components/pre_styled_buttons/flash_fab.dart'; +import 'package:selfprivacy/ui/router/router.dart'; -class RootPage extends StatefulWidget { +@RoutePage() +class RootPage extends StatefulWidget implements AutoRouteWrapper { const RootPage({super.key}); @override State createState() => _RootPageState(); + + @override + Widget wrappedRoute(final BuildContext context) => this; } class _RootPageState extends State with TickerProviderStateMixin { - late TabController tabController; + bool shouldUseSplitView() => false; - late final AnimationController _controller = AnimationController( - duration: const Duration(milliseconds: 400), - vsync: this, - ); - late final Animation _animation = CurvedAnimation( - parent: _controller, - curve: Curves.fastOutSlowIn, - ); - - @override - void initState() { - tabController = TabController(length: 4, vsync: this); - tabController.addListener(() { - setState(() { - tabController.index == 2 - ? _controller.forward() - : _controller.reverse(); - }); - }); - super.initState(); - } - - @override - void dispose() { - tabController.dispose(); - _controller.dispose(); - super.dispose(); - } + final destinations = rootDestinations; @override Widget build(final BuildContext context) { final bool isReady = context.watch().state is ServerInstallationFinished; - return Provider( - create: (final _) => ChangeTab(tabController.animateTo), - child: Scaffold( - body: TabBarView( - controller: tabController, - children: const [ - ProvidersPage(), - ServicesPage(), - UsersPage(), - MorePage(), + if (context.read().state.isOnboardingShowing) { + context.router.replace(const OnboardingRoute()); + } + + return AutoRouter( + builder: (final context, final child) { + final currentDestinationIndex = destinations.indexWhere( + (final destination) => + context.router.isRouteActive(destination.route.routeName), + ); + final isOtherRouterActive = + context.router.root.current.name != RootRoute.name; + final routeName = getRouteTitle(context.router.current.name).tr(); + return RootScaffoldWithNavigation( + title: routeName, + destinations: destinations, + showBottomBar: + !(currentDestinationIndex == -1 && !isOtherRouterActive), + showFab: isReady, + child: child, + ); + }, + ); + } +} + +class MainScreenNavigationRail extends StatelessWidget { + const MainScreenNavigationRail({ + required this.destinations, + super.key, + }); + + final List destinations; + + @override + Widget build(final BuildContext context) { + int? activeIndex = destinations.indexWhere( + (final destination) => + context.router.isRouteActive(destination.route.routeName), + ); + if (activeIndex == -1) { + activeIndex = null; + } + + return Padding( + padding: const EdgeInsets.all(8.0), + child: SizedBox( + height: MediaQuery.of(context).size.height, + width: 72, + child: LayoutBuilder( + builder: (final context, final constraints) => SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints(minHeight: constraints.maxHeight), + child: IntrinsicHeight( + child: NavigationRail( + backgroundColor: Colors.transparent, + labelType: NavigationRailLabelType.all, + destinations: destinations + .map( + (final destination) => NavigationRailDestination( + icon: Icon(destination.icon), + label: Text(destination.label), + ), + ) + .toList(), + selectedIndex: activeIndex, + onDestinationSelected: (final index) { + context.router.replaceAll([destinations[index].route]); + }, + ), + ), + ), + ), + ), + ), + ); + } +} + +class MainScreenNavigationDrawer extends StatelessWidget { + const MainScreenNavigationDrawer({ + required this.destinations, + super.key, + }); + + final List destinations; + + @override + Widget build(final BuildContext context) { + int? activeIndex = destinations.indexWhere( + (final destination) => + context.router.isRouteActive(destination.route.routeName), + ); + if (activeIndex == -1) { + activeIndex = null; + } + + return SizedBox( + height: MediaQuery.of(context).size.height, + width: 296, + child: LayoutBuilder( + builder: (final context, final constraints) => NavigationDrawer( + // backgroundColor: Theme.of(context).colorScheme.surfaceVariant, + // surfaceTintColor: Colors.transparent, + key: const Key('PrimaryNavigationDrawer'), + selectedIndex: activeIndex, + onDestinationSelected: (final index) { + context.router.replaceAll([destinations[index].route]); + }, + children: [ + const SizedBox(height: 18), + ...destinations.map( + (final destination) => NavigationDrawerDestination( + icon: Icon(destination.icon), + label: Text(destination.label), + ), + ), ], ), - bottomNavigationBar: BrandTabBar( - controller: tabController, - ), - floatingActionButton: isReady - ? SizedBox( - height: 104 + 16, - child: Column( - crossAxisAlignment: CrossAxisAlignment.end, - mainAxisAlignment: MainAxisAlignment.end, - children: [ - ScaleTransition( - scale: _animation, - child: const AddUserFab(), - ), - const SizedBox(height: 16), - const BrandFab(), - ], - ), - ) - : null, ), ); } diff --git a/lib/ui/pages/server_details/server_details_screen.dart b/lib/ui/pages/server_details/server_details_screen.dart index 487e1a25..a3a521b5 100644 --- a/lib/ui/pages/server_details/server_details_screen.dart +++ b/lib/ui/pages/server_details/server_details_screen.dart @@ -1,7 +1,7 @@ +import 'package:auto_route/auto_route.dart'; import 'package:cubit_form/cubit_form.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:selfprivacy/config/brand_colors.dart'; import 'package:selfprivacy/logic/common_enum/common_enum.dart'; import 'package:selfprivacy/logic/cubit/client_jobs/client_jobs_cubit.dart'; import 'package:selfprivacy/logic/cubit/metrics/metrics_cubit.dart'; @@ -10,18 +10,17 @@ import 'package:selfprivacy/logic/cubit/server_installation/server_installation_ import 'package:selfprivacy/logic/cubit/server_volumes/server_volume_cubit.dart'; import 'package:selfprivacy/logic/models/auto_upgrade_settings.dart'; import 'package:selfprivacy/logic/models/job.dart'; -import 'package:selfprivacy/ui/components/brand_button/segmented_buttons.dart'; -import 'package:selfprivacy/ui/components/brand_cards/filled_card.dart'; -import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; +import 'package:selfprivacy/ui/components/buttons/segmented_buttons.dart'; +import 'package:selfprivacy/ui/components/cards/filled_card.dart'; +import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart'; import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart'; import 'package:selfprivacy/ui/components/brand_loader/brand_loader.dart'; -import 'package:selfprivacy/ui/components/brand_text/brand_text.dart'; import 'package:selfprivacy/ui/components/list_tiles/list_tile_on_surface_variant.dart'; import 'package:selfprivacy/ui/pages/server_details/charts/cpu_chart.dart'; import 'package:selfprivacy/ui/pages/server_details/charts/network_charts.dart'; import 'package:selfprivacy/ui/pages/server_storage/storage_card.dart'; +import 'package:selfprivacy/utils/breakpoints.dart'; import 'package:selfprivacy/utils/extensions/duration.dart'; -import 'package:selfprivacy/utils/named_font_weight.dart'; import 'package:selfprivacy/utils/route_transitions/basic.dart'; import 'package:timezone/timezone.dart'; @@ -32,6 +31,7 @@ part 'time_zone/time_zone.dart'; var navigatorKey = GlobalKey(); +@RoutePage() class ServerDetailsScreen extends StatefulWidget { const ServerDetailsScreen({super.key}); @@ -75,6 +75,7 @@ class _ServerDetailsScreenState extends State return BlocProvider( create: (final context) => context.read()..check(), child: BrandHeroScreen( + hasFlashButton: true, heroIcon: BrandIcons.server, heroTitle: 'server.card_title'.tr(), heroSubtitle: 'server.description'.tr(), diff --git a/lib/ui/pages/server_details/text_details.dart b/lib/ui/pages/server_details/text_details.dart index 5f447901..03126ba5 100644 --- a/lib/ui/pages/server_details/text_details.dart +++ b/lib/ui/pages/server_details/text_details.dart @@ -23,15 +23,13 @@ class _TextDetails extends StatelessWidget { ), ), ), - ...details.metadata - .map( - (final metadata) => ListTileOnSurfaceVariant( - leadingIcon: metadata.type.icon, - title: metadata.name, - subtitle: metadata.value, - ), - ) - .toList(), + ...details.metadata.map( + (final metadata) => ListTileOnSurfaceVariant( + leadingIcon: metadata.type.icon, + title: metadata.name, + subtitle: metadata.value, + ), + ), ], ), ); @@ -39,24 +37,6 @@ class _TextDetails extends StatelessWidget { throw Exception('wrong state'); } } - - Widget getRowTitle(final String title) => Padding( - padding: const EdgeInsets.only(right: 10), - child: BrandText.h5( - title, - textAlign: TextAlign.right, - ), - ); - - Widget getRowValue(final String title, {final bool isBold = false}) => - BrandText.body1( - title, - style: isBold - ? const TextStyle( - fontWeight: NamedFontWeight.demiBold, - ) - : null, - ); } class _TempMessage extends StatelessWidget { @@ -69,7 +49,10 @@ class _TempMessage extends StatelessWidget { Widget build(final BuildContext context) => SizedBox( height: MediaQuery.of(context).size.height - 100, child: Center( - child: BrandText.body2(message), + child: Text( + message, + style: Theme.of(context).textTheme.bodyMedium, + ), ), ); } diff --git a/lib/ui/pages/server_details/time_zone/time_zone.dart b/lib/ui/pages/server_details/time_zone/time_zone.dart index a93b8f23..6863bfe8 100644 --- a/lib/ui/pages/server_details/time_zone/time_zone.dart +++ b/lib/ui/pages/server_details/time_zone/time_zone.dart @@ -57,66 +57,72 @@ class _SelectTimezoneState extends State { } @override - Widget build(final BuildContext context) => Scaffold( - appBar: AppBar( - title: isSearching - ? TextField( - readOnly: false, - textAlign: TextAlign.start, - textInputAction: TextInputAction.next, - enabled: true, - controller: searchController, - decoration: InputDecoration( - errorText: null, - hintText: 'server.timezone_search_bar'.tr(), - ), - ) - : Padding( - padding: const EdgeInsets.only(top: 4.0), - child: Text('server.select_timezone'.tr()), + Widget build(final BuildContext context) { + final isDesktop = Breakpoints.mediumAndUp.isActive(context); + return Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + title: (isDesktop || isSearching) + ? TextField( + readOnly: false, + textAlign: TextAlign.start, + textInputAction: TextInputAction.next, + enabled: true, + controller: searchController, + decoration: InputDecoration( + errorText: null, + hintText: 'server.timezone_search_bar'.tr(), ), - leading: IconButton( - icon: const Icon(Icons.arrow_back), - onPressed: isSearching - ? () => setState(() => isSearching = false) - : () => Navigator.of(context).pop(), - ), - actions: [ - if (!isSearching) - IconButton( - icon: const Icon(Icons.search), - onPressed: () => setState(() => isSearching = true), + ) + : Padding( + padding: const EdgeInsets.only(top: 4.0), + child: Text('server.select_timezone'.tr()), ), - ], + leading: !isDesktop + ? IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: isSearching + ? () => setState(() => isSearching = false) + : () => Navigator.of(context).pop(), + ) + : null, + actions: [ + if (!isSearching && !isDesktop) + IconButton( + icon: const Icon(Icons.search), + onPressed: () => setState(() => isSearching = true), + ), + ], + ), + body: SafeArea( + child: ListView( + controller: scrollController, + children: locations + .where( + (final Location location) => timezoneFilterValue == null + ? true + : location.name + .toLowerCase() + .contains(timezoneFilterValue!) || + Duration( + milliseconds: location.currentTimeZone.offset, + ) + .toDayHourMinuteFormat() + .contains(timezoneFilterValue!), + ) + .toList() + .asMap() + .map( + (final key, final value) => locationToListTile(key, value), + ) + .values + .toList(), ), - body: SafeArea( - child: ListView( - controller: scrollController, - children: locations - .where( - (final Location location) => timezoneFilterValue == null - ? true - : location.name - .toLowerCase() - .contains(timezoneFilterValue!) || - Duration( - milliseconds: location.currentTimeZone.offset, - ) - .toDayHourMinuteFormat() - .contains(timezoneFilterValue!), - ) - .toList() - .asMap() - .map( - (final key, final value) => locationToListTile(key, value), - ) - .values - .toList(), - ), - ), - ); + ), + ); + } - MapEntry locationToListTile( + MapEntry locationToListTile( final int key, final Location location, ) { @@ -126,46 +132,19 @@ class _SelectTimezoneState extends State { return MapEntry( key, - Container( - height: 75, - padding: const EdgeInsets.symmetric(horizontal: 20), - decoration: const BoxDecoration( - border: Border( - bottom: BorderSide( - color: BrandColors.dividerColor, - ), - ), + ListTile( + title: Text( + location.name, ), - child: InkWell( - onTap: () { - context.read().repository.setTimezone( - location.name, - ); - Navigator.of(context).pop(); - }, - child: Container( - padding: const EdgeInsets.symmetric(vertical: 16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - BrandText.body1( - location.name, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - BrandText.small( - 'GMT ${duration.toDayHourMinuteFormat()} ${area.isNotEmpty ? '($area)' : ''}', - style: const TextStyle( - fontSize: 13, - ), - ), - ], - ), - ), + subtitle: Text( + 'GMT ${duration.toDayHourMinuteFormat()} ${area.isNotEmpty ? '($area)' : ''}', ), + onTap: () { + context.read().repository.setTimezone( + location.name, + ); + Navigator.of(context).pop(); + }, ), ); } diff --git a/lib/ui/pages/server_storage/binds_migration/migration_process_page.dart b/lib/ui/pages/server_storage/binds_migration/migration_process_page.dart index 2f896673..8d42cb6f 100644 --- a/lib/ui/pages/server_storage/binds_migration/migration_process_page.dart +++ b/lib/ui/pages/server_storage/binds_migration/migration_process_page.dart @@ -2,8 +2,8 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:selfprivacy/logic/cubit/server_jobs/server_jobs_cubit.dart'; import 'package:selfprivacy/logic/models/json/server_job.dart'; -import 'package:selfprivacy/ui/components/brand_button/brand_button.dart'; -import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; +import 'package:selfprivacy/ui/components/buttons/brand_button.dart'; +import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart'; import 'package:selfprivacy/ui/components/brand_linear_indicator/brand_linear_indicator.dart'; import 'package:selfprivacy/ui/pages/root_route.dart'; import 'package:selfprivacy/utils/route_transitions/basic.dart'; diff --git a/lib/ui/pages/server_storage/binds_migration/services_migration.dart b/lib/ui/pages/server_storage/binds_migration/services_migration.dart index bb95ae94..cdb3339b 100644 --- a/lib/ui/pages/server_storage/binds_migration/services_migration.dart +++ b/lib/ui/pages/server_storage/binds_migration/services_migration.dart @@ -1,21 +1,19 @@ +import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:selfprivacy/logic/cubit/server_jobs/server_jobs_cubit.dart'; import 'package:selfprivacy/logic/cubit/services/services_cubit.dart'; import 'package:selfprivacy/logic/models/disk_size.dart'; import 'package:selfprivacy/logic/models/service.dart'; -import 'package:selfprivacy/ui/components/brand_bottom_sheet/brand_bottom_sheet.dart'; -import 'package:selfprivacy/ui/components/brand_button/brand_button.dart'; +import 'package:selfprivacy/ui/components/buttons/brand_button.dart'; import 'package:selfprivacy/ui/components/brand_header/brand_header.dart'; import 'package:selfprivacy/ui/components/info_box/info_box.dart'; import 'package:selfprivacy/logic/models/disk_status.dart'; import 'package:selfprivacy/ui/components/jobs_content/jobs_content.dart'; import 'package:selfprivacy/ui/components/storage_list_items/server_storage_list_item.dart'; import 'package:selfprivacy/ui/components/storage_list_items/service_migration_list_item.dart'; -import 'package:selfprivacy/ui/helpers/modals.dart'; -import 'package:selfprivacy/ui/pages/root_route.dart'; -import 'package:selfprivacy/utils/route_transitions/basic.dart'; +@RoutePage() class ServicesMigrationPage extends StatefulWidget { const ServicesMigrationPage({ required this.services, @@ -110,22 +108,20 @@ class _ServicesMigrationPageState extends State { ), child: Column( children: [ - ...widget.diskStatus.diskVolumes - .map( - (final volume) => Column( - children: [ - ServerStorageListItem( - volume: recalculatedDiskUsages( - volume, - widget.services, - ), - dense: true, - ), - const SizedBox(height: headerVerticalPadding), - ], + ...widget.diskStatus.diskVolumes.map( + (final volume) => Column( + children: [ + ServerStorageListItem( + volume: recalculatedDiskUsages( + volume, + widget.services, + ), + dense: true, ), - ) - .toList(), + const SizedBox(height: headerVerticalPadding), + ], + ), + ), ], ), ), @@ -138,23 +134,21 @@ class _ServicesMigrationPageState extends State { children: [ if (widget.services.isEmpty) const Center(child: CircularProgressIndicator()), - ...widget.services - .map( - (final service) => Column( - children: [ - const SizedBox(height: 8), - ServiceMigrationListItem( - service: service, - diskStatus: widget.diskStatus, - selectedVolume: serviceToDisk[service.id]!, - onChange: onChange, - ), - const SizedBox(height: 4), - const Divider(), - ], + ...widget.services.map( + (final service) => Column( + children: [ + const SizedBox(height: 8), + ServiceMigrationListItem( + service: service, + diskStatus: widget.diskStatus, + selectedVolume: serviceToDisk[service.id]!, + onChange: onChange, ), - ) - .toList(), + const SizedBox(height: 4), + const Divider(), + ], + ), + ), Padding( padding: const EdgeInsets.all(8.0), child: InfoBox( @@ -180,17 +174,10 @@ class _ServicesMigrationPageState extends State { } } } - Navigator.of(context).pushAndRemoveUntil( - materialRoute(const RootPage()), - (final predicate) => false, - ); - showBrandBottomSheet( + context.router.popUntilRoot(); + showModalBottomSheet( context: context, - builder: (final BuildContext context) => - const BrandBottomSheet( - isExpended: true, - child: JobsContent(), - ), + builder: (final BuildContext context) => const JobsContent(), ); }, ), diff --git a/lib/ui/pages/server_storage/extending_volume.dart b/lib/ui/pages/server_storage/extending_volume.dart index d40c628c..41c81140 100644 --- a/lib/ui/pages/server_storage/extending_volume.dart +++ b/lib/ui/pages/server_storage/extending_volume.dart @@ -1,3 +1,4 @@ +import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart'; @@ -5,12 +6,11 @@ import 'package:selfprivacy/logic/cubit/provider_volumes/provider_volume_cubit.d import 'package:selfprivacy/logic/cubit/server_volumes/server_volume_cubit.dart'; import 'package:selfprivacy/logic/models/disk_size.dart'; import 'package:selfprivacy/logic/models/price.dart'; -import 'package:selfprivacy/ui/components/brand_button/brand_button.dart'; -import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; +import 'package:selfprivacy/ui/components/buttons/brand_button.dart'; +import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart'; import 'package:selfprivacy/logic/models/disk_status.dart'; -import 'package:selfprivacy/ui/pages/root_route.dart'; -import 'package:selfprivacy/utils/route_transitions/basic.dart'; +@RoutePage() class ExtendingVolumePage extends StatefulWidget { const ExtendingVolumePage({ required this.diskVolumeToResize, @@ -155,10 +155,7 @@ class _ExtendingVolumePageState extends State { DiskSize.fromGibibyte(_currentSliderGbValue), context.read().reload, ); - Navigator.of(context).pushAndRemoveUntil( - materialRoute(const RootPage()), - (final predicate) => false, - ); + context.router.popUntilRoot(); }, child: Text('storage.extend_volume_button.title'.tr()), ), diff --git a/lib/ui/pages/server_storage/server_storage.dart b/lib/ui/pages/server_storage/server_storage.dart index 2a5206e2..eb7a586b 100644 --- a/lib/ui/pages/server_storage/server_storage.dart +++ b/lib/ui/pages/server_storage/server_storage.dart @@ -1,16 +1,17 @@ +import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; 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_button/outlined_button.dart'; -import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; +import 'package:selfprivacy/ui/components/buttons/outlined_button.dart'; +import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart'; import 'package:selfprivacy/logic/models/disk_status.dart'; -import 'package:selfprivacy/ui/pages/server_storage/extending_volume.dart'; import 'package:selfprivacy/ui/components/storage_list_items/server_storage_list_item.dart'; -import 'package:selfprivacy/utils/route_transitions/basic.dart'; +import 'package:selfprivacy/ui/router/router.dart'; +@RoutePage() class ServerStoragePage extends StatefulWidget { const ServerStoragePage({ required this.diskStatus, @@ -45,28 +46,26 @@ class _ServerStoragePageState extends State { heroTitle: 'storage.card_title'.tr(), children: [ // ...sections, - ...widget.diskStatus.diskVolumes - .map( - (final volume) => Column( - mainAxisSize: MainAxisSize.min, - children: [ - ServerStorageSection( - volume: volume, - diskStatus: widget.diskStatus, - services: services - .where( - (final service) => - service.storageUsage.volume == volume.name, - ) - .toList(), - ), - const SizedBox(height: 16), - const Divider(), - const SizedBox(height: 16), - ], + ...widget.diskStatus.diskVolumes.map( + (final volume) => Column( + mainAxisSize: MainAxisSize.min, + children: [ + ServerStorageSection( + volume: volume, + diskStatus: widget.diskStatus, + services: services + .where( + (final service) => + service.storageUsage.volume == volume.name, + ) + .toList(), ), - ) - .toList(), + const SizedBox(height: 16), + const Divider(), + const SizedBox(height: 16), + ], + ), + ), const SizedBox(height: 8), ], ); @@ -93,24 +92,20 @@ class ServerStorageSection extends StatelessWidget { volume: volume, ), const SizedBox(height: 16), - ...services - .map( - (final service) => ServerConsumptionListTile( - service: service, - volume: volume, - ), - ) - .toList(), + ...services.map( + (final service) => ServerConsumptionListTile( + service: service, + volume: volume, + ), + ), if (volume.isResizable) ...[ const SizedBox(height: 16), BrandOutlinedButton( title: 'storage.extend_volume_button.title'.tr(), - onPressed: () => Navigator.of(context).push( - materialRoute( - ExtendingVolumePage( - diskVolumeToResize: volume, - diskStatus: diskStatus, - ), + onPressed: () => context.pushRoute( + ExtendingVolumeRoute( + diskVolumeToResize: volume, + diskStatus: diskStatus, ), ), ), @@ -138,7 +133,10 @@ class ServerConsumptionListTile extends StatelessWidget { service.svgIcon, width: 24.0, height: 24.0, - color: Theme.of(context).colorScheme.onBackground, + colorFilter: ColorFilter.mode( + Theme.of(context).colorScheme.onBackground, + BlendMode.srcIn, + ), ), rightSideText: service.storageUsage.used.toString(), percentage: service.storageUsage.used.byte / volume.sizeTotal.byte, diff --git a/lib/ui/pages/server_storage/storage_card.dart b/lib/ui/pages/server_storage/storage_card.dart index ae0c2369..ac633463 100644 --- a/lib/ui/pages/server_storage/storage_card.dart +++ b/lib/ui/pages/server_storage/storage_card.dart @@ -1,12 +1,12 @@ +import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart'; import 'package:selfprivacy/logic/cubit/providers/providers_cubit.dart'; import 'package:selfprivacy/ui/components/icon_status_mask/icon_status_mask.dart'; import 'package:selfprivacy/logic/models/disk_status.dart'; -import 'package:selfprivacy/ui/pages/server_storage/server_storage.dart'; import 'package:selfprivacy/ui/components/storage_list_items/server_storage_list_item.dart'; -import 'package:selfprivacy/utils/route_transitions/basic.dart'; +import 'package:selfprivacy/ui/router/router.dart'; class StorageCard extends StatelessWidget { const StorageCard({ @@ -45,13 +45,8 @@ class StorageCard extends StatelessWidget { clipBehavior: Clip.antiAlias, child: InkResponse( highlightShape: BoxShape.rectangle, - onTap: () => Navigator.of(context).push( - materialRoute( - ServerStoragePage( - diskStatus: diskStatus, - ), - ), - ), + onTap: () => + context.pushRoute(ServerStorageRoute(diskStatus: diskStatus)), child: Padding( padding: const EdgeInsets.all(16.0), child: Column( diff --git a/lib/ui/pages/services/service_page.dart b/lib/ui/pages/services/service_page.dart index 22db2bf6..0c7beafb 100644 --- a/lib/ui/pages/services/service_page.dart +++ b/lib/ui/pages/services/service_page.dart @@ -1,3 +1,4 @@ +import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; @@ -6,12 +7,12 @@ import 'package:selfprivacy/logic/cubit/server_volumes/server_volume_cubit.dart' import 'package:selfprivacy/logic/cubit/services/services_cubit.dart'; import 'package:selfprivacy/logic/models/job.dart'; import 'package:selfprivacy/logic/models/service.dart'; -import 'package:selfprivacy/ui/components/brand_cards/filled_card.dart'; -import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; -import 'package:selfprivacy/ui/pages/server_storage/binds_migration/services_migration.dart'; +import 'package:selfprivacy/ui/components/cards/filled_card.dart'; +import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart'; +import 'package:selfprivacy/ui/router/router.dart'; import 'package:selfprivacy/utils/launch_url.dart'; -import 'package:selfprivacy/utils/route_transitions/basic.dart'; +@RoutePage() class ServicePage extends StatefulWidget { const ServicePage({required this.serviceId, super.key}); @@ -46,11 +47,15 @@ class _ServicePageState extends State { return BrandHeroScreen( hasBackButton: true, + hasFlashButton: true, heroIconWidget: SvgPicture.string( service.svgIcon, width: 48.0, height: 48.0, - color: Theme.of(context).colorScheme.onBackground, + colorFilter: ColorFilter.mode( + Theme.of(context).colorScheme.onBackground, + BlendMode.srcIn, + ), ), heroTitle: service.displayName, children: [ @@ -108,14 +113,12 @@ class _ServicePageState extends State { ListTile( iconColor: Theme.of(context).colorScheme.onBackground, // Open page ServicesMigrationPage - onTap: () => Navigator.of(context).push( - materialRoute( - ServicesMigrationPage( - services: [service], - diskStatus: - context.read().state.diskStatus, - isMigration: false, - ), + onTap: () => context.pushRoute( + ServicesMigrationRoute( + services: [service], + diskStatus: + context.read().state.diskStatus, + isMigration: false, ), ), leading: const Icon(Icons.drive_file_move_outlined), diff --git a/lib/ui/pages/services/services.dart b/lib/ui/pages/services/services.dart index 54192367..99bb848e 100644 --- a/lib/ui/pages/services/services.dart +++ b/lib/ui/pages/services/services.dart @@ -1,3 +1,4 @@ +import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:selfprivacy/config/brand_theme.dart'; @@ -5,17 +6,16 @@ import 'package:selfprivacy/logic/cubit/server_installation/server_installation_ import 'package:selfprivacy/logic/cubit/services/services_cubit.dart'; import 'package:selfprivacy/logic/models/service.dart'; import 'package:selfprivacy/logic/models/state_types.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_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/ui/router/router.dart'; +import 'package:selfprivacy/utils/breakpoints.dart'; import 'package:selfprivacy/utils/launch_url.dart'; -import 'package:selfprivacy/utils/route_transitions/basic.dart'; import 'package:selfprivacy/utils/ui_helpers.dart'; +@RoutePage() class ServicesPage extends StatefulWidget { const ServicesPage({super.key}); @@ -34,32 +34,35 @@ class _ServicesPageState extends State { .sort((final a, final b) => a.status.index.compareTo(b.status.index)); return Scaffold( - appBar: PreferredSize( - preferredSize: const Size.fromHeight(52), - child: BrandHeader( - title: 'basis.services'.tr(), - ), - ), + appBar: Breakpoints.small.isActive(context) + ? PreferredSize( + preferredSize: const Size.fromHeight(52), + child: BrandHeader( + title: 'basis.services'.tr(), + ), + ) + : null, body: RefreshIndicator( onRefresh: () async { - context.read().reload(); + await context.read().reload(); }, child: ListView( padding: paddingH15V0, children: [ - BrandText.body1('basis.services_title'.tr()), + Text( + 'basis.services_title'.tr(), + style: Theme.of(context).textTheme.bodyLarge, + ), const SizedBox(height: 24), if (!isReady) ...[const NotReadyCard(), const SizedBox(height: 24)], - ...services - .map( - (final service) => Padding( - padding: const EdgeInsets.only( - bottom: 30, - ), - child: _Card(service: service), - ), - ) - .toList() + ...services.map( + (final service) => Padding( + padding: const EdgeInsets.only( + bottom: 30, + ), + child: _Card(service: service), + ), + ) ], ), ), @@ -98,81 +101,106 @@ class _Card extends StatelessWidget { } } - return GestureDetector( - onTap: isReady - ? () => Navigator.of(context) - .push(materialRoute(ServicePage(serviceId: service.id))) - : null, - child: BrandCards.big( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - IconStatusMask( - status: getStatus(service.status), - icon: SvgPicture.string( - service.svgIcon, - width: 30.0, - height: 30.0, - color: Theme.of(context).colorScheme.onBackground, - ), - ), - ], - ), - ClipRect( - child: Stack( + return Card( + clipBehavior: Clip.antiAlias, + child: InkResponse( + highlightShape: BoxShape.rectangle, + onTap: isReady + ? () => context.pushRoute( + ServiceRoute(serviceId: service.id), + ) + : null, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 10), - BrandText.h2(service.displayName), - const SizedBox(height: 10), - if (service.url != '' && service.url != null) - Column( - children: [ - GestureDetector( - onTap: () => launchURL( - service.url, - ), - child: Text( - '${service.url}', - style: TextStyle( - color: - Theme.of(context).colorScheme.secondary, - decoration: TextDecoration.underline, - ), - ), - ), - const SizedBox(height: 10), - ], - ), - if (service.id == 'mailserver') - Column( - children: [ - Text( - domainName, - style: TextStyle( - color: Theme.of(context).colorScheme.primary, - decoration: TextDecoration.underline, - ), - ), - const SizedBox(height: 10), - ], - ), - BrandText.body2(service.loginInfo), - const SizedBox(height: 10), - BrandText.body2(service.description), - const SizedBox(height: 10), - ], + IconStatusMask( + status: getStatus(service.status), + icon: SvgPicture.string( + service.svgIcon, + width: 30.0, + height: 30.0, + colorFilter: const ColorFilter.mode( + Colors.white, + BlendMode.srcIn, + ), + ), ), ], ), - ) - ], + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 12), + Text( + service.displayName, + style: Theme.of(context).textTheme.headlineMedium, + ), + const SizedBox(height: 8), + if (service.url != '' && service.url != null) + Column( + children: [ + _ServiceLink( + url: service.url ?? '', + ), + const SizedBox(height: 10), + ], + ), + if (service.id == 'mailserver') + Column( + children: [ + _ServiceLink( + url: domainName, + isActive: false, + ), + const SizedBox(height: 10), + ], + ), + Text( + service.loginInfo, + style: Theme.of(context).textTheme.bodyMedium, + ), + const SizedBox(height: 10), + Text( + service.description, + style: Theme.of(context).textTheme.bodyMedium, + ), + const SizedBox(height: 10), + ], + ) + ], + ), ), ), ); } } + +class _ServiceLink extends StatelessWidget { + const _ServiceLink({ + required this.url, + this.isActive = true, + }); + + final String url; + final bool isActive; + + @override + Widget build(final BuildContext context) => GestureDetector( + onTap: isActive + ? () => launchURL( + url, + ) + : null, + child: Text( + url, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.primary, + decoration: TextDecoration.underline, + ), + ), + ); +} diff --git a/lib/ui/pages/setup/initializing/dns_provider_picker.dart b/lib/ui/pages/setup/initializing/dns_provider_picker.dart index d60c1b20..20f166cb 100644 --- a/lib/ui/pages/setup/initializing/dns_provider_picker.dart +++ b/lib/ui/pages/setup/initializing/dns_provider_picker.dart @@ -6,11 +6,10 @@ import 'package:selfprivacy/config/brand_theme.dart'; import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart'; import 'package:selfprivacy/logic/cubit/forms/setup/initializing/dns_provider_form_cubit.dart'; import 'package:selfprivacy/logic/models/hive/server_domain.dart'; -import 'package:selfprivacy/ui/components/brand_button/brand_button.dart'; -import 'package:selfprivacy/ui/components/brand_bottom_sheet/brand_bottom_sheet.dart'; -import 'package:selfprivacy/ui/components/brand_button/outlined_button.dart'; -import 'package:selfprivacy/ui/components/brand_cards/outlined_card.dart'; import 'package:selfprivacy/ui/components/brand_md/brand_md.dart'; +import 'package:selfprivacy/ui/components/buttons/brand_button.dart'; +import 'package:selfprivacy/ui/components/buttons/outlined_button.dart'; +import 'package:selfprivacy/ui/components/cards/outlined_card.dart'; import 'package:selfprivacy/utils/network_utils.dart'; class DnsProviderPicker extends StatefulWidget { @@ -130,18 +129,15 @@ class ProviderInputDataPage extends StatelessWidget { context: context, isScrollControlled: true, backgroundColor: Colors.transparent, - builder: (final BuildContext context) => BrandBottomSheet( - isExpended: true, - child: Padding( - padding: paddingH15V0, - child: ListView( - padding: const EdgeInsets.symmetric(vertical: 16), - children: [ - BrandMarkdown( - fileName: providerInfo.pathToHow, - ), - ], - ), + builder: (final BuildContext context) => Padding( + padding: paddingH15V0, + child: ListView( + padding: const EdgeInsets.symmetric(vertical: 16), + children: [ + BrandMarkdown( + fileName: providerInfo.pathToHow, + ), + ], ), ), ), diff --git a/lib/ui/pages/setup/initializing/initializing.dart b/lib/ui/pages/setup/initializing/initializing.dart index 2b5a906c..e3868f1e 100644 --- a/lib/ui/pages/setup/initializing/initializing.dart +++ b/lib/ui/pages/setup/initializing/initializing.dart @@ -1,7 +1,7 @@ +import 'package:auto_route/auto_route.dart'; import 'package:cubit_form/cubit_form.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:selfprivacy/config/brand_theme.dart'; import 'package:selfprivacy/logic/cubit/forms/setup/initializing/server_provider_form_cubit.dart'; import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; import 'package:selfprivacy/logic/cubit/forms/factories/field_cubit_factory.dart'; @@ -9,19 +9,21 @@ import 'package:selfprivacy/logic/cubit/forms/setup/initializing/backblaze_form_ import 'package:selfprivacy/logic/cubit/forms/setup/initializing/dns_provider_form_cubit.dart'; import 'package:selfprivacy/logic/cubit/forms/setup/initializing/domain_setup_cubit.dart'; import 'package:selfprivacy/logic/cubit/forms/setup/initializing/root_user_form_cubit.dart'; -import 'package:selfprivacy/ui/components/brand_bottom_sheet/brand_bottom_sheet.dart'; -import 'package:selfprivacy/ui/components/brand_button/brand_button.dart'; -import 'package:selfprivacy/ui/components/brand_md/brand_md.dart'; -import 'package:selfprivacy/ui/components/brand_text/brand_text.dart'; +import 'package:selfprivacy/logic/cubit/support_system/support_system_cubit.dart'; +import 'package:selfprivacy/ui/components/buttons/brand_button.dart'; import 'package:selfprivacy/ui/components/brand_timer/brand_timer.dart'; +import 'package:selfprivacy/ui/components/drawers/progress_drawer.dart'; import 'package:selfprivacy/ui/components/progress_bar/progress_bar.dart'; -import 'package:selfprivacy/ui/pages/root_route.dart'; +import 'package:selfprivacy/ui/components/drawers/support_drawer.dart'; +import 'package:selfprivacy/ui/layouts/responsive_layout_with_infobox.dart'; import 'package:selfprivacy/ui/pages/setup/initializing/dns_provider_picker.dart'; import 'package:selfprivacy/ui/pages/setup/initializing/server_provider_picker.dart'; import 'package:selfprivacy/ui/pages/setup/initializing/server_type_picker.dart'; import 'package:selfprivacy/ui/pages/setup/recovering/recovery_routing.dart'; -import 'package:selfprivacy/utils/route_transitions/basic.dart'; +import 'package:selfprivacy/ui/router/router.dart'; +import 'package:selfprivacy/utils/breakpoints.dart'; +@RoutePage() class InitializingPage extends StatelessWidget { const InitializingPage({super.key}); @@ -49,99 +51,155 @@ class InitializingPage extends StatelessWidget { ][cubit.state.progress.index](); } + const steps = [ + 'initializing.steps.hosting', + 'initializing.steps.server_type', + 'initializing.steps.dns_provider', + 'initializing.steps.backups_provider', + 'initializing.steps.domain', + 'initializing.steps.master_account', + 'initializing.steps.server', + 'initializing.steps.dns_setup', + 'initializing.steps.nixos_installation', + 'initializing.steps.server_reboot', + 'initializing.steps.final_checks', + ]; + return BlocListener( listener: (final context, final state) { if (cubit.state is ServerInstallationFinished) { - Navigator.of(context) - .pushReplacement(materialRoute(const RootPage())); + context.router.popUntilRoot(); } }, child: Scaffold( - appBar: AppBar( - actions: [ - if (cubit.state is ServerInstallationFinished) - IconButton( - icon: const Icon(Icons.check), - onPressed: () { - Navigator.of(context) - .pushReplacement(materialRoute(const RootPage())); - }, - ) - ], - title: Text( - 'more_page.configuration_wizard'.tr(), - ), - bottom: PreferredSize( - preferredSize: const Size.fromHeight(28), - child: Padding( - padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), - child: ProgressBar( - steps: const [ - 'Hosting', - 'Server Type', - 'CloudFlare', - 'Backblaze', - 'Domain', - 'User', - 'Server', - 'Installation', - ], - activeIndex: cubit.state.porgressBar, - ), - ), - ), - ), - body: SingleChildScrollView( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(16.0, 0, 16.0, 0.0), - child: AnimatedSwitcher( - duration: const Duration(milliseconds: 300), - child: actualInitializingPage, - ), - ), - ConstrainedBox( - constraints: BoxConstraints( - minHeight: MediaQuery.of(context).size.height - - MediaQuery.of(context).padding.top - - MediaQuery.of(context).padding.bottom - - 566, - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - alignment: Alignment.center, - child: BrandButton.text( - title: cubit.state is ServerInstallationFinished - ? 'basis.close'.tr() - : 'basis.later'.tr(), - onPressed: () { - Navigator.of(context).pushAndRemoveUntil( - materialRoute(const RootPage()), - (final predicate) => false, - ); - }, - ), + endDrawer: const SupportDrawer(), + endDrawerEnableOpenDragGesture: false, + appBar: Breakpoints.large.isActive(context) + ? null + : AppBar( + actions: [ + if (cubit.state is ServerInstallationFinished) + IconButton( + icon: const Icon(Icons.check), + onPressed: () { + context.router.popUntilRoot(); + }, ), - if (cubit.state is ServerInstallationEmpty || - cubit.state is ServerInstallationNotFinished) - Container( - alignment: Alignment.center, - child: BrandButton.text( - title: 'basis.connect_to_existing'.tr(), + const SizedBox.shrink(), + ], + title: Text( + 'more_page.configuration_wizard'.tr(), + ), + bottom: PreferredSize( + preferredSize: const Size.fromHeight(28), + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), + child: ProgressBar( + steps: const [ + 'Hosting', + 'Server Type', + 'CloudFlare', + 'Backblaze', + 'Domain', + 'User', + 'Server', + 'Installation', + ], + activeIndex: cubit.state.porgressBar, + ), + ), + ), + ), + body: LayoutBuilder( + builder: (final context, final constraints) => Row( + children: [ + if (Breakpoints.large.isActive(context)) + ProgressDrawer( + steps: steps, + currentStep: cubit.state.progress.index, + title: 'more_page.configuration_wizard'.tr(), + constraints: constraints, + trailing: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (cubit.state is ServerInstallationEmpty || + cubit.state is ServerInstallationNotFinished) + Container( + alignment: Alignment.center, + child: BrandButton.filled( + text: 'basis.connect_to_existing'.tr(), + onPressed: () { + context.router.replace(const RecoveryRoute()); + }, + ), + ), + ConstrainedBox( + constraints: const BoxConstraints( + minWidth: double.infinity, + ), + child: OutlinedButton( + child: Text( + cubit.state is ServerInstallationFinished + ? 'basis.close'.tr() + : 'basis.later'.tr(), + ), onPressed: () { - Navigator.of(context).push( - materialRoute( - const RecoveryRouting(), - ), - ); + context.router.popUntilRoot(); }, ), - ) - ], + ), + ], + ), + ), + SizedBox( + width: constraints.maxWidth - + (Breakpoints.large.isActive(context) ? 300 : 0), + height: constraints.maxHeight, + child: SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: Breakpoints.large.isActive(context) + ? const EdgeInsets.all(16.0) + : const EdgeInsets.fromLTRB(16.0, 0, 16.0, 0.0), + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: actualInitializingPage, + ), + ), + if (!Breakpoints.large.isActive(context)) + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + alignment: Alignment.center, + child: BrandButton.text( + title: + cubit.state is ServerInstallationFinished + ? 'basis.close'.tr() + : 'basis.later'.tr(), + onPressed: () { + context.router.popUntilRoot(); + }, + ), + ), + if (cubit.state is ServerInstallationEmpty || + cubit.state is ServerInstallationNotFinished) + Container( + alignment: Alignment.center, + child: BrandButton.text( + title: 'basis.connect_to_existing'.tr(), + onPressed: () { + context.router + .replace(const RecoveryRoute()); + }, + ), + ) + ], + ), + ], + ), ), ), ], @@ -182,15 +240,6 @@ class InitializingPage extends StatelessWidget { ), ); - void _showModal(final BuildContext context, final Widget widget) { - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (final BuildContext context) => widget, - ); - } - Widget _stepDnsProviderToken( final ServerInstallationCubit initializingCubit, ) => @@ -213,50 +262,57 @@ class InitializingPage extends StatelessWidget { child: Builder( builder: (final context) { final formCubitState = context.watch().state; - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '${'initializing.connect_to_server_provider'.tr()}Backblaze', - style: Theme.of(context).textTheme.headlineSmall, - ), - const SizedBox(height: 32), - CubitFormTextField( - formFieldCubit: context.read().keyId, - textAlign: TextAlign.center, - scrollPadding: const EdgeInsets.only(bottom: 70), - decoration: const InputDecoration( - hintText: 'KeyID', + return ResponsiveLayoutWithInfobox( + topChild: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '${'initializing.connect_to_server_provider'.tr()}Backblaze', + style: Theme.of(context).textTheme.headlineSmall, ), - ), - const SizedBox(height: 16), - CubitFormTextField( - formFieldCubit: - context.read().applicationKey, - textAlign: TextAlign.center, - scrollPadding: const EdgeInsets.only(bottom: 70), - decoration: const InputDecoration( - hintText: 'Master Application Key', - ), - ), - const SizedBox(height: 32), - BrandButton.rised( - onPressed: formCubitState.isSubmitting - ? null - : () => context.read().trySubmit(), - text: 'basis.connect'.tr(), - ), - const SizedBox(height: 10), - BrandButton.text( - onPressed: () => _showModal( - context, - const _HowTo( - fileName: 'how_backblaze', + ], + ), + primaryColumn: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CubitFormTextField( + formFieldCubit: context.read().keyId, + textAlign: TextAlign.center, + scrollPadding: const EdgeInsets.only(bottom: 70), + decoration: const InputDecoration( + hintText: 'KeyID', ), ), - title: 'initializing.how'.tr(), - ), - ], + const SizedBox(height: 16), + CubitFormTextField( + formFieldCubit: + context.read().applicationKey, + textAlign: TextAlign.center, + scrollPadding: const EdgeInsets.only(bottom: 70), + decoration: const InputDecoration( + hintText: 'Master Application Key', + ), + ), + const SizedBox(height: 32), + BrandButton.rised( + onPressed: formCubitState.isSubmitting + ? null + : () => context.read().trySubmit(), + text: 'basis.connect'.tr(), + ), + const SizedBox(height: 10), + BrandButton.text( + onPressed: () { + context.read().showArticle( + article: 'how_backblaze', + context: context, + ); + Scaffold.of(context).openEndDrawer(); + }, + title: 'initializing.how'.tr(), + ), + ], + ), ); }, ), @@ -269,9 +325,8 @@ class InitializingPage extends StatelessWidget { builder: (final context) { final DomainSetupState state = context.watch().state; - return SizedBox( - width: double.infinity, - child: Column( + return ResponsiveLayoutWithInfobox( + topChild: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( @@ -283,7 +338,11 @@ class InitializingPage extends StatelessWidget { 'initializing.use_this_domain_text'.tr(), style: Theme.of(context).textTheme.bodyMedium, ), - const SizedBox(height: 32), + ], + ), + primaryColumn: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ if (state is Empty) Text( 'initializing.no_connected_domains'.tr(), @@ -323,7 +382,7 @@ class InitializingPage extends StatelessWidget { ], if (state is Empty) ...[ const SizedBox(height: 30), - BrandButton.rised( + BrandButton.filled( onPressed: () => context.read().load(), child: Row( mainAxisAlignment: MainAxisAlignment.center, @@ -333,14 +392,17 @@ class InitializingPage extends StatelessWidget { color: Colors.white, ), const SizedBox(width: 10), - BrandText.buttonTitleText('domain.update_list'.tr()), + Text( + 'domain.update_list'.tr(), + style: Theme.of(context).textTheme.bodyLarge, + ), ], ), ), ], if (state is Loaded) ...[ const SizedBox(height: 32), - BrandButton.rised( + BrandButton.filled( onPressed: () => context.read().saveDomain(), text: 'initializing.save_domain'.tr(), @@ -361,74 +423,83 @@ class InitializingPage extends StatelessWidget { builder: (final context) { final formCubitState = context.watch().state; - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'initializing.create_master_account'.tr(), - style: Theme.of(context).textTheme.headlineSmall, - ), - const SizedBox(height: 16), - Text( - 'initializing.enter_username_and_password'.tr(), - style: Theme.of(context).textTheme.bodyMedium, - ), - if (formCubitState.isErrorShown) const SizedBox(height: 16), - if (formCubitState.isErrorShown) + return ResponsiveLayoutWithInfobox( + topChild: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ Text( - 'users.username_rule'.tr(), - style: TextStyle( - color: Theme.of(context).colorScheme.error, + 'initializing.create_master_account'.tr(), + style: Theme.of(context).textTheme.headlineSmall, + ), + const SizedBox(height: 16), + Text( + 'initializing.enter_username_and_password'.tr(), + style: Theme.of(context).textTheme.bodyMedium, + ), + ], + ), + primaryColumn: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (formCubitState.isErrorShown) const SizedBox(height: 16), + if (formCubitState.isErrorShown) + Text( + 'users.username_rule'.tr(), + style: TextStyle( + color: Theme.of(context).colorScheme.error, + ), + ), + const SizedBox(height: 32), + CubitFormTextField( + formFieldCubit: context.read().userName, + textAlign: TextAlign.center, + scrollPadding: const EdgeInsets.only(bottom: 70), + decoration: InputDecoration( + hintText: 'basis.username'.tr(), ), ), - const SizedBox(height: 32), - CubitFormTextField( - formFieldCubit: context.read().userName, - textAlign: TextAlign.center, - scrollPadding: const EdgeInsets.only(bottom: 70), - decoration: InputDecoration( - hintText: 'basis.username'.tr(), - ), - ), - const SizedBox(height: 16), - BlocBuilder, FieldCubitState>( - bloc: context.read().isVisible, - builder: (final context, final state) { - final bool isVisible = state.value; - return CubitFormTextField( - obscureText: !isVisible, - formFieldCubit: - context.read().password, - textAlign: TextAlign.center, - scrollPadding: const EdgeInsets.only(bottom: 70), - decoration: InputDecoration( - hintText: 'basis.password'.tr(), - suffixIcon: IconButton( - icon: Icon( - isVisible ? Icons.visibility : Icons.visibility_off, + const SizedBox(height: 16), + BlocBuilder, FieldCubitState>( + bloc: context.read().isVisible, + builder: (final context, final state) { + final bool isVisible = state.value; + return CubitFormTextField( + obscureText: !isVisible, + formFieldCubit: + context.read().password, + textAlign: TextAlign.center, + scrollPadding: const EdgeInsets.only(bottom: 70), + decoration: InputDecoration( + hintText: 'basis.password'.tr(), + suffixIcon: IconButton( + icon: Icon( + isVisible + ? Icons.visibility + : Icons.visibility_off, + ), + onPressed: () => context + .read() + .isVisible + .setValue(!isVisible), ), - onPressed: () => context - .read() - .isVisible - .setValue(!isVisible), + suffixIconConstraints: + const BoxConstraints(minWidth: 60), + prefixIconConstraints: + const BoxConstraints(maxWidth: 60), + prefixIcon: Container(), ), - suffixIconConstraints: - const BoxConstraints(minWidth: 60), - prefixIconConstraints: - const BoxConstraints(maxWidth: 60), - prefixIcon: Container(), - ), - ); - }, - ), - const SizedBox(height: 32), - BrandButton.rised( - onPressed: formCubitState.isSubmitting - ? null - : () => context.read().trySubmit(), - text: 'basis.connect'.tr(), - ), - ], + ); + }, + ), + const SizedBox(height: 32), + BrandButton.filled( + onPressed: formCubitState.isSubmitting + ? null + : () => context.read().trySubmit(), + text: 'basis.connect'.tr(), + ), + ], + ), ); }, ), @@ -438,27 +509,28 @@ class InitializingPage extends StatelessWidget { final bool isLoading = (appConfigCubit.state as ServerInstallationNotFinished).isLoading; return Builder( - builder: (final context) => Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'initializing.final'.tr(), - style: Theme.of(context).textTheme.headlineSmall, - ), - const SizedBox(height: 16), - Text( - 'initializing.create_server'.tr(), - style: Theme.of(context).textTheme.bodyMedium, - ), - const SizedBox(height: 128), - BrandButton.rised( - onPressed: - isLoading ? null : appConfigCubit.createServerAndSetDnsRecords, - text: isLoading - ? 'basis.loading'.tr() - : 'initializing.create_server'.tr(), - ), - ], + builder: (final context) => ResponsiveLayoutWithInfobox( + topChild: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'initializing.final'.tr(), + style: Theme.of(context).textTheme.headlineSmall, + ), + const SizedBox(height: 16), + Text( + 'initializing.create_server'.tr(), + style: Theme.of(context).textTheme.bodyMedium, + ), + ], + ), + primaryColumn: BrandButton.filled( + onPressed: + isLoading ? null : appConfigCubit.createServerAndSetDnsRecords, + text: isLoading + ? 'basis.loading'.tr() + : 'initializing.create_server'.tr(), + ), ), ); } @@ -487,84 +559,67 @@ class InitializingPage extends StatelessWidget { return Builder( builder: (final context) => SizedBox( width: double.infinity, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'initializing.checks'.tr(args: [doneCount.toString(), '4']), - style: Theme.of(context).textTheme.headlineSmall, - ), - const SizedBox(height: 16), - if (text != null) + child: ResponsiveLayoutWithInfobox( + topChild: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ Text( - text, - style: Theme.of(context).textTheme.bodyMedium, + 'initializing.checks'.tr(args: [doneCount.toString(), '4']), + style: Theme.of(context).textTheme.headlineSmall, ), - const SizedBox(height: 128), - const SizedBox(height: 10), - if (doneCount == 0 && state.dnsMatches != null) - Column( - children: state.dnsMatches!.entries.map((final entry) { - final String domain = entry.key; - final bool isCorrect = entry.value; - return Row( - children: [ - if (isCorrect) - const Icon(Icons.check, color: Colors.green), - if (!isCorrect) - const Icon(Icons.schedule, color: Colors.amber), - const SizedBox(width: 10), - Text(domain), - ], - ); - }).toList(), - ), - const SizedBox(height: 10), - if (!state.isLoading) - Row( - children: [ - Text( - 'initializing.until_the_next_check'.tr(), - style: Theme.of(context).textTheme.bodyMedium, - ), - BrandTimer( - startDateTime: state.timerStart!, - duration: state.duration!, - ) - ], - ), - if (state.isLoading) - Text( - 'initializing.check'.tr(), - style: Theme.of(context).textTheme.bodyMedium, - ), - ], + const SizedBox(height: 16), + if (text != null) + Text( + text, + style: Theme.of(context).textTheme.bodyMedium, + ), + ], + ), + primaryColumn: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 128), + const SizedBox(height: 10), + if (doneCount == 0 && state.dnsMatches != null) + Column( + children: state.dnsMatches!.entries.map((final entry) { + final String domain = entry.key; + final bool isCorrect = entry.value; + return Row( + children: [ + if (isCorrect) + const Icon(Icons.check, color: Colors.green), + if (!isCorrect) + const Icon(Icons.schedule, color: Colors.amber), + const SizedBox(width: 10), + Text(domain), + ], + ); + }).toList(), + ), + const SizedBox(height: 10), + if (!state.isLoading) + Row( + children: [ + Text( + 'initializing.until_the_next_check'.tr(), + style: Theme.of(context).textTheme.bodyMedium, + ), + BrandTimer( + startDateTime: state.timerStart!, + duration: state.duration!, + ) + ], + ), + if (state.isLoading) + Text( + 'initializing.check'.tr(), + style: Theme.of(context).textTheme.bodyMedium, + ), + ], + ), ), ), ); } } - -class _HowTo extends StatelessWidget { - const _HowTo({ - required this.fileName, - }); - - final String fileName; - - @override - Widget build(final BuildContext context) => BrandBottomSheet( - isExpended: true, - child: Padding( - padding: paddingH15V0, - child: ListView( - padding: const EdgeInsets.symmetric(vertical: 16), - children: [ - BrandMarkdown( - fileName: fileName, - ), - ], - ), - ), - ); -} diff --git a/lib/ui/pages/setup/initializing/server_provider_picker.dart b/lib/ui/pages/setup/initializing/server_provider_picker.dart index 56466ec8..8e1c6eee 100644 --- a/lib/ui/pages/setup/initializing/server_provider_picker.dart +++ b/lib/ui/pages/setup/initializing/server_provider_picker.dart @@ -2,16 +2,15 @@ import 'package:cubit_form/cubit_form.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:selfprivacy/config/brand_theme.dart'; import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart'; import 'package:selfprivacy/logic/cubit/forms/setup/initializing/server_provider_form_cubit.dart'; +import 'package:selfprivacy/logic/cubit/support_system/support_system_cubit.dart'; import 'package:selfprivacy/logic/models/hive/server_details.dart'; -import 'package:selfprivacy/ui/components/brand_bottom_sheet/brand_bottom_sheet.dart'; -import 'package:selfprivacy/ui/components/brand_button/brand_button.dart'; -import 'package:selfprivacy/ui/components/brand_button/outlined_button.dart'; -import 'package:selfprivacy/ui/components/brand_cards/outlined_card.dart'; -import 'package:selfprivacy/ui/components/brand_md/brand_md.dart'; +import 'package:selfprivacy/ui/components/buttons/brand_button.dart'; +import 'package:selfprivacy/ui/components/buttons/outlined_button.dart'; +import 'package:selfprivacy/ui/components/cards/outlined_card.dart'; import 'package:selfprivacy/ui/components/info_box/info_box.dart'; +import 'package:selfprivacy/ui/layouts/responsive_layout_with_infobox.dart'; import 'package:selfprivacy/utils/launch_url.dart'; class ServerProviderPicker extends StatefulWidget { @@ -98,56 +97,49 @@ class ProviderInputDataPage extends StatelessWidget { final ServerProviderFormCubit providerCubit; @override - Widget build(final BuildContext context) => Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "${'initializing.connect_to_server_provider'.tr()}${providerInfo.providerType.displayName}", - style: Theme.of(context).textTheme.headlineSmall, - ), - const SizedBox(height: 16), - Text( - 'initializing.connect_to_server_provider_text'.tr(), - style: Theme.of(context).textTheme.bodyMedium, - ), - const SizedBox(height: 32), - CubitFormTextField( - formFieldCubit: providerCubit.apiKey, - textAlign: TextAlign.center, - scrollPadding: const EdgeInsets.only(bottom: 70), - decoration: const InputDecoration( - hintText: 'Provider API Token', + Widget build(final BuildContext context) => ResponsiveLayoutWithInfobox( + topChild: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "${'initializing.connect_to_server_provider'.tr()}${providerInfo.providerType.displayName}", + style: Theme.of(context).textTheme.headlineSmall, ), - ), - const SizedBox(height: 32), - BrandButton.filled( - child: Text('basis.connect'.tr()), - onPressed: () => providerCubit.trySubmit(), - ), - const SizedBox(height: 10), - BrandOutlinedButton( - child: Text('initializing.how'.tr()), - onPressed: () => showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (final BuildContext context) => BrandBottomSheet( - isExpended: true, - child: Padding( - padding: paddingH15V0, - child: ListView( - padding: const EdgeInsets.symmetric(vertical: 16), - children: [ - BrandMarkdown( - fileName: providerInfo.pathToHow, - ), - ], - ), - ), + const SizedBox(height: 16), + Text( + 'initializing.connect_to_server_provider_text'.tr(), + style: Theme.of(context).textTheme.bodyMedium, + ), + ], + ), + primaryColumn: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CubitFormTextField( + formFieldCubit: providerCubit.apiKey, + textAlign: TextAlign.center, + scrollPadding: const EdgeInsets.only(bottom: 70), + decoration: const InputDecoration( + hintText: 'Provider API Token', ), ), - ), - ], + const SizedBox(height: 32), + BrandButton.filled( + child: Text('basis.connect'.tr()), + onPressed: () => providerCubit.trySubmit(), + ), + const SizedBox(height: 10), + BrandOutlinedButton( + child: Text('initializing.how'.tr()), + onPressed: () { + context.read().showArticle( + article: providerInfo.pathToHow, + context: context, + ); + }, + ), + ], + ), ); } @@ -164,175 +156,182 @@ class ProviderSelectionPage extends StatelessWidget { @override Widget build(final BuildContext context) => SizedBox( width: double.infinity, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'initializing.connect_to_server'.tr(), - style: Theme.of(context).textTheme.headlineSmall, - ), - const SizedBox(height: 10), - Text( - 'initializing.select_provider'.tr(), - style: Theme.of(context).textTheme.bodyMedium, - ), - const SizedBox(height: 10), - OutlinedCard( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Container( - width: 40, - height: 40, - padding: const EdgeInsets.all(10), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(40), - color: const Color(0xFFD50C2D), + child: ResponsiveLayoutWithInfobox( + topChild: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'initializing.connect_to_server'.tr(), + style: Theme.of(context).textTheme.headlineSmall, + ), + const SizedBox(height: 10), + Text( + 'initializing.select_provider'.tr(), + style: Theme.of(context).textTheme.bodyMedium, + ), + ], + ), + primaryColumn: Column( + children: [ + OutlinedCard( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + width: 40, + height: 40, + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(40), + color: const Color(0xFFD50C2D), + ), + child: SvgPicture.asset( + 'assets/images/logos/hetzner.svg', + ), ), - child: SvgPicture.asset( - 'assets/images/logos/hetzner.svg', + const SizedBox(width: 16), + Text( + 'Hetzner Cloud', + style: Theme.of(context).textTheme.titleMedium, ), - ), - const SizedBox(width: 16), - Text( - 'Hetzner Cloud', - style: Theme.of(context).textTheme.titleMedium, - ), - ], - ), - const SizedBox(height: 16), - Text( - 'initializing.select_provider_countries_title'.tr(), - style: Theme.of(context).textTheme.bodyLarge, - ), - Text( - 'initializing.select_provider_countries_text_hetzner' - .tr(), - style: Theme.of(context).textTheme.bodySmall, - ), - const SizedBox(height: 16), - Text( - 'initializing.select_provider_price_title'.tr(), - style: Theme.of(context).textTheme.bodyLarge, - ), - Text( - 'initializing.select_provider_price_text_hetzner'.tr(), - style: Theme.of(context).textTheme.bodySmall, - ), - const SizedBox(height: 16), - Text( - 'initializing.select_provider_payment_title'.tr(), - style: Theme.of(context).textTheme.bodyLarge, - ), - Text( - 'initializing.select_provider_payment_text_hetzner'.tr(), - style: Theme.of(context).textTheme.bodySmall, - ), - const SizedBox(height: 16), - Text( - 'initializing.select_provider_email_notice'.tr(), - style: Theme.of(context).textTheme.bodySmall, - ), - const SizedBox(height: 16), - BrandButton.filled( - child: Text('basis.select'.tr()), - onPressed: () { - serverInstallationCubit - .setServerProviderType(ServerProvider.hetzner); - callback(ServerProvider.hetzner); - }, - ), - // Outlined button that will open website - BrandOutlinedButton( - onPressed: () => - launchURL('https://www.hetzner.com/cloud'), - title: 'initializing.select_provider_site_button'.tr(), - ), - ], + ], + ), + const SizedBox(height: 16), + Text( + 'initializing.select_provider_countries_title'.tr(), + style: Theme.of(context).textTheme.bodyLarge, + ), + Text( + 'initializing.select_provider_countries_text_hetzner' + .tr(), + style: Theme.of(context).textTheme.bodySmall, + ), + const SizedBox(height: 16), + Text( + 'initializing.select_provider_price_title'.tr(), + style: Theme.of(context).textTheme.bodyLarge, + ), + Text( + 'initializing.select_provider_price_text_hetzner'.tr(), + style: Theme.of(context).textTheme.bodySmall, + ), + const SizedBox(height: 16), + Text( + 'initializing.select_provider_payment_title'.tr(), + style: Theme.of(context).textTheme.bodyLarge, + ), + Text( + 'initializing.select_provider_payment_text_hetzner' + .tr(), + style: Theme.of(context).textTheme.bodySmall, + ), + const SizedBox(height: 16), + Text( + 'initializing.select_provider_email_notice'.tr(), + style: Theme.of(context).textTheme.bodySmall, + ), + const SizedBox(height: 16), + BrandButton.filled( + child: Text('basis.select'.tr()), + onPressed: () { + serverInstallationCubit + .setServerProviderType(ServerProvider.hetzner); + callback(ServerProvider.hetzner); + }, + ), + // Outlined button that will open website + BrandOutlinedButton( + onPressed: () => + launchURL('https://www.hetzner.com/cloud'), + title: 'initializing.select_provider_site_button'.tr(), + ), + ], + ), ), ), - ), - const SizedBox(height: 16), - OutlinedCard( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Container( - width: 40, - height: 40, - padding: const EdgeInsets.all(10), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(40), - color: const Color(0xFF0080FF), + const SizedBox(height: 16), + OutlinedCard( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + width: 40, + height: 40, + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(40), + color: const Color(0xFF0080FF), + ), + child: SvgPicture.asset( + 'assets/images/logos/digital_ocean.svg', + ), ), - child: SvgPicture.asset( - 'assets/images/logos/digital_ocean.svg', + const SizedBox(width: 16), + Text( + 'Digital Ocean', + style: Theme.of(context).textTheme.titleMedium, ), - ), - const SizedBox(width: 16), - Text( - 'Digital Ocean', - style: Theme.of(context).textTheme.titleMedium, - ), - ], - ), - const SizedBox(height: 16), - Text( - 'initializing.select_provider_countries_title'.tr(), - style: Theme.of(context).textTheme.bodyLarge, - ), - Text( - 'initializing.select_provider_countries_text_do'.tr(), - style: Theme.of(context).textTheme.bodySmall, - ), - const SizedBox(height: 16), - Text( - 'initializing.select_provider_price_title'.tr(), - style: Theme.of(context).textTheme.bodyLarge, - ), - Text( - 'initializing.select_provider_price_text_do'.tr(), - style: Theme.of(context).textTheme.bodySmall, - ), - const SizedBox(height: 16), - Text( - 'initializing.select_provider_payment_title'.tr(), - style: Theme.of(context).textTheme.bodyLarge, - ), - Text( - 'initializing.select_provider_payment_text_do'.tr(), - style: Theme.of(context).textTheme.bodySmall, - ), - const SizedBox(height: 16), - BrandButton.filled( - child: Text('basis.select'.tr()), - onPressed: () { - serverInstallationCubit - .setServerProviderType(ServerProvider.digitalOcean); - callback(ServerProvider.digitalOcean); - }, - ), - // Outlined button that will open website - BrandOutlinedButton( - onPressed: () => - launchURL('https://www.digitalocean.com'), - title: 'initializing.select_provider_site_button'.tr(), - ), - ], + ], + ), + const SizedBox(height: 16), + Text( + 'initializing.select_provider_countries_title'.tr(), + style: Theme.of(context).textTheme.bodyLarge, + ), + Text( + 'initializing.select_provider_countries_text_do'.tr(), + style: Theme.of(context).textTheme.bodySmall, + ), + const SizedBox(height: 16), + Text( + 'initializing.select_provider_price_title'.tr(), + style: Theme.of(context).textTheme.bodyLarge, + ), + Text( + 'initializing.select_provider_price_text_do'.tr(), + style: Theme.of(context).textTheme.bodySmall, + ), + const SizedBox(height: 16), + Text( + 'initializing.select_provider_payment_title'.tr(), + style: Theme.of(context).textTheme.bodyLarge, + ), + Text( + 'initializing.select_provider_payment_text_do'.tr(), + style: Theme.of(context).textTheme.bodySmall, + ), + const SizedBox(height: 16), + BrandButton.filled( + child: Text('basis.select'.tr()), + onPressed: () { + serverInstallationCubit.setServerProviderType( + ServerProvider.digitalOcean, + ); + callback(ServerProvider.digitalOcean); + }, + ), + // Outlined button that will open website + BrandOutlinedButton( + onPressed: () => + launchURL('https://www.digitalocean.com'), + title: 'initializing.select_provider_site_button'.tr(), + ), + ], + ), ), ), - ), - const SizedBox(height: 16), - InfoBox(text: 'initializing.select_provider_notice'.tr()), - ], + ], + ), + secondaryColumn: + InfoBox(text: 'initializing.select_provider_notice'.tr()), ), ); } diff --git a/lib/ui/pages/setup/initializing/server_type_picker.dart b/lib/ui/pages/setup/initializing/server_type_picker.dart index 5a6632c8..3c1c5380 100644 --- a/lib/ui/pages/setup/initializing/server_type_picker.dart +++ b/lib/ui/pages/setup/initializing/server_type_picker.dart @@ -5,8 +5,9 @@ import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_depe import 'package:selfprivacy/logic/cubit/app_settings/app_settings_cubit.dart'; import 'package:selfprivacy/logic/models/server_provider_location.dart'; import 'package:selfprivacy/logic/models/server_type.dart'; -import 'package:selfprivacy/ui/components/brand_button/brand_button.dart'; +import 'package:selfprivacy/ui/components/buttons/brand_button.dart'; import 'package:selfprivacy/ui/components/info_box/info_box.dart'; +import 'package:selfprivacy/ui/layouts/responsive_layout_with_infobox.dart'; class ServerTypePicker extends StatefulWidget { const ServerTypePicker({ @@ -70,50 +71,67 @@ class SelectLocationPage extends StatelessWidget { if ((snapshot.data as List).isEmpty) { return Text('initializing.no_locations_found'.tr()); } - return Column( - children: [ - Text( - 'initializing.choose_location_type'.tr(), - style: Theme.of(context).textTheme.headlineSmall, - ), - const SizedBox(height: 16), - Text( - 'initializing.choose_location_type_text'.tr(), - style: Theme.of(context).textTheme.bodyMedium, - ), - const SizedBox(height: 16), - ...(snapshot.data! as List).map( - (final location) => SizedBox( - width: double.infinity, - child: InkWell( - onTap: () { - callback(location); - }, - child: Card( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '${location.flag ?? ''} ${location.title}', - style: Theme.of(context).textTheme.titleMedium, - ), - const SizedBox(height: 8), - if (location.description != null) - Text( - location.description!, - style: Theme.of(context).textTheme.bodyMedium, + return ResponsiveLayoutWithInfobox( + topChild: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'initializing.choose_location_type'.tr(), + style: Theme.of(context).textTheme.headlineSmall, + ), + const SizedBox(height: 16), + Text( + 'initializing.choose_location_type_text'.tr(), + style: Theme.of(context).textTheme.bodyMedium, + ), + ], + ), + primaryColumn: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ...(snapshot.data! as List).map( + (final location) => Column( + children: [ + SizedBox( + width: double.infinity, + child: Card( + clipBehavior: Clip.antiAlias, + child: InkResponse( + highlightShape: BoxShape.rectangle, + onTap: () { + callback(location); + }, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '${location.flag ?? ''} ${location.title}', + style: Theme.of(context) + .textTheme + .titleMedium, + ), + const SizedBox(height: 8), + if (location.description != null) + Text( + location.description!, + style: Theme.of(context) + .textTheme + .bodyMedium, + ), + ], ), - ], + ), + ), ), ), - ), + const SizedBox(height: 8), + ], ), ), - ), - const SizedBox(height: 24), - ], + ], + ), ); } else { return const Center(child: CircularProgressIndicator()); @@ -180,121 +198,145 @@ class SelectTypePage extends StatelessWidget { ], ); } - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'initializing.choose_server_type'.tr(), - style: Theme.of(context).textTheme.headlineSmall, - ), - const SizedBox(height: 16), - Text( - 'initializing.choose_server_type_text'.tr(), - style: Theme.of(context).textTheme.bodyMedium, - ), - const SizedBox(height: 16), - ...(snapshot.data! as List).map( - (final type) => SizedBox( - width: double.infinity, - child: InkWell( - onTap: () { - serverInstallationCubit.setServerType(type); - }, - child: Card( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - type.title, - style: Theme.of(context).textTheme.titleMedium, - ), - const SizedBox(height: 8), - Row( - children: [ - Icon( - Icons.memory_outlined, - color: - Theme.of(context).colorScheme.onSurface, - ), - const SizedBox(width: 8), - Text( - 'server.core_count'.plural(type.cores), - style: - Theme.of(context).textTheme.bodyMedium, - ), - ], - ), - const SizedBox(height: 8), - Row( - children: [ - Icon( - Icons.memory_outlined, - color: - Theme.of(context).colorScheme.onSurface, - ), - const SizedBox(width: 8), - Text( - 'initializing.choose_server_type_ram' - .tr(args: [type.ram.toString()]), - style: - Theme.of(context).textTheme.bodyMedium, - ), - ], - ), - const SizedBox(height: 8), - Row( - children: [ - Icon( - Icons.sd_card_outlined, - color: - Theme.of(context).colorScheme.onSurface, - ), - const SizedBox(width: 8), - Text( - 'initializing.choose_server_type_storage' - .tr( - args: [type.disk.gibibyte.toString()], + return ResponsiveLayoutWithInfobox( + topChild: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'initializing.choose_server_type'.tr(), + style: Theme.of(context).textTheme.headlineSmall, + ), + const SizedBox(height: 16), + Text( + 'initializing.choose_server_type_text'.tr(), + style: Theme.of(context).textTheme.bodyMedium, + ), + ], + ), + primaryColumn: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ...(snapshot.data! as List).map( + (final type) => Column( + children: [ + SizedBox( + width: double.infinity, + child: InkWell( + onTap: () { + serverInstallationCubit.setServerType(type); + }, + child: Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + type.title, + style: Theme.of(context) + .textTheme + .titleMedium, ), - style: - Theme.of(context).textTheme.bodyMedium, - ), - ], - ), - const SizedBox(height: 8), - const Divider(height: 8), - const SizedBox(height: 8), - Row( - children: [ - Icon( - Icons.payments_outlined, - color: - Theme.of(context).colorScheme.onSurface, - ), - const SizedBox(width: 8), - Text( - 'initializing.choose_server_type_payment_per_month' - .tr( - args: [ - '${type.price.value.toString()} ${type.price.currency}' + const SizedBox(height: 8), + Row( + children: [ + Icon( + Icons.memory_outlined, + color: Theme.of(context) + .colorScheme + .onSurface, + ), + const SizedBox(width: 8), + Text( + 'server.core_count' + .plural(type.cores), + style: Theme.of(context) + .textTheme + .bodyMedium, + ), ], ), - style: - Theme.of(context).textTheme.bodyLarge, - ), - ], + const SizedBox(height: 8), + Row( + children: [ + Icon( + Icons.memory_outlined, + color: Theme.of(context) + .colorScheme + .onSurface, + ), + const SizedBox(width: 8), + Text( + 'initializing.choose_server_type_ram' + .tr(args: [type.ram.toString()]), + style: Theme.of(context) + .textTheme + .bodyMedium, + ), + ], + ), + const SizedBox(height: 8), + Row( + children: [ + Icon( + Icons.sd_card_outlined, + color: Theme.of(context) + .colorScheme + .onSurface, + ), + const SizedBox(width: 8), + Text( + 'initializing.choose_server_type_storage' + .tr( + args: [ + type.disk.gibibyte.toString() + ], + ), + style: Theme.of(context) + .textTheme + .bodyMedium, + ), + ], + ), + const SizedBox(height: 8), + const Divider(height: 8), + const SizedBox(height: 8), + Row( + children: [ + Icon( + Icons.payments_outlined, + color: Theme.of(context) + .colorScheme + .onSurface, + ), + const SizedBox(width: 8), + Text( + 'initializing.choose_server_type_payment_per_month' + .tr( + args: [ + '${type.price.value.toString()} ${type.price.currency}' + ], + ), + style: Theme.of(context) + .textTheme + .bodyLarge, + ), + ], + ), + ], + ), ), - ], + ), ), ), - ), + const SizedBox(height: 8), + ], ), ), - ), - const SizedBox(height: 16), - InfoBox(text: 'initializing.choose_server_type_notice'.tr()), - ], + ], + ), + secondaryColumn: + InfoBox(text: 'initializing.choose_server_type_notice'.tr()), ); } else { return const Center(child: CircularProgressIndicator()); diff --git a/lib/ui/pages/setup/recovering/recover_by_new_device_key.dart b/lib/ui/pages/setup/recovering/recover_by_new_device_key.dart index bb2ebbf0..4f6cf352 100644 --- a/lib/ui/pages/setup/recovering/recover_by_new_device_key.dart +++ b/lib/ui/pages/setup/recovering/recover_by_new_device_key.dart @@ -1,8 +1,8 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:selfprivacy/logic/cubit/forms/setup/recovering/recovery_device_form_cubit.dart'; -import 'package:selfprivacy/ui/components/brand_button/brand_button.dart'; -import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; +import 'package:selfprivacy/ui/components/buttons/brand_button.dart'; +import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart'; import 'package:selfprivacy/utils/route_transitions/basic.dart'; import 'package:cubit_form/cubit_form.dart'; import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; @@ -17,6 +17,7 @@ class RecoverByNewDeviceKeyInstruction extends StatelessWidget { heroSubtitle: 'recovering.method_device_description'.tr(), hasBackButton: true, hasFlashButton: false, + ignoreBreakpoints: true, onBackButtonPressed: context.read().revertRecoveryStep, children: [ @@ -61,6 +62,7 @@ class RecoverByNewDeviceKeyInput extends StatelessWidget { heroSubtitle: 'recovering.method_device_input_description'.tr(), hasBackButton: true, hasFlashButton: false, + ignoreBreakpoints: true, children: [ CubitFormTextField( formFieldCubit: diff --git a/lib/ui/pages/setup/recovering/recover_by_old_token.dart b/lib/ui/pages/setup/recovering/recover_by_old_token.dart index 2b65ba8b..42d60f34 100644 --- a/lib/ui/pages/setup/recovering/recover_by_old_token.dart +++ b/lib/ui/pages/setup/recovering/recover_by_old_token.dart @@ -1,8 +1,8 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:selfprivacy/logic/cubit/forms/setup/recovering/recovery_device_form_cubit.dart'; -import 'package:selfprivacy/ui/components/brand_button/brand_button.dart'; -import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; +import 'package:selfprivacy/ui/components/buttons/brand_button.dart'; +import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart'; import 'package:selfprivacy/ui/components/brand_md/brand_md.dart'; import 'package:cubit_form/cubit_form.dart'; import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; @@ -28,6 +28,7 @@ class RecoverByOldTokenInstruction extends StatelessWidget { heroTitle: 'recovering.recovery_main_header'.tr(), hasBackButton: true, hasFlashButton: false, + ignoreBreakpoints: true, onBackButtonPressed: context.read().revertRecoveryStep, children: [ @@ -72,6 +73,7 @@ class RecoverByOldToken extends StatelessWidget { heroSubtitle: 'recovering.method_device_input_description'.tr(), hasBackButton: true, hasFlashButton: false, + ignoreBreakpoints: true, children: [ CubitFormTextField( formFieldCubit: diff --git a/lib/ui/pages/setup/recovering/recover_by_recovery_key.dart b/lib/ui/pages/setup/recovering/recover_by_recovery_key.dart index c47b924f..b39dc2da 100644 --- a/lib/ui/pages/setup/recovering/recover_by_recovery_key.dart +++ b/lib/ui/pages/setup/recovering/recover_by_recovery_key.dart @@ -4,8 +4,8 @@ import 'package:flutter/material.dart'; import 'package:selfprivacy/logic/cubit/forms/factories/field_cubit_factory.dart'; import 'package:selfprivacy/logic/cubit/forms/setup/recovering/recovery_device_form_cubit.dart'; import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; -import 'package:selfprivacy/ui/components/brand_button/brand_button.dart'; -import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; +import 'package:selfprivacy/ui/components/buttons/brand_button.dart'; +import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart'; class RecoverByRecoveryKey extends StatelessWidget { const RecoverByRecoveryKey({super.key}); @@ -31,6 +31,7 @@ class RecoverByRecoveryKey extends StatelessWidget { heroSubtitle: 'recovering.method_recovery_input_description'.tr(), hasBackButton: true, hasFlashButton: false, + ignoreBreakpoints: true, onBackButtonPressed: context.read().revertRecoveryStep, children: [ diff --git a/lib/ui/pages/setup/recovering/recovery_confirm_backblaze.dart b/lib/ui/pages/setup/recovering/recovery_confirm_backblaze.dart index 0b7e7a9e..f7216a74 100644 --- a/lib/ui/pages/setup/recovering/recovery_confirm_backblaze.dart +++ b/lib/ui/pages/setup/recovering/recovery_confirm_backblaze.dart @@ -1,13 +1,11 @@ import 'package:cubit_form/cubit_form.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:selfprivacy/config/brand_theme.dart'; import 'package:selfprivacy/logic/cubit/forms/setup/initializing/backblaze_form_cubit.dart'; import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; -import 'package:selfprivacy/ui/components/brand_bottom_sheet/brand_bottom_sheet.dart'; -import 'package:selfprivacy/ui/components/brand_button/brand_button.dart'; -import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; -import 'package:selfprivacy/ui/components/brand_md/brand_md.dart'; +import 'package:selfprivacy/logic/cubit/support_system/support_system_cubit.dart'; +import 'package:selfprivacy/ui/components/buttons/brand_button.dart'; +import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart'; class RecoveryConfirmBackblaze extends StatelessWidget { const RecoveryConfirmBackblaze({super.key}); @@ -28,6 +26,8 @@ class RecoveryConfirmBackblaze extends StatelessWidget { heroTitle: 'recovering.confirm_backblaze'.tr(), heroSubtitle: 'recovering.confirm_backblaze_description'.tr(), hasBackButton: true, + ignoreBreakpoints: true, + hasSupportDrawer: true, onBackButtonPressed: () { Navigator.of(context).popUntil((final route) => route.isFirst); }, @@ -57,27 +57,15 @@ class RecoveryConfirmBackblaze extends StatelessWidget { text: 'basis.connect'.tr(), ), const SizedBox(height: 16), - BrandButton.text( - onPressed: () => showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (final BuildContext context) => BrandBottomSheet( - isExpended: true, - child: Padding( - padding: paddingH15V0, - child: ListView( - padding: const EdgeInsets.symmetric(vertical: 16), - children: const [ - BrandMarkdown( - fileName: 'how_backblaze', + Builder( + builder: (final context) => BrandButton.text( + onPressed: () => + context.read().showArticle( + article: 'how_backblaze', + context: context, ), - ], - ), - ), - ), + title: 'initializing.how'.tr(), ), - title: 'initializing.how'.tr(), ), ], ); diff --git a/lib/ui/pages/setup/recovering/recovery_confirm_cloudflare.dart b/lib/ui/pages/setup/recovering/recovery_confirm_cloudflare.dart index 4b766e56..93c889a5 100644 --- a/lib/ui/pages/setup/recovering/recovery_confirm_cloudflare.dart +++ b/lib/ui/pages/setup/recovering/recovery_confirm_cloudflare.dart @@ -1,13 +1,11 @@ import 'package:cubit_form/cubit_form.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:selfprivacy/config/brand_theme.dart'; import 'package:selfprivacy/logic/cubit/forms/setup/initializing/dns_provider_form_cubit.dart'; import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; -import 'package:selfprivacy/ui/components/brand_bottom_sheet/brand_bottom_sheet.dart'; -import 'package:selfprivacy/ui/components/brand_button/brand_button.dart'; -import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; -import 'package:selfprivacy/ui/components/brand_md/brand_md.dart'; +import 'package:selfprivacy/logic/cubit/support_system/support_system_cubit.dart'; +import 'package:selfprivacy/ui/components/buttons/brand_button.dart'; +import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart'; class RecoveryConfirmCloudflare extends StatelessWidget { const RecoveryConfirmCloudflare({super.key}); @@ -31,6 +29,8 @@ class RecoveryConfirmCloudflare extends StatelessWidget { ), hasBackButton: true, hasFlashButton: false, + ignoreBreakpoints: true, + hasSupportDrawer: true, onBackButtonPressed: context.read().revertRecoveryStep, children: [ @@ -49,27 +49,15 @@ class RecoveryConfirmCloudflare extends StatelessWidget { text: 'basis.connect'.tr(), ), const SizedBox(height: 16), - BrandButton.text( - onPressed: () => showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (final BuildContext context) => BrandBottomSheet( - isExpended: true, - child: Padding( - padding: paddingH15V0, - child: ListView( - padding: const EdgeInsets.symmetric(vertical: 16), - children: const [ - BrandMarkdown( - fileName: 'how_cloudflare', + Builder( + builder: (final context) => BrandButton.text( + onPressed: () => + context.read().showArticle( + article: 'how_cloudflare', + context: context, ), - ], - ), - ), - ), + title: 'initializing.how'.tr(), ), - title: 'initializing.how'.tr(), ), ], ); diff --git a/lib/ui/pages/setup/recovering/recovery_confirm_server.dart b/lib/ui/pages/setup/recovering/recovery_confirm_server.dart index 3b47cc76..2efcff87 100644 --- a/lib/ui/pages/setup/recovering/recovery_confirm_server.dart +++ b/lib/ui/pages/setup/recovering/recovery_confirm_server.dart @@ -2,9 +2,9 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart'; import 'package:selfprivacy/logic/models/server_basic_info.dart'; -import 'package:selfprivacy/ui/components/brand_button/brand_button.dart'; -import 'package:selfprivacy/ui/components/brand_cards/filled_card.dart'; -import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; +import 'package:selfprivacy/ui/components/buttons/brand_button.dart'; +import 'package:selfprivacy/ui/components/cards/filled_card.dart'; +import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart'; class RecoveryConfirmServer extends StatefulWidget { const RecoveryConfirmServer({super.key}); @@ -38,6 +38,7 @@ class _RecoveryConfirmServerState extends State { ? 'recovering.choose_server_description'.tr() : 'recovering.confirm_server_description'.tr(), hasBackButton: true, + ignoreBreakpoints: true, onBackButtonPressed: () { Navigator.of(context).popUntil((final route) => route.isFirst); }, diff --git a/lib/ui/pages/setup/recovering/recovery_method_select.dart b/lib/ui/pages/setup/recovering/recovery_method_select.dart index 68129d57..f8cec44a 100644 --- a/lib/ui/pages/setup/recovering/recovery_method_select.dart +++ b/lib/ui/pages/setup/recovering/recovery_method_select.dart @@ -2,9 +2,9 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; -import 'package:selfprivacy/ui/components/brand_button/brand_button.dart'; -import 'package:selfprivacy/ui/components/brand_cards/outlined_card.dart'; -import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; +import 'package:selfprivacy/ui/components/buttons/brand_button.dart'; +import 'package:selfprivacy/ui/components/cards/outlined_card.dart'; +import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart'; import 'package:selfprivacy/ui/pages/setup/recovering/recover_by_old_token.dart'; import 'package:selfprivacy/utils/route_transitions/basic.dart'; @@ -17,6 +17,7 @@ class RecoveryMethodSelect extends StatelessWidget { heroSubtitle: 'recovering.method_select_description'.tr(), hasBackButton: true, hasFlashButton: false, + ignoreBreakpoints: true, onBackButtonPressed: context.read().revertRecoveryStep, children: [ @@ -74,6 +75,7 @@ class RecoveryFallbackMethodSelect extends StatelessWidget { heroSubtitle: 'recovering.fallback_select_description'.tr(), hasBackButton: true, hasFlashButton: false, + ignoreBreakpoints: true, children: [ OutlinedCard( child: ListTile( diff --git a/lib/ui/pages/setup/recovering/recovery_routing.dart b/lib/ui/pages/setup/recovering/recovery_routing.dart index 38016a06..0d11c4d8 100644 --- a/lib/ui/pages/setup/recovering/recovery_routing.dart +++ b/lib/ui/pages/setup/recovering/recovery_routing.dart @@ -1,11 +1,12 @@ +import 'package:auto_route/auto_route.dart'; import 'package:cubit_form/cubit_form.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; import 'package:selfprivacy/logic/cubit/forms/factories/field_cubit_factory.dart'; import 'package:selfprivacy/logic/cubit/forms/setup/recovering/recovery_domain_form_cubit.dart'; -import 'package:selfprivacy/ui/components/brand_button/brand_button.dart'; -import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; +import 'package:selfprivacy/ui/components/buttons/brand_button.dart'; +import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart'; import 'package:selfprivacy/ui/pages/root_route.dart'; import 'package:selfprivacy/ui/pages/setup/recovering/recover_by_old_token.dart'; import 'package:selfprivacy/ui/pages/setup/recovering/recover_by_recovery_key.dart'; @@ -17,6 +18,7 @@ import 'package:selfprivacy/ui/pages/setup/recovering/recovery_server_provider_c import 'package:selfprivacy/ui/pages/setup/recovering/recovery_method_select.dart'; import 'package:selfprivacy/utils/route_transitions/basic.dart'; +@RoutePage() class RecoveryRouting extends StatelessWidget { const RecoveryRouting({super.key}); @@ -110,6 +112,7 @@ class SelectDomainToRecover extends StatelessWidget { heroSubtitle: 'recovering.domain_recovery_description'.tr(), hasBackButton: true, hasFlashButton: false, + ignoreBreakpoints: true, onBackButtonPressed: () { Navigator.of(context).pushAndRemoveUntil( materialRoute(const RootPage()), diff --git a/lib/ui/pages/setup/recovering/recovery_server_provider_connected.dart b/lib/ui/pages/setup/recovering/recovery_server_provider_connected.dart index 71b43d42..9b6cb09e 100644 --- a/lib/ui/pages/setup/recovering/recovery_server_provider_connected.dart +++ b/lib/ui/pages/setup/recovering/recovery_server_provider_connected.dart @@ -1,13 +1,11 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:selfprivacy/config/brand_theme.dart'; import 'package:selfprivacy/logic/cubit/forms/setup/initializing/server_provider_form_cubit.dart'; -import 'package:selfprivacy/ui/components/brand_bottom_sheet/brand_bottom_sheet.dart'; -import 'package:selfprivacy/ui/components/brand_button/brand_button.dart'; -import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; +import 'package:selfprivacy/logic/cubit/support_system/support_system_cubit.dart'; +import 'package:selfprivacy/ui/components/buttons/brand_button.dart'; +import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart'; import 'package:cubit_form/cubit_form.dart'; import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; -import 'package:selfprivacy/ui/components/brand_md/brand_md.dart'; class RecoveryServerProviderConnected extends StatelessWidget { const RecoveryServerProviderConnected({super.key}); @@ -32,6 +30,8 @@ class RecoveryServerProviderConnected extends StatelessWidget { ), hasBackButton: true, hasFlashButton: false, + ignoreBreakpoints: true, + hasSupportDrawer: true, onBackButtonPressed: () { Navigator.of(context).popUntil((final route) => route.isFirst); }, @@ -52,26 +52,14 @@ class RecoveryServerProviderConnected extends StatelessWidget { child: Text('basis.continue'.tr()), ), const SizedBox(height: 16), - BrandButton.text( - title: 'initializing.how'.tr(), - onPressed: () => showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (final BuildContext context) => BrandBottomSheet( - isExpended: true, - child: Padding( - padding: paddingH15V0, - child: ListView( - padding: const EdgeInsets.symmetric(vertical: 16), - children: const [ - BrandMarkdown( - fileName: 'how_hetzner', + Builder( + builder: (final context) => BrandButton.text( + title: 'initializing.how'.tr(), + onPressed: () => + context.read().showArticle( + article: 'how_hetzner', + context: context, ), - ], - ), - ), - ), ), ), ], diff --git a/lib/ui/pages/users/add_user_fab.dart b/lib/ui/pages/users/add_user_fab.dart deleted file mode 100644 index 7e87e51d..00000000 --- a/lib/ui/pages/users/add_user_fab.dart +++ /dev/null @@ -1,22 +0,0 @@ -part of 'users.dart'; - -class AddUserFab extends StatelessWidget { - const AddUserFab({super.key}); - - @override - Widget build(final BuildContext context) => FloatingActionButton.small( - heroTag: 'new_user_fab', - onPressed: () { - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (final BuildContext context) => Padding( - padding: MediaQuery.of(context).viewInsets, - child: const NewUser(), - ), - ); - }, - child: const Icon(Icons.person_add_outlined), - ); -} diff --git a/lib/ui/pages/users/empty.dart b/lib/ui/pages/users/empty.dart index dc2d292b..f58dc740 100644 --- a/lib/ui/pages/users/empty.dart +++ b/lib/ui/pages/users/empty.dart @@ -11,21 +11,25 @@ class _NoUsers extends StatelessWidget { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - const Icon(BrandIcons.users, size: 50, color: BrandColors.grey7), + Icon( + BrandIcons.users, + size: 50, + color: Theme.of(context).colorScheme.onBackground, + ), const SizedBox(height: 20), - BrandText.h2( + Text( 'users.nobody_here'.tr(), - style: const TextStyle( - color: BrandColors.grey7, - ), + style: Theme.of(context).textTheme.headlineMedium?.copyWith( + color: Theme.of(context).colorScheme.onBackground, + ), ), const SizedBox(height: 10), - BrandText.medium( + Text( text, textAlign: TextAlign.center, - style: const TextStyle( - color: BrandColors.grey7, - ), + style: Theme.of(context).textTheme.titleSmall?.copyWith( + color: Theme.of(context).colorScheme.onBackground, + ), ), ], ), @@ -43,21 +47,25 @@ class _CouldNotLoadUsers extends StatelessWidget { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - const Icon(BrandIcons.users, size: 50, color: BrandColors.grey7), + Icon( + BrandIcons.users, + size: 50, + color: Theme.of(context).colorScheme.onBackground, + ), const SizedBox(height: 20), - BrandText.h2( + Text( 'users.could_not_fetch_users'.tr(), - style: const TextStyle( - color: BrandColors.grey7, - ), + style: Theme.of(context).textTheme.headlineMedium?.copyWith( + color: Theme.of(context).colorScheme.onBackground, + ), ), const SizedBox(height: 10), - BrandText.medium( + Text( text, textAlign: TextAlign.center, - style: const TextStyle( - color: BrandColors.grey7, - ), + style: Theme.of(context).textTheme.titleSmall?.copyWith( + color: Theme.of(context).colorScheme.onBackground, + ), ), ], ), diff --git a/lib/ui/pages/users/new_user.dart b/lib/ui/pages/users/new_user.dart index 7c393296..d7ed2aca 100644 --- a/lib/ui/pages/users/new_user.dart +++ b/lib/ui/pages/users/new_user.dart @@ -1,7 +1,8 @@ part of 'users.dart'; -class NewUser extends StatelessWidget { - const NewUser({super.key}); +@RoutePage() +class NewUserPage extends StatelessWidget { + const NewUserPage({super.key}); @override Widget build(final BuildContext context) { @@ -10,108 +11,89 @@ class NewUser extends StatelessWidget { final String domainName = UiHelpers.getDomainName(config); - return BrandBottomSheet( - child: BlocProvider( - create: (final BuildContext context) { - final jobCubit = context.read(); - final jobState = jobCubit.state; - final users = []; - users.addAll(context.read().state.users); - if (jobState is JobsStateWithJobs) { - final jobs = jobState.clientJobList; - for (final job in jobs) { - if (job is CreateUserJob) { - users.add(job.user); - } + return BlocProvider( + create: (final BuildContext context) { + final jobCubit = context.read(); + final jobState = jobCubit.state; + final users = []; + users.addAll(context.read().state.users); + if (jobState is JobsStateWithJobs) { + final jobs = jobState.clientJobList; + for (final job in jobs) { + if (job is CreateUserJob) { + users.add(job.user); } } - return UserFormCubit( - jobsCubit: jobCubit, - fieldFactory: FieldCubitFactory(context), - ); - }, - child: Builder( - builder: (final BuildContext context) { - final FormCubitState formCubitState = - context.watch().state; + } + return UserFormCubit( + jobsCubit: jobCubit, + fieldFactory: FieldCubitFactory(context), + ); + }, + child: Builder( + builder: (final BuildContext context) { + final FormCubitState formCubitState = + context.watch().state; - return BlocListener( - listener: - (final BuildContext context, final FormCubitState state) { - if (state.isSubmitted) { - Navigator.pop(context); - } - }, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - BrandHeader( - title: 'users.new_user'.tr(), - ), - const SizedBox(width: 14), - Padding( - padding: paddingH15V0, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (formCubitState.isErrorShown) - Text( - 'users.username_rule'.tr(), - style: TextStyle( - color: Theme.of(context).colorScheme.error, - ), - ), - const SizedBox(width: 14), - IntrinsicHeight( - child: CubitFormTextField( - formFieldCubit: context.read().login, - decoration: InputDecoration( - labelText: 'users.login'.tr(), - suffixText: '@$domainName', - ), - ), - ), - const SizedBox(height: 20), - CubitFormTextField( - formFieldCubit: - context.read().password, - decoration: InputDecoration( - alignLabelWithHint: false, - labelText: 'basis.password'.tr(), - suffixIcon: Padding( - padding: const EdgeInsets.only(right: 8), - child: IconButton( - icon: Icon( - BrandIcons.refresh, - color: - Theme.of(context).colorScheme.secondary, - ), - onPressed: context - .read() - .genNewPassword, - ), - ), - ), - ), - const SizedBox(height: 30), - BrandButton.rised( - onPressed: formCubitState.isSubmitting - ? null - : () => context.read().trySubmit(), - text: 'basis.create'.tr(), - ), - const SizedBox(height: 40), - Text('users.new_user_info_note'.tr()), - const SizedBox(height: 30), - ], + return BlocListener( + listener: (final BuildContext context, final FormCubitState state) { + if (state.isSubmitted) { + context.router.pop(); + } + }, + child: BrandHeroScreen( + heroTitle: 'users.new_user'.tr(), + heroIcon: Icons.person_add_outlined, + children: [ + if (formCubitState.isErrorShown) + Text( + 'users.username_rule'.tr(), + style: TextStyle( + color: Theme.of(context).colorScheme.error, ), ), - ], - ), - ); - }, - ), + const SizedBox(width: 14), + IntrinsicHeight( + child: CubitFormTextField( + formFieldCubit: context.read().login, + decoration: InputDecoration( + labelText: 'users.login'.tr(), + suffixText: '@$domainName', + ), + ), + ), + const SizedBox(height: 20), + CubitFormTextField( + formFieldCubit: context.read().password, + decoration: InputDecoration( + alignLabelWithHint: false, + labelText: 'basis.password'.tr(), + suffixIcon: Padding( + padding: const EdgeInsets.only(right: 8), + child: IconButton( + icon: Icon( + BrandIcons.refresh, + color: Theme.of(context).colorScheme.secondary, + ), + onPressed: context.read().genNewPassword, + ), + ), + ), + ), + const SizedBox(height: 30), + BrandButton.rised( + onPressed: formCubitState.isSubmitting + ? null + : () => context.read().trySubmit(), + text: 'basis.create'.tr(), + ), + const SizedBox(height: 40), + Text('users.new_user_info_note'.tr()), + const SizedBox(height: 30), + ], + ), + ); + }, ), ); } diff --git a/lib/ui/pages/users/reset_password.dart b/lib/ui/pages/users/reset_password.dart index 87f95902..64785d3a 100644 --- a/lib/ui/pages/users/reset_password.dart +++ b/lib/ui/pages/users/reset_password.dart @@ -9,76 +9,73 @@ class ResetPassword extends StatelessWidget { final User user; @override - Widget build(final BuildContext context) => BrandBottomSheet( - child: BlocProvider( - create: (final BuildContext context) => UserFormCubit( - jobsCubit: context.read(), - fieldFactory: FieldCubitFactory(context), - initialUser: user, - ), - child: Builder( - builder: (final BuildContext context) { - final FormCubitState formCubitState = - context.watch().state; + Widget build(final BuildContext context) => BlocProvider( + create: (final BuildContext context) => UserFormCubit( + jobsCubit: context.read(), + fieldFactory: FieldCubitFactory(context), + initialUser: user, + ), + child: Builder( + builder: (final BuildContext context) { + final FormCubitState formCubitState = + context.watch().state; - return BlocListener( - listener: - (final BuildContext context, final FormCubitState state) { - if (state.isSubmitted) { - Navigator.pop(context); - } - }, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - BrandHeader( - title: 'users.reset_password'.tr(), - ), - const SizedBox(width: 14), - Padding( - padding: paddingH15V0, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - CubitFormTextField( - formFieldCubit: - context.read().password, - decoration: InputDecoration( - alignLabelWithHint: false, - labelText: 'basis.password'.tr(), - suffixIcon: Padding( - padding: const EdgeInsets.only(right: 8), - child: IconButton( - icon: Icon( - BrandIcons.refresh, - color: - Theme.of(context).colorScheme.secondary, - ), - onPressed: context - .read() - .genNewPassword, + return BlocListener( + listener: + (final BuildContext context, final FormCubitState state) { + if (state.isSubmitted) { + Navigator.pop(context); + } + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + BrandHeader( + title: 'users.reset_password'.tr(), + ), + const SizedBox(width: 14), + Padding( + padding: paddingH15V0, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + CubitFormTextField( + formFieldCubit: + context.read().password, + decoration: InputDecoration( + alignLabelWithHint: false, + labelText: 'basis.password'.tr(), + suffixIcon: Padding( + padding: const EdgeInsets.only(right: 8), + child: IconButton( + icon: Icon( + BrandIcons.refresh, + color: + Theme.of(context).colorScheme.secondary, ), + onPressed: context + .read() + .genNewPassword, ), ), ), - const SizedBox(height: 30), - BrandButton.rised( - onPressed: formCubitState.isSubmitting - ? null - : () => - context.read().trySubmit(), - text: 'basis.apply'.tr(), - ), - const SizedBox(height: 30), - ], - ), + ), + const SizedBox(height: 30), + BrandButton.rised( + onPressed: formCubitState.isSubmitting + ? null + : () => context.read().trySubmit(), + text: 'basis.apply'.tr(), + ), + const SizedBox(height: 30), + ], ), - ], - ), - ); - }, - ), + ), + ], + ), + ); + }, ), ); } diff --git a/lib/ui/pages/users/user.dart b/lib/ui/pages/users/user.dart index 1d781811..bbc01012 100644 --- a/lib/ui/pages/users/user.dart +++ b/lib/ui/pages/users/user.dart @@ -11,9 +11,7 @@ class _User extends StatelessWidget { @override Widget build(final BuildContext context) => InkWell( onTap: () { - Navigator.of(context).push( - materialRoute(UserDetails(login: user.login)), - ); + context.pushRoute(UserDetailsRoute(login: user.login)); }, child: Container( padding: paddingH15V0, @@ -30,17 +28,17 @@ class _User extends StatelessWidget { ), const SizedBox(width: 20), Flexible( - child: isRootUser - ? BrandText.h4Underlined(user.login) - // cross out text if user not found on server - : BrandText.h4( - user.login, - style: user.isFoundOnServer - ? null - : const TextStyle( - decoration: TextDecoration.lineThrough, - ), + child: Text( + user.login, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + color: Theme.of(context).colorScheme.onBackground, + decoration: isRootUser + ? TextDecoration.underline + : user.isFoundOnServer + ? TextDecoration.none + : TextDecoration.lineThrough, ), + ), ), ], ), diff --git a/lib/ui/pages/users/user_details.dart b/lib/ui/pages/users/user_details.dart index 3ae5b86d..be7205fb 100644 --- a/lib/ui/pages/users/user_details.dart +++ b/lib/ui/pages/users/user_details.dart @@ -1,7 +1,8 @@ part of 'users.dart'; -class UserDetails extends StatelessWidget { - const UserDetails({ +@RoutePage() +class UserDetailsPage extends StatelessWidget { + const UserDetailsPage({ required this.login, super.key, }); @@ -25,6 +26,7 @@ class UserDetails extends StatelessWidget { if (user.type == UserType.root) { return BrandHeroScreen( hasBackButton: true, + hasFlashButton: true, heroTitle: 'ssh.root_title'.tr(), heroSubtitle: 'ssh.root_subtitle'.tr(), children: [ @@ -35,6 +37,7 @@ class UserDetails extends StatelessWidget { return BrandHeroScreen( hasBackButton: true, + hasFlashButton: true, heroTitle: user.login, children: [ _UserLogins(user: user, domainName: domainName), @@ -87,6 +90,7 @@ class _DeleteUserTile extends StatelessWidget { onTap: () => { showDialog( context: context, + // useRootNavigator: false, builder: (final BuildContext context) => AlertDialog( title: Text('basis.confirmation'.tr()), content: SingleChildScrollView( @@ -102,7 +106,7 @@ class _DeleteUserTile extends StatelessWidget { TextButton( child: Text('basis.cancel'.tr()), onPressed: () { - Navigator.of(context).pop(); + context.router.pop(); }, ), TextButton( @@ -114,9 +118,8 @@ class _DeleteUserTile extends StatelessWidget { ), onPressed: () { context.read().addJob(DeleteUserJob(user: user)); - Navigator.of(context) - ..pop() - ..pop(); + context.router.childControllers.first.pop(); + context.router.pop(); }, ), ], @@ -231,9 +234,7 @@ class _SshKeysCard extends StatelessWidget { publicKey: key, ), ); - Navigator.of(context) - ..pop() - ..pop(); + context.popRoute(); }, ), ], @@ -253,72 +254,69 @@ class NewSshKey extends StatelessWidget { final User user; @override - Widget build(final BuildContext context) => BrandBottomSheet( - child: BlocProvider( - create: (final context) { - final jobCubit = context.read(); - final jobState = jobCubit.state; - if (jobState is JobsStateWithJobs) { - final jobs = jobState.clientJobList; - for (final job in jobs) { - if (job is CreateSSHKeyJob && job.user.login == user.login) { - user.sshKeys.add(job.publicKey); - } + Widget build(final BuildContext context) => BlocProvider( + create: (final context) { + final jobCubit = context.read(); + final jobState = jobCubit.state; + if (jobState is JobsStateWithJobs) { + final jobs = jobState.clientJobList; + for (final job in jobs) { + if (job is CreateSSHKeyJob && job.user.login == user.login) { + user.sshKeys.add(job.publicKey); } } - return SshFormCubit( - jobsCubit: jobCubit, - user: user, - ); - }, - child: Builder( - builder: (final context) { - final formCubitState = context.watch().state; + } + return SshFormCubit( + jobsCubit: jobCubit, + user: user, + ); + }, + child: Builder( + builder: (final context) { + final formCubitState = context.watch().state; - return BlocListener( - listener: (final context, final state) { - if (state.isSubmitted) { - Navigator.pop(context); - } - }, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - BrandHeader( - title: user.login, - ), - const SizedBox(width: 14), - Padding( - padding: paddingH15V0, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - IntrinsicHeight( - child: CubitFormTextField( - formFieldCubit: context.read().key, - decoration: InputDecoration( - labelText: 'ssh.input_label'.tr(), - ), + return BlocListener( + listener: (final context, final state) { + if (state.isSubmitted) { + Navigator.pop(context); + } + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + BrandHeader( + title: user.login, + ), + const SizedBox(width: 14), + Padding( + padding: paddingH15V0, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + IntrinsicHeight( + child: CubitFormTextField( + formFieldCubit: context.read().key, + decoration: InputDecoration( + labelText: 'ssh.input_label'.tr(), ), ), - const SizedBox(height: 30), - BrandButton.rised( - onPressed: formCubitState.isSubmitting - ? null - : () => - context.read().trySubmit(), - text: 'ssh.create'.tr(), - ), - const SizedBox(height: 30), - ], - ), + ), + const SizedBox(height: 30), + BrandButton.rised( + onPressed: formCubitState.isSubmitting + ? null + : () => context.read().trySubmit(), + text: 'ssh.create'.tr(), + ), + const SizedBox(height: 30), + ], ), - ], - ), - ); - }, - ), + ), + ], + ), + ); + }, ), ); } diff --git a/lib/ui/pages/users/users.dart b/lib/ui/pages/users/users.dart index caf6d441..58cb3b7d 100644 --- a/lib/ui/pages/users/users.dart +++ b/lib/ui/pages/users/users.dart @@ -1,7 +1,7 @@ +import 'package:auto_route/auto_route.dart'; import 'package:cubit_form/cubit_form.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:selfprivacy/config/brand_colors.dart'; import 'package:selfprivacy/config/brand_theme.dart'; import 'package:selfprivacy/logic/cubit/forms/user/ssh_form_cubit.dart'; import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; @@ -11,28 +11,26 @@ import 'package:selfprivacy/logic/cubit/client_jobs/client_jobs_cubit.dart'; import 'package:selfprivacy/logic/cubit/users/users_cubit.dart'; import 'package:selfprivacy/logic/models/job.dart'; import 'package:selfprivacy/logic/models/hive/user.dart'; -import 'package:selfprivacy/ui/components/brand_bottom_sheet/brand_bottom_sheet.dart'; -import 'package:selfprivacy/ui/components/brand_button/brand_button.dart'; -import 'package:selfprivacy/ui/components/brand_button/outlined_button.dart'; -import 'package:selfprivacy/ui/components/brand_cards/filled_card.dart'; +import 'package:selfprivacy/ui/components/buttons/brand_button.dart'; +import 'package:selfprivacy/ui/components/buttons/outlined_button.dart'; +import 'package:selfprivacy/ui/components/cards/filled_card.dart'; import 'package:selfprivacy/ui/components/brand_header/brand_header.dart'; -import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; +import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart'; import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart'; -import 'package:selfprivacy/ui/components/brand_text/brand_text.dart'; import 'package:selfprivacy/ui/components/info_box/info_box.dart'; import 'package:selfprivacy/ui/components/list_tiles/list_tile_on_surface_variant.dart'; import 'package:selfprivacy/ui/components/not_ready_card/not_ready_card.dart'; +import 'package:selfprivacy/ui/router/router.dart'; +import 'package:selfprivacy/utils/breakpoints.dart'; import 'package:selfprivacy/utils/ui_helpers.dart'; -import 'package:selfprivacy/utils/route_transitions/basic.dart'; - part 'empty.dart'; part 'new_user.dart'; part 'user.dart'; part 'user_details.dart'; -part 'add_user_fab.dart'; part 'reset_password.dart'; +@RoutePage() class UsersPage extends StatelessWidget { const UsersPage({super.key}); @@ -64,7 +62,7 @@ class UsersPage extends StatelessWidget { } return RefreshIndicator( onRefresh: () async { - context.read().refresh(); + await context.read().refresh(); }, child: Container( padding: const EdgeInsets.symmetric(horizontal: 15), @@ -90,15 +88,38 @@ class UsersPage extends StatelessWidget { } return RefreshIndicator( onRefresh: () async { - context.read().refresh(); + await context.read().refresh(); }, - child: ListView.builder( - itemCount: users.length, - itemBuilder: (final BuildContext context, final int index) => - _User( - user: users[index], - isRootUser: users[index].type == UserType.primary, - ), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: FilledButton.tonal( + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.person_add_outlined), + const SizedBox(width: 8), + Text('users.new_user'.tr()), + ], + ), + onPressed: () { + context.pushRoute(const NewUserRoute()); + }, + ), + ), + Expanded( + child: ListView.builder( + itemCount: users.length, + itemBuilder: + (final BuildContext context, final int index) => _User( + user: users[index], + isRootUser: users[index].type == UserType.primary, + ), + ), + ), + ], ), ); }, @@ -106,12 +127,14 @@ class UsersPage extends StatelessWidget { } return Scaffold( - appBar: PreferredSize( - preferredSize: const Size.fromHeight(52), - child: BrandHeader( - title: 'basis.users'.tr(), - ), - ), + appBar: Breakpoints.small.isActive(context) + ? PreferredSize( + preferredSize: const Size.fromHeight(52), + child: BrandHeader( + title: 'basis.users'.tr(), + ), + ) + : null, body: child, ); } diff --git a/lib/ui/router/root_destinations.dart b/lib/ui/router/root_destinations.dart new file mode 100644 index 00000000..0d981894 --- /dev/null +++ b/lib/ui/router/root_destinations.dart @@ -0,0 +1,46 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart'; +import 'package:selfprivacy/ui/router/router.dart'; + +class RouteDestination { + const RouteDestination({ + required this.route, + required this.icon, + required this.label, + required this.title, + }); + + final PageRouteInfo route; + final IconData icon; + final String label; + final String title; +} + +final rootDestinations = [ + RouteDestination( + route: const ProvidersRoute(), + icon: BrandIcons.server, + label: 'basis.providers'.tr(), + title: 'basis.providers_title'.tr(), + ), + RouteDestination( + route: const ServicesRoute(), + icon: BrandIcons.box, + label: 'basis.services'.tr(), + title: 'basis.services'.tr(), + ), + RouteDestination( + route: const UsersRoute(), + icon: BrandIcons.users, + label: 'basis.users'.tr(), + title: 'basis.users'.tr(), + ), + RouteDestination( + route: const MoreRoute(), + icon: Icons.menu_rounded, + label: 'basis.more'.tr(), + title: 'basis.more'.tr(), + ), +]; diff --git a/lib/ui/router/router.dart b/lib/ui/router/router.dart new file mode 100644 index 00000000..23a4ec0e --- /dev/null +++ b/lib/ui/router/router.dart @@ -0,0 +1,150 @@ +import 'package:animations/animations.dart'; +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:selfprivacy/logic/models/disk_status.dart'; +import 'package:selfprivacy/logic/models/service.dart'; +import 'package:selfprivacy/ui/pages/backup_details/backup_details.dart'; +import 'package:selfprivacy/ui/pages/devices/devices.dart'; +import 'package:selfprivacy/ui/pages/dns_details/dns_details.dart'; +import 'package:selfprivacy/ui/pages/more/about_application.dart'; +import 'package:selfprivacy/ui/pages/more/app_settings/app_settings.dart'; +import 'package:selfprivacy/ui/pages/more/app_settings/developer_settings.dart'; +import 'package:selfprivacy/ui/pages/more/console.dart'; +import 'package:selfprivacy/ui/pages/more/more.dart'; +import 'package:selfprivacy/ui/pages/onboarding/onboarding.dart'; +import 'package:selfprivacy/ui/pages/providers/providers.dart'; +import 'package:selfprivacy/ui/pages/recovery_key/recovery_key.dart'; +import 'package:selfprivacy/ui/pages/root_route.dart'; +import 'package:selfprivacy/ui/pages/server_details/server_details_screen.dart'; +import 'package:selfprivacy/ui/pages/server_storage/binds_migration/services_migration.dart'; +import 'package:selfprivacy/ui/pages/server_storage/extending_volume.dart'; +import 'package:selfprivacy/ui/pages/server_storage/server_storage.dart'; +import 'package:selfprivacy/ui/pages/services/service_page.dart'; +import 'package:selfprivacy/ui/pages/services/services.dart'; +import 'package:selfprivacy/ui/pages/setup/initializing/initializing.dart'; +import 'package:selfprivacy/ui/pages/setup/recovering/recovery_routing.dart'; +import 'package:selfprivacy/ui/pages/users/users.dart'; + +part 'router.gr.dart'; + +Widget fadeThroughTransition( + final BuildContext context, + final Animation animation, + final Animation secondaryAnimation, + final Widget child, +) => + SharedAxisTransition( + key: UniqueKey(), + animation: animation, + secondaryAnimation: secondaryAnimation, + transitionType: SharedAxisTransitionType.vertical, + child: child, + ); + +@AutoRouterConfig( + // transitionsBuilder: fadeThroughTransition, + replaceInRouteName: 'Page|Screen|Routing,Route', +) +class RootRouter extends _$RootRouter { + RootRouter(GlobalKey super.navigatorKey); + + @override + RouteType get defaultRouteType => const RouteType.material(); + @override + final List routes = [ + AutoRoute(page: OnboardingRoute.page), + AutoRoute(page: InitializingRoute.page), + AutoRoute(page: RecoveryRoute.page), + AutoRoute( + page: RootRoute.page, + path: '/', + children: [ + CustomRoute( + page: ProvidersRoute.page, + usesPathAsKey: true, + path: '', + transitionsBuilder: fadeThroughTransition, + durationInMilliseconds: 400, + ), + CustomRoute( + page: ServicesRoute.page, + usesPathAsKey: true, + transitionsBuilder: fadeThroughTransition, + durationInMilliseconds: 400, + ), + CustomRoute( + page: UsersRoute.page, + usesPathAsKey: true, + transitionsBuilder: fadeThroughTransition, + durationInMilliseconds: 400, + ), + CustomRoute( + page: MoreRoute.page, + usesPathAsKey: true, + transitionsBuilder: fadeThroughTransition, + durationInMilliseconds: 400, + ), + AutoRoute(page: AppSettingsRoute.page), + AutoRoute(page: UserDetailsRoute.page), + AutoRoute(page: NewUserRoute.page), + AutoRoute(page: RecoveryKeyRoute.page), + AutoRoute(page: DevicesRoute.page), + AutoRoute(page: AboutApplicationRoute.page), + AutoRoute(page: DeveloperSettingsRoute.page), + AutoRoute(page: ServiceRoute.page), + AutoRoute(page: ServerDetailsRoute.page), + AutoRoute(page: DnsDetailsRoute.page), + AutoRoute(page: BackupDetailsRoute.page), + AutoRoute(page: ServerStorageRoute.page), + AutoRoute(page: ExtendingVolumeRoute.page), + ], + ), + AutoRoute(page: ServicesMigrationRoute.page), + AutoRoute(page: ConsoleRoute.page), + ]; +} + +// Function to map route names to route titles +String getRouteTitle(final String routeName) { + switch (routeName) { + case 'RootRoute': + return 'basis.app_name'; + case 'ProvidersRoute': + return 'basis.providers_title'; + case 'ServicesRoute': + case 'ServiceRoute': + return 'basis.services'; + case 'UsersRoute': + return 'basis.users'; + case 'MoreRoute': + return 'basis.more'; + case 'AppSettingsRoute': + return 'application_settings.title'; + case 'UserDetailsRoute': + return 'users.details_title'; + case 'NewUserRoute': + return 'users.new_user'; + case 'RecoveryKeyRoute': + return 'recovery_key.key_main_header'; + case 'DevicesRoute': + return 'devices.main_screen.header'; + case 'AboutApplicationRoute': + return 'about_us_page.title'; + case 'ConsoleRoute': + return 'console_page.title'; + case 'DeveloperSettingsRoute': + return 'developer_settings.title'; + case 'DnsDetailsRoute': + return 'domain.screen_title'; + case 'ServerDetailsRoute': + return 'server.card_title'; + case 'BackupDetailsRoute': + return 'backup.card_title'; + case 'ServerStorageRoute': + return 'storage.card_title'; + case 'ExtendingVolumeRoute': + return 'storage.extending_volume_title'; + default: + return routeName; + } +} diff --git a/lib/ui/router/router.gr.dart b/lib/ui/router/router.gr.dart new file mode 100644 index 00000000..e5b0449f --- /dev/null +++ b/lib/ui/router/router.gr.dart @@ -0,0 +1,637 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +// ************************************************************************** +// AutoRouterGenerator +// ************************************************************************** + +// ignore_for_file: type=lint +// coverage:ignore-file + +part of 'router.dart'; + +abstract class _$RootRouter extends RootStackRouter { + _$RootRouter([GlobalKey? navigatorKey]) + : super(navigatorKey: navigatorKey); + + @override + final Map pagesMap = { + BackupDetailsRoute.name: (routeData) { + return AutoRoutePage( + routeData: routeData, + child: const BackupDetailsPage(), + ); + }, + RootRoute.name: (routeData) { + return AutoRoutePage( + routeData: routeData, + child: WrappedRoute(child: const RootPage()), + ); + }, + ServiceRoute.name: (routeData) { + final args = routeData.argsAs(); + return AutoRoutePage( + routeData: routeData, + child: ServicePage( + serviceId: args.serviceId, + key: args.key, + ), + ); + }, + ServicesRoute.name: (routeData) { + return AutoRoutePage( + routeData: routeData, + child: const ServicesPage(), + ); + }, + ServerDetailsRoute.name: (routeData) { + return AutoRoutePage( + routeData: routeData, + child: const ServerDetailsScreen(), + ); + }, + UsersRoute.name: (routeData) { + return AutoRoutePage( + routeData: routeData, + child: const UsersPage(), + ); + }, + NewUserRoute.name: (routeData) { + return AutoRoutePage( + routeData: routeData, + child: const NewUserPage(), + ); + }, + UserDetailsRoute.name: (routeData) { + final args = routeData.argsAs(); + return AutoRoutePage( + routeData: routeData, + child: UserDetailsPage( + login: args.login, + key: args.key, + ), + ); + }, + AppSettingsRoute.name: (routeData) { + return AutoRoutePage( + routeData: routeData, + child: const AppSettingsPage(), + ); + }, + DeveloperSettingsRoute.name: (routeData) { + return AutoRoutePage( + routeData: routeData, + child: const DeveloperSettingsPage(), + ); + }, + MoreRoute.name: (routeData) { + return AutoRoutePage( + routeData: routeData, + child: const MorePage(), + ); + }, + AboutApplicationRoute.name: (routeData) { + return AutoRoutePage( + routeData: routeData, + child: const AboutApplicationPage(), + ); + }, + ConsoleRoute.name: (routeData) { + return AutoRoutePage( + routeData: routeData, + child: const ConsolePage(), + ); + }, + ProvidersRoute.name: (routeData) { + return AutoRoutePage( + routeData: routeData, + child: const ProvidersPage(), + ); + }, + RecoveryKeyRoute.name: (routeData) { + return AutoRoutePage( + routeData: routeData, + child: const RecoveryKeyPage(), + ); + }, + DnsDetailsRoute.name: (routeData) { + return AutoRoutePage( + routeData: routeData, + child: const DnsDetailsPage(), + ); + }, + RecoveryRoute.name: (routeData) { + return AutoRoutePage( + routeData: routeData, + child: const RecoveryRouting(), + ); + }, + InitializingRoute.name: (routeData) { + return AutoRoutePage( + routeData: routeData, + child: const InitializingPage(), + ); + }, + ServerStorageRoute.name: (routeData) { + final args = routeData.argsAs(); + return AutoRoutePage( + routeData: routeData, + child: ServerStoragePage( + diskStatus: args.diskStatus, + key: args.key, + ), + ); + }, + ExtendingVolumeRoute.name: (routeData) { + final args = routeData.argsAs(); + return AutoRoutePage( + routeData: routeData, + child: ExtendingVolumePage( + diskVolumeToResize: args.diskVolumeToResize, + diskStatus: args.diskStatus, + key: args.key, + ), + ); + }, + ServicesMigrationRoute.name: (routeData) { + final args = routeData.argsAs(); + return AutoRoutePage( + routeData: routeData, + child: ServicesMigrationPage( + services: args.services, + diskStatus: args.diskStatus, + isMigration: args.isMigration, + key: args.key, + ), + ); + }, + DevicesRoute.name: (routeData) { + return AutoRoutePage( + routeData: routeData, + child: const DevicesScreen(), + ); + }, + OnboardingRoute.name: (routeData) { + return AutoRoutePage( + routeData: routeData, + child: const OnboardingPage(), + ); + }, + }; +} + +/// generated route for +/// [BackupDetailsPage] +class BackupDetailsRoute extends PageRouteInfo { + const BackupDetailsRoute({List? children}) + : super( + BackupDetailsRoute.name, + initialChildren: children, + ); + + static const String name = 'BackupDetailsRoute'; + + static const PageInfo page = PageInfo(name); +} + +/// generated route for +/// [RootPage] +class RootRoute extends PageRouteInfo { + const RootRoute({List? children}) + : super( + RootRoute.name, + initialChildren: children, + ); + + static const String name = 'RootRoute'; + + static const PageInfo page = PageInfo(name); +} + +/// generated route for +/// [ServicePage] +class ServiceRoute extends PageRouteInfo { + ServiceRoute({ + required String serviceId, + Key? key, + List? children, + }) : super( + ServiceRoute.name, + args: ServiceRouteArgs( + serviceId: serviceId, + key: key, + ), + initialChildren: children, + ); + + static const String name = 'ServiceRoute'; + + static const PageInfo page = + PageInfo(name); +} + +class ServiceRouteArgs { + const ServiceRouteArgs({ + required this.serviceId, + this.key, + }); + + final String serviceId; + + final Key? key; + + @override + String toString() { + return 'ServiceRouteArgs{serviceId: $serviceId, key: $key}'; + } +} + +/// generated route for +/// [ServicesPage] +class ServicesRoute extends PageRouteInfo { + const ServicesRoute({List? children}) + : super( + ServicesRoute.name, + initialChildren: children, + ); + + static const String name = 'ServicesRoute'; + + static const PageInfo page = PageInfo(name); +} + +/// generated route for +/// [ServerDetailsScreen] +class ServerDetailsRoute extends PageRouteInfo { + const ServerDetailsRoute({List? children}) + : super( + ServerDetailsRoute.name, + initialChildren: children, + ); + + static const String name = 'ServerDetailsRoute'; + + static const PageInfo page = PageInfo(name); +} + +/// generated route for +/// [UsersPage] +class UsersRoute extends PageRouteInfo { + const UsersRoute({List? children}) + : super( + UsersRoute.name, + initialChildren: children, + ); + + static const String name = 'UsersRoute'; + + static const PageInfo page = PageInfo(name); +} + +/// generated route for +/// [NewUserPage] +class NewUserRoute extends PageRouteInfo { + const NewUserRoute({List? children}) + : super( + NewUserRoute.name, + initialChildren: children, + ); + + static const String name = 'NewUserRoute'; + + static const PageInfo page = PageInfo(name); +} + +/// generated route for +/// [UserDetailsPage] +class UserDetailsRoute extends PageRouteInfo { + UserDetailsRoute({ + required String login, + Key? key, + List? children, + }) : super( + UserDetailsRoute.name, + args: UserDetailsRouteArgs( + login: login, + key: key, + ), + initialChildren: children, + ); + + static const String name = 'UserDetailsRoute'; + + static const PageInfo page = + PageInfo(name); +} + +class UserDetailsRouteArgs { + const UserDetailsRouteArgs({ + required this.login, + this.key, + }); + + final String login; + + final Key? key; + + @override + String toString() { + return 'UserDetailsRouteArgs{login: $login, key: $key}'; + } +} + +/// generated route for +/// [AppSettingsPage] +class AppSettingsRoute extends PageRouteInfo { + const AppSettingsRoute({List? children}) + : super( + AppSettingsRoute.name, + initialChildren: children, + ); + + static const String name = 'AppSettingsRoute'; + + static const PageInfo page = PageInfo(name); +} + +/// generated route for +/// [DeveloperSettingsPage] +class DeveloperSettingsRoute extends PageRouteInfo { + const DeveloperSettingsRoute({List? children}) + : super( + DeveloperSettingsRoute.name, + initialChildren: children, + ); + + static const String name = 'DeveloperSettingsRoute'; + + static const PageInfo page = PageInfo(name); +} + +/// generated route for +/// [MorePage] +class MoreRoute extends PageRouteInfo { + const MoreRoute({List? children}) + : super( + MoreRoute.name, + initialChildren: children, + ); + + static const String name = 'MoreRoute'; + + static const PageInfo page = PageInfo(name); +} + +/// generated route for +/// [AboutApplicationPage] +class AboutApplicationRoute extends PageRouteInfo { + const AboutApplicationRoute({List? children}) + : super( + AboutApplicationRoute.name, + initialChildren: children, + ); + + static const String name = 'AboutApplicationRoute'; + + static const PageInfo page = PageInfo(name); +} + +/// generated route for +/// [ConsolePage] +class ConsoleRoute extends PageRouteInfo { + const ConsoleRoute({List? children}) + : super( + ConsoleRoute.name, + initialChildren: children, + ); + + static const String name = 'ConsoleRoute'; + + static const PageInfo page = PageInfo(name); +} + +/// generated route for +/// [ProvidersPage] +class ProvidersRoute extends PageRouteInfo { + const ProvidersRoute({List? children}) + : super( + ProvidersRoute.name, + initialChildren: children, + ); + + static const String name = 'ProvidersRoute'; + + static const PageInfo page = PageInfo(name); +} + +/// generated route for +/// [RecoveryKeyPage] +class RecoveryKeyRoute extends PageRouteInfo { + const RecoveryKeyRoute({List? children}) + : super( + RecoveryKeyRoute.name, + initialChildren: children, + ); + + static const String name = 'RecoveryKeyRoute'; + + static const PageInfo page = PageInfo(name); +} + +/// generated route for +/// [DnsDetailsPage] +class DnsDetailsRoute extends PageRouteInfo { + const DnsDetailsRoute({List? children}) + : super( + DnsDetailsRoute.name, + initialChildren: children, + ); + + static const String name = 'DnsDetailsRoute'; + + static const PageInfo page = PageInfo(name); +} + +/// generated route for +/// [RecoveryRouting] +class RecoveryRoute extends PageRouteInfo { + const RecoveryRoute({List? children}) + : super( + RecoveryRoute.name, + initialChildren: children, + ); + + static const String name = 'RecoveryRoute'; + + static const PageInfo page = PageInfo(name); +} + +/// generated route for +/// [InitializingPage] +class InitializingRoute extends PageRouteInfo { + const InitializingRoute({List? children}) + : super( + InitializingRoute.name, + initialChildren: children, + ); + + static const String name = 'InitializingRoute'; + + static const PageInfo page = PageInfo(name); +} + +/// generated route for +/// [ServerStoragePage] +class ServerStorageRoute extends PageRouteInfo { + ServerStorageRoute({ + required DiskStatus diskStatus, + Key? key, + List? children, + }) : super( + ServerStorageRoute.name, + args: ServerStorageRouteArgs( + diskStatus: diskStatus, + key: key, + ), + initialChildren: children, + ); + + static const String name = 'ServerStorageRoute'; + + static const PageInfo page = + PageInfo(name); +} + +class ServerStorageRouteArgs { + const ServerStorageRouteArgs({ + required this.diskStatus, + this.key, + }); + + final DiskStatus diskStatus; + + final Key? key; + + @override + String toString() { + return 'ServerStorageRouteArgs{diskStatus: $diskStatus, key: $key}'; + } +} + +/// generated route for +/// [ExtendingVolumePage] +class ExtendingVolumeRoute extends PageRouteInfo { + ExtendingVolumeRoute({ + required DiskVolume diskVolumeToResize, + required DiskStatus diskStatus, + Key? key, + List? children, + }) : super( + ExtendingVolumeRoute.name, + args: ExtendingVolumeRouteArgs( + diskVolumeToResize: diskVolumeToResize, + diskStatus: diskStatus, + key: key, + ), + initialChildren: children, + ); + + static const String name = 'ExtendingVolumeRoute'; + + static const PageInfo page = + PageInfo(name); +} + +class ExtendingVolumeRouteArgs { + const ExtendingVolumeRouteArgs({ + required this.diskVolumeToResize, + required this.diskStatus, + this.key, + }); + + final DiskVolume diskVolumeToResize; + + final DiskStatus diskStatus; + + final Key? key; + + @override + String toString() { + return 'ExtendingVolumeRouteArgs{diskVolumeToResize: $diskVolumeToResize, diskStatus: $diskStatus, key: $key}'; + } +} + +/// generated route for +/// [ServicesMigrationPage] +class ServicesMigrationRoute extends PageRouteInfo { + ServicesMigrationRoute({ + required List services, + required DiskStatus diskStatus, + required bool isMigration, + Key? key, + List? children, + }) : super( + ServicesMigrationRoute.name, + args: ServicesMigrationRouteArgs( + services: services, + diskStatus: diskStatus, + isMigration: isMigration, + key: key, + ), + initialChildren: children, + ); + + static const String name = 'ServicesMigrationRoute'; + + static const PageInfo page = + PageInfo(name); +} + +class ServicesMigrationRouteArgs { + const ServicesMigrationRouteArgs({ + required this.services, + required this.diskStatus, + required this.isMigration, + this.key, + }); + + final List services; + + final DiskStatus diskStatus; + + final bool isMigration; + + final Key? key; + + @override + String toString() { + return 'ServicesMigrationRouteArgs{services: $services, diskStatus: $diskStatus, isMigration: $isMigration, key: $key}'; + } +} + +/// generated route for +/// [DevicesScreen] +class DevicesRoute extends PageRouteInfo { + const DevicesRoute({List? children}) + : super( + DevicesRoute.name, + initialChildren: children, + ); + + static const String name = 'DevicesRoute'; + + static const PageInfo page = PageInfo(name); +} + +/// generated route for +/// [OnboardingPage] +class OnboardingRoute extends PageRouteInfo { + const OnboardingRoute({List? children}) + : super( + OnboardingRoute.name, + initialChildren: children, + ); + + static const String name = 'OnboardingRoute'; + + static const PageInfo page = PageInfo(name); +} diff --git a/lib/utils/breakpoints.dart b/lib/utils/breakpoints.dart new file mode 100644 index 00000000..0e9104e2 --- /dev/null +++ b/lib/utils/breakpoints.dart @@ -0,0 +1,131 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; + +const Set _desktop = { + TargetPlatform.linux, + TargetPlatform.macOS, + TargetPlatform.windows +}; + +const Set _mobile = { + TargetPlatform.android, + TargetPlatform.fuchsia, + TargetPlatform.iOS, +}; + +/// A group of standard breakpoints built according to the material +/// specifications for screen width size. +/// +/// See also: +/// +/// * [AdaptiveScaffold], which uses some of these Breakpoints as defaults. +class Breakpoints { + /// This is a standard breakpoint that can be used as a fallthrough in the + /// case that no other breakpoint is active. + /// + /// It is active from a width of -1 dp to infinity. + static const Breakpoint standard = WidthPlatformBreakpoint(begin: -1); + + /// A window whose width is less than 600 dp and greater than 0 dp. + static const Breakpoint small = WidthPlatformBreakpoint(begin: 0, end: 600); + + /// A window whose width is greater than 0 dp. + static const Breakpoint smallAndUp = WidthPlatformBreakpoint(begin: 0); + + /// A desktop screen whose width is less than 600 dp and greater than 0 dp. + static const Breakpoint smallDesktop = + WidthPlatformBreakpoint(begin: 0, end: 600, platform: _desktop); + + /// A mobile screen whose width is less than 600 dp and greater than 0 dp. + static const Breakpoint smallMobile = + WidthPlatformBreakpoint(begin: 0, end: 600, platform: _mobile); + + /// A window whose width is between 600 dp and 840 dp. + static const Breakpoint medium = + WidthPlatformBreakpoint(begin: 600, end: 840); + + /// A window whose width is greater than 600 dp. + static const Breakpoint mediumAndUp = WidthPlatformBreakpoint(begin: 600); + + /// A desktop window whose width is between 600 dp and 840 dp. + static const Breakpoint mediumDesktop = + WidthPlatformBreakpoint(begin: 600, end: 840, platform: _desktop); + + /// A mobile window whose width is between 600 dp and 840 dp. + static const Breakpoint mediumMobile = + WidthPlatformBreakpoint(begin: 600, end: 840, platform: _mobile); + + /// A window whose width is greater than 840 dp. + static const Breakpoint large = WidthPlatformBreakpoint(begin: 840); + + /// A desktop window whose width is greater than 840 dp. + static const Breakpoint largeDesktop = + WidthPlatformBreakpoint(begin: 840, platform: _desktop); + + /// A mobile window whose width is greater than 840 dp. + static const Breakpoint largeMobile = + WidthPlatformBreakpoint(begin: 840, platform: _mobile); +} + +/// A class that can be used to quickly generate [Breakpoint]s that depend on +/// the screen width and the platform. +class WidthPlatformBreakpoint extends Breakpoint { + /// Returns a const [Breakpoint] with the given constraints. + const WidthPlatformBreakpoint({this.begin, this.end, this.platform}); + + /// The beginning width dp value. If left null then the [Breakpoint] will have + /// no lower bound. + final double? begin; + + /// The end width dp value. If left null then the [Breakpoint] will have no + /// upper bound. + final double? end; + + /// A Set of [TargetPlatform]s that the [Breakpoint] will be active on. If + /// left null then it will be active on all platforms. + final Set? platform; + + @override + bool isActive(final BuildContext context) { + final TargetPlatform host = Theme.of(context).platform; + final bool isRightPlatform = platform?.contains(host) ?? true; + + // Null boundaries are unbounded, assign the max/min of their associated + // direction on a number line. + final double width = MediaQuery.of(context).size.width; + final double lowerBound = begin ?? double.negativeInfinity; + final double upperBound = end ?? double.infinity; + + return width >= lowerBound && width < upperBound && isRightPlatform; + } +} + +/// An interface to define the conditions that distinguish between types of +/// screens. +/// +/// Adaptive apps usually display differently depending on the screen type: a +/// compact layout for smaller screens, or a relaxed layout for larger screens. +/// Override this class by defining `isActive` to fetch the screen property +/// (usually `MediaQuery.of`) and return true if the condition is met. +/// +/// Breakpoints do not need to be exclusive because they are tested in order +/// with the last Breakpoint active taking priority. +/// +/// If the condition is only based on the screen width and/or the device type, +/// use [WidthPlatformBreakpoint] to define the [Breakpoint]. +/// +/// See also: +/// +/// * [SlotLayout.config], which uses breakpoints to dictate the layout of the +/// screen. +abstract class Breakpoint { + /// Returns a const [Breakpoint]. + const Breakpoint(); + + /// A method that returns true based on conditions related to the context of + /// the screen such as MediaQuery.of(context).size.width. + bool isActive(final BuildContext context); +} diff --git a/lib/utils/extensions/text_extensions.dart b/lib/utils/extensions/text_extensions.dart index bfacc600..a00f7096 100644 --- a/lib/utils/extensions/text_extensions.dart +++ b/lib/utils/extensions/text_extensions.dart @@ -1,4 +1,4 @@ -import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; extension TextExtension on Text { Text withColor(final Color color) => Text( diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index b3e22002..6140a508 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -11,7 +11,6 @@ import dynamic_color import flutter_secure_storage_macos import package_info import path_provider_foundation -import share_plus import shared_preferences_foundation import url_launcher_macos import wakelock_macos @@ -23,7 +22,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) FLTPackageInfoPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) - SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) WakelockMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockMacosPlugin")) diff --git a/pubspec.lock b/pubspec.lock index 4ffd3c72..2b7c3254 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -17,6 +17,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.4.0" + animations: + dependency: "direct main" + description: + name: animations + sha256: fe8a6bdca435f718bb1dc8a11661b2c22504c6da40ef934cee8327ed77934164 + url: "https://pub.dev" + source: hosted + version: "2.0.7" archive: dependency: transitive description: @@ -41,6 +49,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.10.0" + auto_route: + dependency: "direct main" + description: + name: auto_route + sha256: c012d5c631f9a201f07203a7084742618a1cef5e4d9f7b1e4fde6bc6158dd134 + url: "https://pub.dev" + source: hosted + version: "6.1.0" + auto_route_generator: + dependency: "direct dev" + description: + name: auto_route_generator + sha256: a3f11c3b1e6e884d1592924f3b7212855f1c7c8791c12d3b41b87ab81fb9d3b8 + url: "https://pub.dev" + source: hosted + version: "6.0.0" auto_size_text: dependency: "direct main" description: @@ -209,14 +233,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.6.2" - cross_file: - dependency: transitive - description: - name: cross_file - sha256: "0b0036e8cccbfbe0555fd83c1d31a6f30b77a96b598b35a5d36dd41f718695e9" - url: "https://pub.dev" - source: hosted - version: "0.3.3+4" crypt: dependency: "direct main" description: @@ -245,10 +261,10 @@ packages: dependency: transitive description: name: dart_style - sha256: "7a03456c3490394c8e7665890333e91ae8a49be43542b616e414449ac358acd4" + sha256: "5be16bf1707658e4c03078d4a9b90208ded217fb02c163e207d334082412f2fb" url: "https://pub.dev" source: hosted - version: "2.2.4" + version: "2.2.5" dbus: dependency: transitive description: @@ -1045,22 +1061,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.27.7" - share_plus: - dependency: "direct main" - description: - name: share_plus - sha256: e387077716f80609bb979cd199331033326033ecd1c8f200a90c5f57b1c9f55e - url: "https://pub.dev" - source: hosted - version: "6.3.0" - share_plus_platform_interface: - dependency: transitive - description: - name: share_plus_platform_interface - sha256: "82ddd4ab9260c295e6e39612d4ff00390b9a7a21f1bb1da771e2f232d80ab8a1" - url: "https://pub.dev" - source: hosted - version: "3.2.0" shared_preferences: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 407089e8..2055494c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -8,6 +8,8 @@ environment: flutter: ">=3.7.0" dependencies: + animations: ^2.0.7 + auto_route: ^6.0.1 auto_size_text: ^3.0.0 basic_utils: ^5.4.2 crypt: ^4.2.1 @@ -44,12 +46,12 @@ dependencies: pretty_dio_logger: ^1.2.0-beta-1 provider: ^6.0.5 pub_semver: ^2.1.3 - share_plus: ^6.3.0 timezone: ^0.9.1 url_launcher: ^6.1.8 wakelock: ^0.6.2 dev_dependencies: + auto_route_generator: ^6.0.0 flutter_test: sdk: flutter build_runner: ^2.3.3 diff --git a/shell.nix b/shell.nix new file mode 100644 index 00000000..f7cbc91a --- /dev/null +++ b/shell.nix @@ -0,0 +1,35 @@ +with (import { }); + +mkShell { + buildInputs = [ + at-spi2-core.dev + clang + cmake + dart + dbus.dev + flutter + gtk3 + libdatrie + libepoxy.dev + libselinux + libsepol + libthai + libxkbcommon + libsecret + ninja + pcre + pkg-config + util-linux.dev + xorg.libXdmcp + xorg.libXtst + xorg.libX11 + + glib + jsoncpp + libgcrypt + libgpg-error + ]; + shellHook = '' + export LD_LIBRARY_PATH=${libepoxy}/lib + ''; +} diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 2d280636..4ff4b899 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -10,7 +10,6 @@ #include #include #include -#include #include void RegisterPlugins(flutter::PluginRegistry* registry) { @@ -22,8 +21,6 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); LocalAuthPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("LocalAuthPlugin")); - SharePlusWindowsPluginCApiRegisterWithRegistrar( - registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index b4df792f..ae5ef358 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -7,7 +7,6 @@ list(APPEND FLUTTER_PLUGIN_LIST dynamic_color flutter_secure_storage_windows local_auth_windows - share_plus url_launcher_windows )