From c5fa712ef0f94a0ca4ebed5506da4cc5a5d76c80 Mon Sep 17 00:00:00 2001 From: Inex Code Date: Tue, 3 May 2022 13:45:10 +0300 Subject: [PATCH 01/52] Test custom colors --- .metadata | 29 ++- analysis_options.yaml | 29 +++ lib/config/brand_theme.dart | 1 + lib/main.dart | 28 +- lib/theming/factory/app_theme_factory.dart | 74 ++++++ .../brand_tab_bar/brand_tab_bar.dart | 64 ++--- .../components/pre_styled_buttons/flash.dart | 2 +- lib/ui/pages/rootRoute.dart | 2 + .../server_details/time_zone/time_zone.dart | 2 +- linux/.gitignore | 1 + linux/CMakeLists.txt | 138 ++++++++++ linux/flutter/CMakeLists.txt | 88 +++++++ linux/flutter/generated_plugin_registrant.cc | 23 ++ linux/flutter/generated_plugin_registrant.h | 15 ++ linux/flutter/generated_plugins.cmake | 26 ++ linux/main.cc | 6 + linux/my_application.cc | 104 ++++++++ linux/my_application.h | 18 ++ pubspec.lock | 56 +++- pubspec.yaml | 5 +- windows/.gitignore | 17 ++ windows/CMakeLists.txt | 101 ++++++++ windows/flutter/CMakeLists.txt | 104 ++++++++ .../flutter/generated_plugin_registrant.cc | 20 ++ windows/flutter/generated_plugin_registrant.h | 15 ++ windows/flutter/generated_plugins.cmake | 26 ++ windows/runner/CMakeLists.txt | 32 +++ windows/runner/Runner.rc | 121 +++++++++ windows/runner/flutter_window.cpp | 61 +++++ windows/runner/flutter_window.h | 33 +++ windows/runner/main.cpp | 43 +++ windows/runner/resource.h | 16 ++ windows/runner/resources/app_icon.ico | Bin 0 -> 33772 bytes windows/runner/runner.exe.manifest | 20 ++ windows/runner/utils.cpp | 64 +++++ windows/runner/utils.h | 19 ++ windows/runner/win32_window.cpp | 245 ++++++++++++++++++ windows/runner/win32_window.h | 98 +++++++ 38 files changed, 1675 insertions(+), 71 deletions(-) create mode 100644 analysis_options.yaml create mode 100644 lib/theming/factory/app_theme_factory.dart create mode 100644 linux/.gitignore create mode 100644 linux/CMakeLists.txt create mode 100644 linux/flutter/CMakeLists.txt create mode 100644 linux/flutter/generated_plugin_registrant.cc create mode 100644 linux/flutter/generated_plugin_registrant.h create mode 100644 linux/flutter/generated_plugins.cmake create mode 100644 linux/main.cc create mode 100644 linux/my_application.cc create mode 100644 linux/my_application.h create mode 100644 windows/.gitignore create mode 100644 windows/CMakeLists.txt create mode 100644 windows/flutter/CMakeLists.txt create mode 100644 windows/flutter/generated_plugin_registrant.cc create mode 100644 windows/flutter/generated_plugin_registrant.h create mode 100644 windows/flutter/generated_plugins.cmake create mode 100644 windows/runner/CMakeLists.txt create mode 100644 windows/runner/Runner.rc create mode 100644 windows/runner/flutter_window.cpp create mode 100644 windows/runner/flutter_window.h create mode 100644 windows/runner/main.cpp create mode 100644 windows/runner/resource.h create mode 100644 windows/runner/resources/app_icon.ico create mode 100644 windows/runner/runner.exe.manifest create mode 100644 windows/runner/utils.cpp create mode 100644 windows/runner/utils.h create mode 100644 windows/runner/win32_window.cpp create mode 100644 windows/runner/win32_window.h diff --git a/.metadata b/.metadata index f0274b3e..4903dbf5 100644 --- a/.metadata +++ b/.metadata @@ -1,10 +1,33 @@ # This file tracks properties of this Flutter project. # Used by Flutter tool to assess capabilities and perform upgrades etc. # -# This file should be version controlled and should not be manually edited. +# This file should be version controlled. version: - revision: 1aafb3a8b9b0c36241c5f5b34ee914770f015818 - channel: stable + revision: 5293f3cd4427b4b48ed155e7a3852c6b3c53d94a + channel: beta project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 5293f3cd4427b4b48ed155e7a3852c6b3c53d94a + base_revision: 5293f3cd4427b4b48ed155e7a3852c6b3c53d94a + - platform: linux + create_revision: 5293f3cd4427b4b48ed155e7a3852c6b3c53d94a + base_revision: 5293f3cd4427b4b48ed155e7a3852c6b3c53d94a + - platform: windows + create_revision: 5293f3cd4427b4b48ed155e7a3852c6b3c53d94a + base_revision: 5293f3cd4427b4b48ed155e7a3852c6b3c53d94a + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 00000000..61b6c4de --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,29 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at + # https://dart-lang.github.io/linter/lints/index.html. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # 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 + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/lib/config/brand_theme.dart b/lib/config/brand_theme.dart index 23755de3..8ebc54ee 100644 --- a/lib/config/brand_theme.dart +++ b/lib/config/brand_theme.dart @@ -4,6 +4,7 @@ import 'package:selfprivacy/config/text_themes.dart'; import 'brand_colors.dart'; final lightTheme = ThemeData( + useMaterial3: true, primaryColor: BrandColors.primary, fontFamily: 'Inter', brightness: Brightness.light, diff --git a/lib/main.dart b/lib/main.dart index db3f3b96..e04d7d56 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,7 +2,9 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; 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/initializing/initializing.dart'; import 'package:selfprivacy/ui/pages/onboarding/onboarding.dart'; import 'package:selfprivacy/ui/pages/rootRoute.dart'; @@ -33,13 +35,33 @@ void main() async { await EasyLocalization.ensureInitialized(); tz.initializeTimeZones(); + final lightThemeData = await AppThemeFactory.create( + isDark: false, + fallbackColor: BrandColors.primary, + ); + final darkThemeData = await AppThemeFactory.create( + isDark: true, + fallbackColor: BrandColors.primary, + ); + BlocOverrides.runZoned( - () => runApp(Localization(child: MyApp())), + () => runApp(Localization(child: MyApp( + lightThemeData: lightThemeData, + darkThemeData: darkThemeData, + ))), blocObserver: SimpleBlocObserver(), ); } class MyApp extends StatelessWidget { + const MyApp({ + required this.lightThemeData, + required this.darkThemeData, + }); + + final ThemeData lightThemeData; + final ThemeData darkThemeData; + @override Widget build(BuildContext context) { return Localization( @@ -57,7 +79,9 @@ class MyApp extends StatelessWidget { locale: context.locale, debugShowCheckedModeBanner: false, title: 'SelfPrivacy', - theme: appSettings.isDarkModeOn ? darkTheme : lightTheme, + theme: lightThemeData, + darkTheme: darkThemeData, + themeMode: appSettings.isDarkModeOn ? ThemeMode.dark : ThemeMode.light, home: appSettings.isOnbordingShowing ? OnboardingPage(nextPage: InitializingPage()) : RootPage(), diff --git a/lib/theming/factory/app_theme_factory.dart b/lib/theming/factory/app_theme_factory.dart new file mode 100644 index 00000000..87478612 --- /dev/null +++ b/lib/theming/factory/app_theme_factory.dart @@ -0,0 +1,74 @@ +import 'dart:io'; + +import 'package:dynamic_color/dynamic_color.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:system_theme/system_theme.dart'; +import 'package:gtk_theme_fl/gtk_theme_fl.dart'; + +abstract class AppThemeFactory { + AppThemeFactory._(); + + static Future create( + {required bool isDark, required Color fallbackColor}) { + return _createAppTheme( + isDark: isDark, + fallbackColor: fallbackColor, + ); + } + + static Future _createAppTheme({ + bool isDark: false, + required Color fallbackColor, + }) async { + ColorScheme? gtkColorsScheme; + var brightness = isDark ? Brightness.dark : Brightness.light; + + final dynamicColorsScheme = await _getDynamicColors(brightness); + + if (Platform.isLinux) { + GtkThemeData themeData = await GtkThemeData.initialize(); + gtkColorsScheme = ColorScheme.fromSeed( + seedColor: Color(themeData.theme_selected_bg_color), + brightness: brightness, + background: Color(themeData.theme_bg_color), + surface: Color(themeData.theme_base_color), + ); + } + + final accentColor = await SystemAccentColor(fallbackColor) + ..load(); + + final fallbackColorScheme = ColorScheme.fromSeed( + seedColor: accentColor.accent, + brightness: brightness, + ); + + final colorScheme = dynamicColorsScheme ?? gtkColorsScheme ?? fallbackColorScheme; + + final appTypography = Typography.material2021(); + + final materialThemeData = ThemeData( + colorScheme: colorScheme, + brightness: colorScheme.brightness, + typography: appTypography, + useMaterial3: true, + appBarTheme: AppBarTheme( + elevation: 0, + backgroundColor: colorScheme.primary, + foregroundColor: colorScheme.onPrimary, + ), + ); + + return materialThemeData; + } + + static Future _getDynamicColors(Brightness brightness) { + try { + return DynamicColorPlugin.getCorePalette().then( + (corePallet) => corePallet?.toColorScheme(brightness: brightness)); + } on PlatformException { + return Future.value(null); + } + } +} diff --git a/lib/ui/components/brand_tab_bar/brand_tab_bar.dart b/lib/ui/components/brand_tab_bar/brand_tab_bar.dart index 3c544331..3b0b0dac 100644 --- a/lib/ui/components/brand_tab_bar/brand_tab_bar.dart +++ b/lib/ui/components/brand_tab_bar/brand_tab_bar.dart @@ -38,26 +38,19 @@ class _BrandTabBarState extends State { @override Widget build(BuildContext context) { - final paddingBottom = MediaQuery.of(context).padding.bottom; + return 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(), BrandIcons.menu, 3), + ], + onDestinationSelected: (index) { + widget.controller!.animateTo(index); + }, + selectedIndex: currentIndex ?? 0, + labelBehavior: NavigationDestinationLabelBehavior.onlyShowSelected, - return SizedBox( - height: paddingBottom + _kBottomTabBarHeight, - child: Container( - padding: EdgeInsets.symmetric(horizontal: 16), - color: Theme.of(context).brightness == Brightness.dark - ? BrandColors.navBackgroundDark - : BrandColors.navBackgroundLight, - child: Row( - crossAxisAlignment: CrossAxisAlignment.stretch, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - _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(), BrandIcons.menu, 3), - ], - ), - ), ); } @@ -68,36 +61,9 @@ class _BrandTabBarState extends State { var isActive = currentIndex == index; var color = isActive ? activeColor : BrandColors.inactive; - return InkWell( - onTap: () => widget.controller!.animateTo(index), - child: Padding( - padding: EdgeInsets.all(6), - child: ConstrainedBox( - constraints: BoxConstraints(minWidth: 40), - child: Column( - children: [ - Icon(iconData, color: color), - SizedBox(height: 3), - Row( - children: [ - if (isActive) ...[ - Container( - height: 5, - width: 5, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: BrandColors.red2, - ), - ), - SizedBox(width: 5), - ], - Text(label, style: TextStyle(fontSize: 9, color: color)), - ], - ) - ], - ), - ), - ), + return NavigationDestination( + icon: Icon(iconData), + label: label, ); } } diff --git a/lib/ui/components/pre_styled_buttons/flash.dart b/lib/ui/components/pre_styled_buttons/flash.dart index 5e9b1875..3888af29 100644 --- a/lib/ui/components/pre_styled_buttons/flash.dart +++ b/lib/ui/components/pre_styled_buttons/flash.dart @@ -22,7 +22,7 @@ class _BrandFlashButtonState extends State<_BrandFlashButton> ).animate(_animationController); super.initState(); - WidgetsBinding.instance!.addPostFrameCallback(_afterLayout); + WidgetsBinding.instance.addPostFrameCallback(_afterLayout); } void _afterLayout(_) { diff --git a/lib/ui/pages/rootRoute.dart b/lib/ui/pages/rootRoute.dart index 74a28880..24865688 100644 --- a/lib/ui/pages/rootRoute.dart +++ b/lib/ui/pages/rootRoute.dart @@ -1,6 +1,8 @@ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:selfprivacy/logic/api_maps/server.dart'; +import 'package:selfprivacy/ui/components/brand_icons/brand_icons.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'; 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 04f192c0..9802bbb8 100644 --- a/lib/ui/pages/server_details/time_zone/time_zone.dart +++ b/lib/ui/pages/server_details/time_zone/time_zone.dart @@ -16,7 +16,7 @@ class _SelectTimezoneState extends State { @override void initState() { - WidgetsBinding.instance!.addPostFrameCallback(_afterLayout); + WidgetsBinding.instance.addPostFrameCallback(_afterLayout); super.initState(); } diff --git a/linux/.gitignore b/linux/.gitignore new file mode 100644 index 00000000..d3896c98 --- /dev/null +++ b/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt new file mode 100644 index 00000000..cc332a28 --- /dev/null +++ b/linux/CMakeLists.txt @@ -0,0 +1,138 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.10) +project(runner LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "selfprivacy") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "pro.kherel.selfprivacy") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Load bundled libraries from the lib/ directory relative to the binary. +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Root filesystem for cross-building. +if(FLUTTER_TARGET_PLATFORM_SYSROOT) + set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +endif() + +# Define build configuration options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Define the application target. To change its name, change BINARY_NAME above, +# not the value here, or `flutter run` will no longer work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add dependency libraries. Add any application-specific dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) + +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) + install(FILES "${bundled_library}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endforeach(bundled_library) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/linux/flutter/CMakeLists.txt b/linux/flutter/CMakeLists.txt new file mode 100644 index 00000000..d5bd0164 --- /dev/null +++ b/linux/flutter/CMakeLists.txt @@ -0,0 +1,88 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 00000000..cf327b12 --- /dev/null +++ b/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,23 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include +#include +#include + +void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin"); + flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar); + g_autoptr(FlPluginRegistrar) gtk_theme_fl_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "GtkThemeFlPlugin"); + gtk_theme_fl_plugin_register_with_registrar(gtk_theme_fl_registrar); + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); +} diff --git a/linux/flutter/generated_plugin_registrant.h b/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 00000000..e0f0a47b --- /dev/null +++ b/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void fl_register_plugins(FlPluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake new file mode 100644 index 00000000..6c700a87 --- /dev/null +++ b/linux/flutter/generated_plugins.cmake @@ -0,0 +1,26 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + flutter_secure_storage_linux + gtk_theme_fl + url_launcher_linux +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/linux/main.cc b/linux/main.cc new file mode 100644 index 00000000..e7c5c543 --- /dev/null +++ b/linux/main.cc @@ -0,0 +1,6 @@ +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/linux/my_application.cc b/linux/my_application.cc new file mode 100644 index 00000000..8c470fbc --- /dev/null +++ b/linux/my_application.cc @@ -0,0 +1,104 @@ +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "selfprivacy"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } else { + gtk_window_set_title(window, "selfprivacy"); + } + + gtk_window_set_default_size(window, 1280, 720); + gtk_widget_show(GTK_WIDGET(window)); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, + "flags", G_APPLICATION_NON_UNIQUE, + nullptr)); +} diff --git a/linux/my_application.h b/linux/my_application.h new file mode 100644 index 00000000..72271d5e --- /dev/null +++ b/linux/my_application.h @@ -0,0 +1,18 @@ +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/pubspec.lock b/pubspec.lock index 681a805a..eb6c1553 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -175,7 +175,7 @@ packages: name: collection url: "https://pub.dartlang.org" source: hosted - version: "1.15.0" + version: "1.16.0" convert: dependency: transitive description: @@ -232,13 +232,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "4.0.4" + dynamic_color: + dependency: "direct main" + description: + name: dynamic_color + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.2" easy_localization: dependency: "direct main" description: name: easy_localization url: "https://pub.dartlang.org" source: hosted - version: "3.0.0" + version: "3.0.1-dev" easy_logger: dependency: transitive description: @@ -273,7 +280,7 @@ packages: name: fake_async url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.3.0" ffi: dependency: transitive description: @@ -420,6 +427,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.0" + gtk_theme_fl: + dependency: "direct main" + description: + name: gtk_theme_fl + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.1" hive: dependency: "direct main" description: @@ -496,7 +510,7 @@ packages: name: js url: "https://pub.dartlang.org" source: hosted - version: "0.6.3" + version: "0.6.4" json_annotation: dependency: "direct main" description: @@ -552,7 +566,7 @@ packages: name: material_color_utilities url: "https://pub.dartlang.org" source: hosted - version: "0.1.3" + version: "0.1.4" meta: dependency: transitive description: @@ -573,7 +587,7 @@ packages: name: modal_bottom_sheet url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.0.1" nanoid: dependency: "direct main" description: @@ -615,7 +629,7 @@ packages: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.8.0" + version: "1.8.1" path_provider: dependency: transitive description: @@ -914,7 +928,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.8.1" + version: "1.8.2" ssh_key: dependency: "direct main" description: @@ -950,6 +964,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.0" + system_theme: + dependency: "direct main" + description: + name: system_theme + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + system_theme_web: + dependency: transitive + description: + name: system_theme_web + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.2" term_glyph: dependency: transitive description: @@ -963,21 +991,21 @@ packages: name: test url: "https://pub.dartlang.org" source: hosted - version: "1.19.5" + version: "1.20.2" test_api: dependency: transitive description: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.8" + version: "0.4.9" test_core: dependency: transitive description: name: test_core url: "https://pub.dartlang.org" source: hosted - version: "0.4.9" + version: "0.4.11" timezone: dependency: "direct main" description: @@ -1068,7 +1096,7 @@ packages: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.1.1" + version: "2.1.2" vm_service: dependency: transitive description: @@ -1161,5 +1189,5 @@ packages: source: hosted version: "3.1.0" sdks: - dart: ">=2.15.0 <3.0.0" - flutter: ">=2.10.0" + dart: ">=2.17.0-0 <3.0.0" + flutter: ">=2.13.0-0.2.pre" diff --git a/pubspec.yaml b/pubspec.yaml index 29a3100c..3eb8e801 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -14,6 +14,7 @@ dependencies: cubit_form: ^2.0.1 cupertino_icons: ^1.0.4 dio: ^4.0.4 + dynamic_color: ^1.2.2 easy_localization: ^3.0.0 either_option: ^2.0.1-dev.1 equatable: ^2.0.3 @@ -24,12 +25,13 @@ dependencies: flutter_markdown: ^0.6.9 flutter_secure_storage: ^5.0.2 get_it: ^7.2.0 + gtk_theme_fl: ^0.0.1 hive: ^2.0.5 hive_flutter: ^1.1.0 ionicons: ^0.1.2 json_annotation: ^4.4.0 local_auth: ^1.1.11 - modal_bottom_sheet: ^2.0.0 + modal_bottom_sheet: ^2.0.1 nanoid: ^1.0.0 package_info: ^2.0.2 pointycastle: ^3.5.1 @@ -38,6 +40,7 @@ dependencies: rsa_encrypt: ^2.0.0 share_plus: ^3.0.5 ssh_key: ^0.7.1 + system_theme: ^2.0.0 timezone: ^0.8.0 url_launcher: ^6.0.20 wakelock: ^0.6.1+1 diff --git a/windows/.gitignore b/windows/.gitignore new file mode 100644 index 00000000..d492d0d9 --- /dev/null +++ b/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/windows/CMakeLists.txt b/windows/CMakeLists.txt new file mode 100644 index 00000000..e3ec7b3a --- /dev/null +++ b/windows/CMakeLists.txt @@ -0,0 +1,101 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(selfprivacy LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "selfprivacy") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/windows/flutter/CMakeLists.txt b/windows/flutter/CMakeLists.txt new file mode 100644 index 00000000..930d2071 --- /dev/null +++ b/windows/flutter/CMakeLists.txt @@ -0,0 +1,104 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + windows-x64 $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 00000000..37b7695e --- /dev/null +++ b/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,20 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include +#include +#include + +void RegisterPlugins(flutter::PluginRegistry* registry) { + FlutterSecureStorageWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); + SystemThemePluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("SystemThemePlugin")); + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); +} diff --git a/windows/flutter/generated_plugin_registrant.h b/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 00000000..dc139d85 --- /dev/null +++ b/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake new file mode 100644 index 00000000..a1aaa278 --- /dev/null +++ b/windows/flutter/generated_plugins.cmake @@ -0,0 +1,26 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + flutter_secure_storage_windows + system_theme + url_launcher_windows +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/windows/runner/CMakeLists.txt b/windows/runner/CMakeLists.txt new file mode 100644 index 00000000..b9e550fb --- /dev/null +++ b/windows/runner/CMakeLists.txt @@ -0,0 +1,32 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/windows/runner/Runner.rc b/windows/runner/Runner.rc new file mode 100644 index 00000000..e2d13c33 --- /dev/null +++ b/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#ifdef FLUTTER_BUILD_NUMBER +#define VERSION_AS_NUMBER FLUTTER_BUILD_NUMBER +#else +#define VERSION_AS_NUMBER 1,0,0 +#endif + +#ifdef FLUTTER_BUILD_NAME +#define VERSION_AS_STRING #FLUTTER_BUILD_NAME +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "pro.kherel" "\0" + VALUE "FileDescription", "selfprivacy" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "selfprivacy" "\0" + VALUE "LegalCopyright", "Copyright (C) 2022 pro.kherel. All rights reserved." "\0" + VALUE "OriginalFilename", "selfprivacy.exe" "\0" + VALUE "ProductName", "selfprivacy" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/windows/runner/flutter_window.cpp b/windows/runner/flutter_window.cpp new file mode 100644 index 00000000..b43b9095 --- /dev/null +++ b/windows/runner/flutter_window.cpp @@ -0,0 +1,61 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/windows/runner/flutter_window.h b/windows/runner/flutter_window.h new file mode 100644 index 00000000..6da0652f --- /dev/null +++ b/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/windows/runner/main.cpp b/windows/runner/main.cpp new file mode 100644 index 00000000..ce57a2c0 --- /dev/null +++ b/windows/runner/main.cpp @@ -0,0 +1,43 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.CreateAndShow(L"selfprivacy", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/windows/runner/resource.h b/windows/runner/resource.h new file mode 100644 index 00000000..66a65d1e --- /dev/null +++ b/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/windows/runner/resources/app_icon.ico b/windows/runner/resources/app_icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..c04e20caf6370ebb9253ad831cc31de4a9c965f6 GIT binary patch literal 33772 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 literal 0 HcmV?d00001 diff --git a/windows/runner/runner.exe.manifest b/windows/runner/runner.exe.manifest new file mode 100644 index 00000000..c977c4a4 --- /dev/null +++ b/windows/runner/runner.exe.manifest @@ -0,0 +1,20 @@ + + + + + PerMonitorV2 + + + + + + + + + + + + + + + diff --git a/windows/runner/utils.cpp b/windows/runner/utils.cpp new file mode 100644 index 00000000..f5bf9fa0 --- /dev/null +++ b/windows/runner/utils.cpp @@ -0,0 +1,64 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr); + std::string utf8_string; + if (target_length == 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, utf8_string.data(), + target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/windows/runner/utils.h b/windows/runner/utils.h new file mode 100644 index 00000000..3879d547 --- /dev/null +++ b/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/windows/runner/win32_window.cpp b/windows/runner/win32_window.cpp new file mode 100644 index 00000000..c10f08dc --- /dev/null +++ b/windows/runner/win32_window.cpp @@ -0,0 +1,245 @@ +#include "win32_window.h" + +#include + +#include "resource.h" + +namespace { + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + FreeLibrary(user32_module); + } +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::CreateAndShow(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW | WS_VISIBLE, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + return OnCreate(); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} diff --git a/windows/runner/win32_window.h b/windows/runner/win32_window.h new file mode 100644 index 00000000..17ba4311 --- /dev/null +++ b/windows/runner/win32_window.h @@ -0,0 +1,98 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates and shows a win32 window with |title| and position and size using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size to will treat the width height passed in to this function + // as logical pixels and scale to appropriate for the default monitor. Returns + // true if the window was created successfully. + bool CreateAndShow(const std::wstring& title, + const Point& origin, + const Size& size); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responsponds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_ From 9cec5e901a4522a062f23c0b378c61c720b8b915 Mon Sep 17 00:00:00 2001 From: Inex Code Date: Tue, 3 May 2022 15:18:06 +0300 Subject: [PATCH 02/52] hardcode dark theme on linux for now --- lib/theming/factory/app_theme_factory.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/theming/factory/app_theme_factory.dart b/lib/theming/factory/app_theme_factory.dart index 87478612..82407dfb 100644 --- a/lib/theming/factory/app_theme_factory.dart +++ b/lib/theming/factory/app_theme_factory.dart @@ -30,7 +30,9 @@ abstract class AppThemeFactory { GtkThemeData themeData = await GtkThemeData.initialize(); gtkColorsScheme = ColorScheme.fromSeed( seedColor: Color(themeData.theme_selected_bg_color), - brightness: brightness, + brightness: Color(themeData.theme_base_color).computeLuminance() > 0.5 + ? Brightness.light + : Brightness.dark, background: Color(themeData.theme_bg_color), surface: Color(themeData.theme_base_color), ); From 31be961dd0856345f3b7e3a642edb7c05648dfb5 Mon Sep 17 00:00:00 2001 From: NaiJi Date: Tue, 10 May 2022 02:16:36 +0300 Subject: [PATCH 03/52] Implement server endpoints for tokens get /auth/recovery_token post /auth/recovery_token post /auth/recovery_token/use post /auth/new_device/authorize post /auth/new_device delete /auth/new_device get /auth/tokens post /auth/tokens delete /auth/tokens --- lib/logic/api_maps/server.dart | 241 ++++++++++++++++++ lib/logic/models/api_token.dart | 20 ++ lib/logic/models/api_token.g.dart | 13 + lib/logic/models/device_token.dart | 17 ++ lib/logic/models/device_token.g.dart | 12 + lib/logic/models/recovery_token_status.dart | 23 ++ lib/logic/models/recovery_token_status.g.dart | 19 ++ 7 files changed, 345 insertions(+) create mode 100644 lib/logic/models/api_token.dart create mode 100644 lib/logic/models/api_token.g.dart create mode 100644 lib/logic/models/device_token.dart create mode 100644 lib/logic/models/device_token.g.dart create mode 100644 lib/logic/models/recovery_token_status.dart create mode 100644 lib/logic/models/recovery_token_status.g.dart diff --git a/lib/logic/api_maps/server.dart b/lib/logic/api_maps/server.dart index 6302611e..7554f46b 100644 --- a/lib/logic/api_maps/server.dart +++ b/lib/logic/api_maps/server.dart @@ -5,9 +5,12 @@ import 'dart:io'; import 'package:dio/dio.dart'; import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/logic/common_enum/common_enum.dart'; +import 'package:selfprivacy/logic/models/api_token.dart'; import 'package:selfprivacy/logic/models/auto_upgrade_settings.dart'; import 'package:selfprivacy/logic/models/backblaze_bucket.dart'; import 'package:selfprivacy/logic/models/backup.dart'; +import 'package:selfprivacy/logic/models/recovery_token_status.dart'; +import 'package:selfprivacy/logic/models/device_token.dart'; import 'package:selfprivacy/logic/models/timezone_settings.dart'; import 'package:selfprivacy/logic/models/user.dart'; @@ -431,6 +434,244 @@ class ServerApi extends ApiMap { .split(')')[0] .replaceAll('"', ''); } + + Future> getRecoveryTokenStatus() async { + var client = await getClient(); + Response response = await client.get( + '/auth/recovery_token', + options: Options( + contentType: 'application/json', + receiveDataWhenStatusError: true, + followRedirects: false, + validateStatus: (status) { + return (status != null) && + (status < HttpStatus.internalServerError); + }), + ); + client.close(); + + if (response.statusCode != null) { + return ApiResponse( + statusCode: response.statusCode!, + data: response.data != null + ? response.data.fromJson(response.data) + : null); + } + + return ApiResponse( + statusCode: HttpStatus.internalServerError, + data: RecoveryTokenStatus(exists: false, valid: false)); + } + + Future> generateRecoveryToken( + DateTime expiration, int uses) async { + var client = await getClient(); + Response response = await client.post( + '/auth/recovery_token', + data: { + 'expiration': expiration.toIso8601String(), + 'uses': uses, + }, + options: Options( + contentType: 'application/json', + receiveDataWhenStatusError: true, + followRedirects: false, + validateStatus: (status) { + return (status != null) && + (status < HttpStatus.internalServerError); + }), + ); + client.close(); + + if (response.statusCode != null) { + return ApiResponse( + statusCode: response.statusCode!, + data: response.data != null ? response.data["token"] : ''); + } + + return ApiResponse(statusCode: HttpStatus.internalServerError, data: ''); + } + + Future> useRecoveryToken(DeviceToken token) async { + var client = await getClient(); + Response response = await client.post( + '/auth/recovery_token/use', + data: { + 'token': token.token, + 'device': token.device, + }, + options: Options( + contentType: 'application/json', + receiveDataWhenStatusError: true, + followRedirects: false, + validateStatus: (status) { + return (status != null) && + (status < HttpStatus.internalServerError); + }), + ); + client.close(); + + if (response.statusCode != null) { + return ApiResponse( + statusCode: response.statusCode!, + data: response.data != null ? response.data["token"] : ''); + } + + return ApiResponse(statusCode: HttpStatus.internalServerError, data: ''); + } + + Future> authorizeDevice(DeviceToken token) async { + var client = await getClient(); + Response response = await client.post( + '/auth/new_device/authorize', + data: { + 'token': token.token, + 'device': token.device, + }, + options: Options( + contentType: 'application/json', + receiveDataWhenStatusError: true, + followRedirects: false, + validateStatus: (status) { + return (status != null) && + (status < HttpStatus.internalServerError); + }), + ); + client.close(); + + if (response.statusCode != null) { + return ApiResponse( + statusCode: response.statusCode!, + data: response.data != null ? response.data : ''); + } + + return ApiResponse(statusCode: HttpStatus.internalServerError, data: ''); + } + + Future> createDeviceToken() async { + var client = await getClient(); + Response response = await client.post( + '/auth/new_device', + options: Options( + contentType: 'application/json', + receiveDataWhenStatusError: true, + followRedirects: false, + validateStatus: (status) { + return (status != null) && + (status < HttpStatus.internalServerError); + }), + ); + client.close(); + + if (response.statusCode != null) { + return ApiResponse( + statusCode: response.statusCode!, + data: response.data != null ? response.data["token"] : ''); + } + + return ApiResponse(statusCode: HttpStatus.internalServerError, data: ''); + } + + Future> deleteDeviceToken() async { + var client = await getClient(); + Response response = await client.delete( + '/auth/new_device', + options: Options( + contentType: 'application/json', + receiveDataWhenStatusError: true, + followRedirects: false, + validateStatus: (status) { + return (status != null) && + (status < HttpStatus.internalServerError); + }), + ); + client.close(); + + if (response.statusCode != null) { + return ApiResponse( + statusCode: response.statusCode!, + data: response.data != null ? response.data : ''); + } + + return ApiResponse(statusCode: HttpStatus.internalServerError, data: ''); + } + + Future>> getApiTokens() async { + var client = await getClient(); + Response response = await client.get( + '/auth/tokens', + options: Options( + contentType: 'application/json', + receiveDataWhenStatusError: true, + followRedirects: false, + validateStatus: (status) { + return (status != null) && + (status < HttpStatus.internalServerError); + }), + ); + client.close(); + + if (response.statusCode != null) { + return ApiResponse( + statusCode: response.statusCode!, + data: (response.data != null) + ? response.data + .map((e) => ApiToken.fromJson(e)) + .toList() + : []); + } + + return ApiResponse(statusCode: HttpStatus.internalServerError, data: []); + } + + Future> refreshCurrentApiToken() async { + var client = await getClient(); + Response response = await client.post( + '/auth/tokens', + options: Options( + contentType: 'application/json', + receiveDataWhenStatusError: true, + followRedirects: false, + validateStatus: (status) { + return (status != null) && + (status < HttpStatus.internalServerError); + }), + ); + client.close(); + + if (response.statusCode != null) { + return ApiResponse( + statusCode: response.statusCode!, + data: (response.data != null) ? response.data["token"] : ''); + } + + return ApiResponse(statusCode: HttpStatus.internalServerError, data: ''); + } + + Future> deleteApiToken(String device) async { + var client = await getClient(); + Response response = await client.delete( + '/auth/tokens', + data: { + 'device': device, + }, + options: Options( + contentType: 'application/json', + receiveDataWhenStatusError: true, + followRedirects: false, + validateStatus: (status) { + return (status != null) && + (status < HttpStatus.internalServerError); + }), + ); + client.close(); + + if (response.statusCode != null) { + return ApiResponse(statusCode: response.statusCode!, data: null); + } + + return ApiResponse(statusCode: HttpStatus.internalServerError, data: null); + } } extension UrlServerExt on ServiceTypes { diff --git a/lib/logic/models/api_token.dart b/lib/logic/models/api_token.dart new file mode 100644 index 00000000..25556399 --- /dev/null +++ b/lib/logic/models/api_token.dart @@ -0,0 +1,20 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'api_token.g.dart'; + +@JsonSerializable() +class ApiToken { + ApiToken({ + required this.name, + required this.date, + required this.is_caller, + }); + + final String name; + final DateTime date; + final bool is_caller; + + factory ApiToken.fromJson(Map json) => + _$ApiTokenFromJson(json); +} diff --git a/lib/logic/models/api_token.g.dart b/lib/logic/models/api_token.g.dart new file mode 100644 index 00000000..c009f58b --- /dev/null +++ b/lib/logic/models/api_token.g.dart @@ -0,0 +1,13 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'api_token.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +ApiToken _$ApiTokenFromJson(Map json) => ApiToken( + name: json['name'] as String, + date: DateTime.parse(json['date'] as String), + is_caller: json['is_caller'] as bool, + ); diff --git a/lib/logic/models/device_token.dart b/lib/logic/models/device_token.dart new file mode 100644 index 00000000..d53299c5 --- /dev/null +++ b/lib/logic/models/device_token.dart @@ -0,0 +1,17 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'device_token.g.dart'; + +@JsonSerializable() +class DeviceToken { + DeviceToken({ + required this.device, + required this.token, + }); + + final String device; + final String token; + + factory DeviceToken.fromJson(Map json) => + _$DeviceTokenFromJson(json); +} diff --git a/lib/logic/models/device_token.g.dart b/lib/logic/models/device_token.g.dart new file mode 100644 index 00000000..efe976c5 --- /dev/null +++ b/lib/logic/models/device_token.g.dart @@ -0,0 +1,12 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'device_token.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +DeviceToken _$DeviceTokenFromJson(Map json) => DeviceToken( + device: json['device'] as String, + token: json['token'] as String, + ); diff --git a/lib/logic/models/recovery_token_status.dart b/lib/logic/models/recovery_token_status.dart new file mode 100644 index 00000000..9c94b41a --- /dev/null +++ b/lib/logic/models/recovery_token_status.dart @@ -0,0 +1,23 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'recovery_token_status.g.dart'; + +@JsonSerializable() +class RecoveryTokenStatus { + RecoveryTokenStatus({ + required this.exists, + required this.valid, + this.date, + this.expiration, + this.uses_left, + }); + + final bool exists; + final DateTime? date; + final DateTime? expiration; + final int? uses_left; + final bool valid; + + factory RecoveryTokenStatus.fromJson(Map json) => + _$RecoveryTokenStatusFromJson(json); +} diff --git a/lib/logic/models/recovery_token_status.g.dart b/lib/logic/models/recovery_token_status.g.dart new file mode 100644 index 00000000..3f213364 --- /dev/null +++ b/lib/logic/models/recovery_token_status.g.dart @@ -0,0 +1,19 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'recovery_token_status.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +RecoveryTokenStatus _$RecoveryTokenStatusFromJson(Map json) => + RecoveryTokenStatus( + exists: json['exists'] as bool, + valid: json['valid'] as bool, + date: + json['date'] == null ? null : DateTime.parse(json['date'] as String), + expiration: json['expiration'] == null + ? null + : DateTime.parse(json['expiration'] as String), + uses_left: json['uses_left'] as int?, + ); From ce3e046f5a971f20741e3d7877094bd91a9e5995 Mon Sep 17 00:00:00 2001 From: NaiJi Date: Tue, 10 May 2022 23:42:33 +0300 Subject: [PATCH 04/52] Improve server endpoints, add recovery page - Handle Dio error codes properly to avoid exceptions - Improve en and ru assets - Improve dns recordings failure handling - Add recovery button to initializing page - Add recovery pages group --- assets/translations/en.json | 3 +- assets/translations/ru.json | 3 +- lib/logic/api_maps/server.dart | 269 ++++++++++++++++-- .../app_config/app_config_repository.dart | 2 +- .../cubit/dns_records/dns_records_cubit.dart | 4 +- lib/main.dart | 2 +- .../not_ready_card/not_ready_card.dart | 2 +- lib/ui/pages/more/more.dart | 2 +- .../{initializing => setup}/initializing.dart | 50 +++- .../setup/recovering/recovery_domain.dart | 54 ++++ 10 files changed, 347 insertions(+), 44 deletions(-) rename lib/ui/pages/{initializing => setup}/initializing.dart (91%) create mode 100644 lib/ui/pages/setup/recovering/recovery_domain.dart diff --git a/assets/translations/en.json b/assets/translations/en.json index 73c3575e..7b184548 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -21,7 +21,8 @@ "saving": "Saving..", "nickname": "Nickname", "loading": "Loading...", - "later": "I will setup it later", + "later": "Skip to setup later", + "connect_to_existing": "Connect to existing server", "reset": "Reset", "details": "Details", "no_data": "No data", diff --git a/assets/translations/ru.json b/assets/translations/ru.json index ee085ed4..7198a040 100644 --- a/assets/translations/ru.json +++ b/assets/translations/ru.json @@ -21,7 +21,8 @@ "saving": "Сохранение…", "nickname": "Никнейм", "loading": "Загрузка", - "later": "Настрою потом", + "later": "Пропустить и настроить потом", + "connect_to_existing": "Подключиться к существующему серверу", "reset": "Сбросить", "details": "Детальная информация", "no_data": "Нет данных", diff --git a/lib/logic/api_maps/server.dart b/lib/logic/api_maps/server.dart index 7554f46b..aac1add0 100644 --- a/lib/logic/api_maps/server.dart +++ b/lib/logic/api_maps/server.dart @@ -58,7 +58,17 @@ class ServerApi extends ApiMap { var client = await getClient(); try { - response = await client.get('/services/status'); + response = await client.get( + '/services/status', + options: Options( + contentType: 'application/json', + receiveDataWhenStatusError: true, + followRedirects: false, + validateStatus: (status) { + return (status != null) && + (status < HttpStatus.internalServerError); + }), + ); res = response.statusCode == HttpStatus.ok; } catch (e) { res = false; @@ -129,7 +139,17 @@ class ServerApi extends ApiMap { Response response; var client = await getClient(); - response = await client.get('/users'); + response = await client.get( + '/users', + options: Options( + contentType: 'application/json', + receiveDataWhenStatusError: true, + followRedirects: false, + validateStatus: (status) { + return (status != null) && + (status < HttpStatus.internalServerError); + }), + ); try { for (var user in response.data) { res.add(user.toString()); @@ -155,6 +175,14 @@ class ServerApi extends ApiMap { data: { 'public_key': sshKey, }, + options: Options( + contentType: 'application/json', + receiveDataWhenStatusError: true, + followRedirects: false, + validateStatus: (status) { + return (status != null) && + (status < HttpStatus.internalServerError); + }), ); close(client); @@ -174,6 +202,14 @@ class ServerApi extends ApiMap { response = await client.put( '/services/ssh/key/send', data: {"public_key": ssh}, + options: Options( + contentType: 'application/json', + receiveDataWhenStatusError: true, + followRedirects: false, + validateStatus: (status) { + return (status != null) && + (status < HttpStatus.internalServerError); + }), ); close(client); @@ -191,7 +227,17 @@ class ServerApi extends ApiMap { Response response; var client = await getClient(); - response = await client.get('/services/ssh/keys/${user.login}'); + response = await client.get( + '/services/ssh/keys/${user.login}', + options: Options( + contentType: 'application/json', + receiveDataWhenStatusError: true, + followRedirects: false, + validateStatus: (status) { + return (status != null) && + (status < HttpStatus.internalServerError); + }), + ); try { res = (response.data as List).map((e) => e as String).toList(); } catch (e) { @@ -215,8 +261,18 @@ class ServerApi extends ApiMap { Response response; var client = await getClient(); - response = await client.delete('/services/ssh/keys/${user.login}', - data: {"public_key": sshKey}); + response = await client.delete( + '/services/ssh/keys/${user.login}', + data: {"public_key": sshKey}, + options: Options( + contentType: 'application/json', + receiveDataWhenStatusError: true, + followRedirects: false, + validateStatus: (status) { + return (status != null) && + (status < HttpStatus.internalServerError); + }), + ); close(client); return ApiResponse( @@ -237,8 +293,13 @@ class ServerApi extends ApiMap { response = await client.delete( '/users/${user.login}', options: Options( - contentType: 'application/json', - ), + contentType: 'application/json', + receiveDataWhenStatusError: true, + followRedirects: false, + validateStatus: (status) { + return (status != null) && + (status < HttpStatus.internalServerError); + }), ); res = response.statusCode == HttpStatus.ok || response.statusCode == HttpStatus.notFound; @@ -262,6 +323,14 @@ class ServerApi extends ApiMap { try { response = await client.get( '/system/configuration/apply', + options: Options( + contentType: 'application/json', + receiveDataWhenStatusError: true, + followRedirects: false, + validateStatus: (status) { + return (status != null) && + (status < HttpStatus.internalServerError); + }), ); res = response.statusCode == HttpStatus.ok; @@ -276,13 +345,33 @@ class ServerApi extends ApiMap { Future switchService(ServiceTypes type, bool needToTurnOn) async { var client = await getClient(); - client.post('/services/${type.url}/${needToTurnOn ? 'enable' : 'disable'}'); + client.post( + '/services/${type.url}/${needToTurnOn ? 'enable' : 'disable'}', + options: Options( + contentType: 'application/json', + receiveDataWhenStatusError: true, + followRedirects: false, + validateStatus: (status) { + return (status != null) && + (status < HttpStatus.internalServerError); + }), + ); close(client); } Future> servicesPowerCheck() async { var client = await getClient(); - Response response = await client.get('/services/status'); + Response response = await client.get( + '/services/status', + options: Options( + contentType: 'application/json', + receiveDataWhenStatusError: true, + followRedirects: false, + validateStatus: (status) { + return (status != null) && + (status < HttpStatus.internalServerError); + }), + ); close(client); return { @@ -303,13 +392,31 @@ class ServerApi extends ApiMap { 'accountKey': bucket.applicationKey, 'bucket': bucket.bucketName, }, + options: Options( + contentType: 'application/json', + receiveDataWhenStatusError: true, + followRedirects: false, + validateStatus: (status) { + return (status != null) && + (status < HttpStatus.internalServerError); + }), ); close(client); } Future startBackup() async { var client = await getClient(); - client.put('/services/restic/backup/create'); + client.put( + '/services/restic/backup/create', + options: Options( + contentType: 'application/json', + receiveDataWhenStatusError: true, + followRedirects: false, + validateStatus: (status) { + return (status != null) && + (status < HttpStatus.internalServerError); + }), + ); close(client); } @@ -320,6 +427,14 @@ class ServerApi extends ApiMap { try { response = await client.get( '/services/restic/backup/list', + options: Options( + contentType: 'application/json', + receiveDataWhenStatusError: true, + followRedirects: false, + validateStatus: (status) { + return (status != null) && + (status < HttpStatus.internalServerError); + }), ); return response.data.map((e) => Backup.fromJson(e)).toList(); } catch (e) { @@ -336,6 +451,14 @@ class ServerApi extends ApiMap { try { response = await client.get( '/services/restic/backup/status', + options: Options( + contentType: 'application/json', + receiveDataWhenStatusError: true, + followRedirects: false, + validateStatus: (status) { + return (status != null) && + (status < HttpStatus.internalServerError); + }), ); return BackupStatus.fromJson(response.data); } catch (e) { @@ -352,40 +475,101 @@ class ServerApi extends ApiMap { Future forceBackupListReload() async { var client = await getClient(); - client.get('/services/restic/backup/reload'); + client.get( + '/services/restic/backup/reload', + options: Options( + contentType: 'application/json', + receiveDataWhenStatusError: true, + followRedirects: false, + validateStatus: (status) { + return (status != null) && + (status < HttpStatus.internalServerError); + }), + ); close(client); } Future restoreBackup(String backupId) async { var client = await getClient(); - client.put('/services/restic/backup/restore', data: {'backupId': backupId}); + client.put( + '/services/restic/backup/restore', + data: {'backupId': backupId}, + options: Options( + contentType: 'application/json', + receiveDataWhenStatusError: true, + followRedirects: false, + validateStatus: (status) { + return (status != null) && + (status < HttpStatus.internalServerError); + }), + ); close(client); } Future pullConfigurationUpdate() async { var client = await getClient(); - Response response = await client.get('/system/configuration/pull'); + Response response = await client.get( + '/system/configuration/pull', + options: Options( + contentType: 'application/json', + receiveDataWhenStatusError: true, + followRedirects: false, + validateStatus: (status) { + return (status != null) && + (status < HttpStatus.internalServerError); + }), + ); close(client); return response.statusCode == HttpStatus.ok; } Future reboot() async { var client = await getClient(); - Response response = await client.get('/system/reboot'); + Response response = await client.get( + '/system/reboot', + options: Options( + contentType: 'application/json', + receiveDataWhenStatusError: true, + followRedirects: false, + validateStatus: (status) { + return (status != null) && + (status < HttpStatus.internalServerError); + }), + ); close(client); return response.statusCode == HttpStatus.ok; } Future upgrade() async { var client = await getClient(); - Response response = await client.get('/system/configuration/upgrade'); + Response response = await client.get( + '/system/configuration/upgrade', + options: Options( + contentType: 'application/json', + receiveDataWhenStatusError: true, + followRedirects: false, + validateStatus: (status) { + return (status != null) && + (status < HttpStatus.internalServerError); + }), + ); close(client); return response.statusCode == HttpStatus.ok; } Future getAutoUpgradeSettings() async { var client = await getClient(); - Response response = await client.get('/system/configuration/autoUpgrade'); + Response response = await client.get( + '/system/configuration/autoUpgrade', + options: Options( + contentType: 'application/json', + receiveDataWhenStatusError: true, + followRedirects: false, + validateStatus: (status) { + return (status != null) && + (status < HttpStatus.internalServerError); + }), + ); close(client); return AutoUpgradeSettings.fromJson(response.data); } @@ -395,13 +579,31 @@ class ServerApi extends ApiMap { await client.put( '/system/configuration/autoUpgrade', data: settings.toJson(), + options: Options( + contentType: 'application/json', + receiveDataWhenStatusError: true, + followRedirects: false, + validateStatus: (status) { + return (status != null) && + (status < HttpStatus.internalServerError); + }), ); close(client); } Future getServerTimezone() async { var client = await getClient(); - Response response = await client.get('/system/configuration/timezone'); + Response response = await client.get( + '/system/configuration/timezone', + options: Options( + contentType: 'application/json', + receiveDataWhenStatusError: true, + followRedirects: false, + validateStatus: (status) { + return (status != null) && + (status < HttpStatus.internalServerError); + }), + ); close(client); return TimeZoneSettings.fromString(response.data); @@ -412,20 +614,45 @@ class ServerApi extends ApiMap { await client.put( '/system/configuration/timezone', data: settings.toJson(), + options: Options( + contentType: 'application/json', + receiveDataWhenStatusError: true, + followRedirects: false, + validateStatus: (status) { + return (status != null) && + (status < HttpStatus.internalServerError); + }), ); close(client); } - Future getDkim() async { + Future getDkim() async { var client = await getClient(); - Response response = await client.get('/services/mailserver/dkim'); + Response response = await client.get( + '/services/mailserver/dkim', + options: Options( + contentType: 'application/json', + receiveDataWhenStatusError: true, + followRedirects: false, + validateStatus: (status) { + return (status != null) && + (status < HttpStatus.internalServerError); + }), + ); close(client); - // if got 404 raise exception - if (response.statusCode == HttpStatus.notFound) { + if (response.statusCode == null) { + return null; + } + + if (response.statusCode == HttpStatus.notFound || response.data == null) { throw Exception('No DKIM key found'); } + if (response.statusCode != HttpStatus.ok) { + return ""; + } + final base64toString = utf8.fuse(base64); return base64toString diff --git a/lib/logic/cubit/app_config/app_config_repository.dart b/lib/logic/cubit/app_config/app_config_repository.dart index 76a63b25..78560f14 100644 --- a/lib/logic/cubit/app_config/app_config_repository.dart +++ b/lib/logic/cubit/app_config/app_config_repository.dart @@ -206,7 +206,7 @@ class AppConfigRepository { var dkimRecordString = await api.getDkim(); - await cloudflareApi.setDkim(dkimRecordString, cloudFlareDomain); + await cloudflareApi.setDkim(dkimRecordString ?? "", cloudFlareDomain); } Future isHttpServerWorking() async { diff --git a/lib/logic/cubit/dns_records/dns_records_cubit.dart b/lib/logic/cubit/dns_records/dns_records_cubit.dart index 227ac227..de03e356 100644 --- a/lib/logic/cubit/dns_records/dns_records_cubit.dart +++ b/lib/logic/cubit/dns_records/dns_records_cubit.dart @@ -97,11 +97,11 @@ class DnsRecordsCubit extends AppConfigDependendCubit { emit(state.copyWith(dnsState: DnsRecordsStatus.refreshing)); final CloudFlareDomain? domain = appConfigCubit.state.cloudFlareDomain; final String? ipAddress = appConfigCubit.state.hetznerServer?.ip4; - final dkimPublicKey = await api.getDkim(); + final String? dkimPublicKey = await api.getDkim(); await cloudflare.removeSimilarRecords(cloudFlareDomain: domain!); await cloudflare.createMultipleDnsRecords( cloudFlareDomain: domain, ip4: ipAddress); - await cloudflare.setDkim(dkimPublicKey, domain); + await cloudflare.setDkim(dkimPublicKey ?? "", domain); await load(); } diff --git a/lib/main.dart b/lib/main.dart index e5af3656..2b98b667 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:selfprivacy/config/hive_config.dart'; -import 'package:selfprivacy/ui/pages/initializing/initializing.dart'; +import 'package:selfprivacy/ui/pages/setup/initializing.dart'; import 'package:selfprivacy/ui/pages/onboarding/onboarding.dart'; import 'package:selfprivacy/ui/pages/rootRoute.dart'; import 'package:wakelock/wakelock.dart'; 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 7d1c6cc5..a2eac28c 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:flutter/material.dart'; import 'package:selfprivacy/config/brand_colors.dart'; import 'package:selfprivacy/config/text_themes.dart'; -import 'package:selfprivacy/ui/pages/initializing/initializing.dart'; +import 'package:selfprivacy/ui/pages/setup/initializing.dart'; import 'package:selfprivacy/utils/route_transitions/basic.dart'; import 'package:easy_localization/easy_localization.dart'; diff --git a/lib/ui/pages/more/more.dart b/lib/ui/pages/more/more.dart index d87438c1..c588c007 100644 --- a/lib/ui/pages/more/more.dart +++ b/lib/ui/pages/more/more.dart @@ -7,7 +7,7 @@ import 'package:selfprivacy/ui/components/brand_divider/brand_divider.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/components/brand_text/brand_text.dart'; -import 'package:selfprivacy/ui/pages/initializing/initializing.dart'; +import 'package:selfprivacy/ui/pages/setup/initializing.dart'; import 'package:selfprivacy/ui/pages/onboarding/onboarding.dart'; import 'package:selfprivacy/ui/pages/rootRoute.dart'; import 'package:selfprivacy/ui/pages/ssh_keys/ssh_keys.dart'; diff --git a/lib/ui/pages/initializing/initializing.dart b/lib/ui/pages/setup/initializing.dart similarity index 91% rename from lib/ui/pages/initializing/initializing.dart rename to lib/ui/pages/setup/initializing.dart index d30569ca..e696e51e 100644 --- a/lib/ui/pages/initializing/initializing.dart +++ b/lib/ui/pages/setup/initializing.dart @@ -17,13 +17,14 @@ import 'package:selfprivacy/ui/components/brand_text/brand_text.dart'; import 'package:selfprivacy/ui/components/brand_timer/brand_timer.dart'; import 'package:selfprivacy/ui/components/progress_bar/progress_bar.dart'; import 'package:selfprivacy/ui/pages/rootRoute.dart'; +import 'package:selfprivacy/ui/pages/setup/recovering/recovery_domain.dart'; import 'package:selfprivacy/utils/route_transitions/basic.dart'; class InitializingPage extends StatelessWidget { @override Widget build(BuildContext context) { var cubit = context.watch(); - var actualPage = [ + var actualInitializingPage = [ () => _stepHetzner(cubit), () => _stepCloudflare(cubit), () => _stepBackblaze(cubit), @@ -69,7 +70,7 @@ class InitializingPage extends StatelessWidget { _addCard( AnimatedSwitcher( duration: Duration(milliseconds: 300), - child: actualPage, + child: actualInitializingPage, ), ), ConstrainedBox( @@ -79,19 +80,38 @@ class InitializingPage extends StatelessWidget { MediaQuery.of(context).padding.bottom - 566, ), - child: Container( - alignment: Alignment.center, - child: BrandButton.text( - title: cubit.state is AppConfigFinished - ? 'basis.close'.tr() - : 'basis.later'.tr(), - onPressed: () { - Navigator.of(context).pushAndRemoveUntil( - materialRoute(RootPage()), - (predicate) => false, - ); - }, - ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + alignment: Alignment.center, + child: BrandButton.text( + title: cubit.state is AppConfigFinished + ? 'basis.close'.tr() + : 'basis.later'.tr(), + onPressed: () { + Navigator.of(context).pushAndRemoveUntil( + materialRoute(RootPage()), + (predicate) => false, + ); + }, + ), + ), + (cubit.state is AppConfigFinished) + ? Container() + : Container( + alignment: Alignment.center, + child: BrandButton.text( + title: 'basis.connect_to_existing'.tr(), + onPressed: () { + Navigator.of(context).pushAndRemoveUntil( + materialRoute(RecoveryDomain()), + (predicate) => false, + ); + }, + ), + ) + ], )), ], ), diff --git a/lib/ui/pages/setup/recovering/recovery_domain.dart b/lib/ui/pages/setup/recovering/recovery_domain.dart new file mode 100644 index 00000000..ffa38085 --- /dev/null +++ b/lib/ui/pages/setup/recovering/recovery_domain.dart @@ -0,0 +1,54 @@ +import 'package:cubit_form/cubit_form.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart'; +import 'package:selfprivacy/ui/components/brand_button/brand_button.dart'; +import 'package:selfprivacy/ui/pages/rootRoute.dart'; +import 'package:selfprivacy/utils/route_transitions/basic.dart'; + +class RecoveryDomain extends StatelessWidget { + @override + Widget build(BuildContext context) { + var cubit = context.watch(); + return BlocListener( + listener: (context, state) { + if (cubit.state is AppConfigFinished) { + Navigator.of(context).pushReplacement(materialRoute(RootPage())); + } + }, + child: SafeArea( + child: Scaffold( + body: SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ConstrainedBox( + constraints: BoxConstraints( + minHeight: MediaQuery.of(context).size.height - + MediaQuery.of(context).padding.top - + MediaQuery.of(context).padding.bottom - + 566, + ), + child: Container( + alignment: Alignment.center, + child: BrandButton.text( + title: cubit.state is AppConfigFinished + ? 'basis.close'.tr() + : 'basis.later'.tr(), + onPressed: () { + Navigator.of(context).pushAndRemoveUntil( + materialRoute(RootPage()), + (predicate) => false, + ); + }, + ), + ), + ), + ], + ), + ), + ), + ), + ); + } +} From 01b1f7462da9911a308d6aeebf045e76dbf8f063 Mon Sep 17 00:00:00 2001 From: NaiJi Date: Wed, 11 May 2022 21:37:08 +0300 Subject: [PATCH 05/52] Implement recovery domain page frontend --- assets/translations/en.json | 7 +- lib/config/hive_config.dart | 1 + .../app_config/app_config_repository.dart | 4 + .../cubit/app_config/app_config_state.dart | 36 ++++++ .../initializing/backblaze_form_cubit.dart | 0 .../initializing/cloudflare_form_cubit.dart | 0 .../initializing/domain_cloudflare.dart | 0 .../initializing/hetzner_form_cubit.dart | 0 .../initializing/root_user_form_cubit.dart | 0 .../recovery_domain_form_cubit.dart | 51 +++++++++ lib/logic/get_it/api_config.dart | 7 ++ .../components/brand_header/brand_header.dart | 2 +- lib/ui/pages/setup/initializing.dart | 16 ++- .../setup/recovering/recovery_domain.dart | 107 +++++++++++------- 14 files changed, 177 insertions(+), 54 deletions(-) rename lib/logic/cubit/forms/{ => setup}/initializing/backblaze_form_cubit.dart (100%) rename lib/logic/cubit/forms/{ => setup}/initializing/cloudflare_form_cubit.dart (100%) rename lib/logic/cubit/forms/{ => setup}/initializing/domain_cloudflare.dart (100%) rename lib/logic/cubit/forms/{ => setup}/initializing/hetzner_form_cubit.dart (100%) rename lib/logic/cubit/forms/{ => setup}/initializing/root_user_form_cubit.dart (100%) create mode 100644 lib/logic/cubit/forms/setup/recovering/recovery_domain_form_cubit.dart diff --git a/assets/translations/en.json b/assets/translations/en.json index 7b184548..eb36af5a 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -22,7 +22,7 @@ "nickname": "Nickname", "loading": "Loading...", "later": "Skip to setup later", - "connect_to_existing": "Connect to existing server", + "connect_to_existing": "Connect to an existing server", "reset": "Reset", "details": "Details", "no_data": "No data", @@ -283,6 +283,11 @@ "finish": "Everything is initialized", "checks": "Checks have been completed \n{} ouf of {}" }, + "recovering": { + "recovery_main_header": "Connect to an existing server", + "domain_recovery_description": "Enter a server domain you want to get access for", + "domain_recover_placeholder": "Your domain" + }, "modals": { "_comment": "messages in modals", "1": "Server with such name, already exist", diff --git a/lib/config/hive_config.dart b/lib/config/hive_config.dart index e7ed84e3..9cd19459 100644 --- a/lib/config/hive_config.dart +++ b/lib/config/hive_config.dart @@ -71,4 +71,5 @@ class BNames { static String sshConfig = 'sshConfig'; static String sshPrivateKey = "sshPrivateKey"; static String sshPublicKey = "sshPublicKey"; + static String serverDomain = "serverDomain"; } diff --git a/lib/logic/cubit/app_config/app_config_repository.dart b/lib/logic/cubit/app_config/app_config_repository.dart index 78560f14..c4095cae 100644 --- a/lib/logic/cubit/app_config/app_config_repository.dart +++ b/lib/logic/cubit/app_config/app_config_repository.dart @@ -239,6 +239,10 @@ class AppConfigRepository { await getIt().storeHetznerKey(key); } + Future saveServerDomain(String domain) async { + await getIt().storeServerDomain(domain); + } + Future saveBackblazeKey(BackblazeCredential backblazeCredential) async { await getIt().storeBackblazeCredential(backblazeCredential); } diff --git a/lib/logic/cubit/app_config/app_config_state.dart b/lib/logic/cubit/app_config/app_config_state.dart index 0608d903..ac456e17 100644 --- a/lib/logic/cubit/app_config/app_config_state.dart +++ b/lib/logic/cubit/app_config/app_config_state.dart @@ -240,3 +240,39 @@ class AppConfigFinished extends AppConfigState { isServerResetedFirstTime, ]; } + +class AppRecovery extends AppConfigState { + const AppRecovery({ + required String hetznerKey, + required String cloudFlareKey, + required BackblazeCredential backblazeCredential, + required CloudFlareDomain cloudFlareDomain, + required User rootUser, + required HetznerServerDetails hetznerServer, + required bool isServerStarted, + required bool isServerResetedFirstTime, + required bool isServerResetedSecondTime, + }) : super( + hetznerKey: hetznerKey, + cloudFlareKey: cloudFlareKey, + backblazeCredential: backblazeCredential, + cloudFlareDomain: cloudFlareDomain, + rootUser: rootUser, + hetznerServer: hetznerServer, + isServerStarted: isServerStarted, + isServerResetedFirstTime: isServerResetedFirstTime, + isServerResetedSecondTime: isServerResetedSecondTime, + ); + + @override + List get props => [ + hetznerKey, + cloudFlareKey, + backblazeCredential, + cloudFlareDomain, + rootUser, + hetznerServer, + isServerStarted, + isServerResetedFirstTime, + ]; +} diff --git a/lib/logic/cubit/forms/initializing/backblaze_form_cubit.dart b/lib/logic/cubit/forms/setup/initializing/backblaze_form_cubit.dart similarity index 100% rename from lib/logic/cubit/forms/initializing/backblaze_form_cubit.dart rename to lib/logic/cubit/forms/setup/initializing/backblaze_form_cubit.dart diff --git a/lib/logic/cubit/forms/initializing/cloudflare_form_cubit.dart b/lib/logic/cubit/forms/setup/initializing/cloudflare_form_cubit.dart similarity index 100% rename from lib/logic/cubit/forms/initializing/cloudflare_form_cubit.dart rename to lib/logic/cubit/forms/setup/initializing/cloudflare_form_cubit.dart diff --git a/lib/logic/cubit/forms/initializing/domain_cloudflare.dart b/lib/logic/cubit/forms/setup/initializing/domain_cloudflare.dart similarity index 100% rename from lib/logic/cubit/forms/initializing/domain_cloudflare.dart rename to lib/logic/cubit/forms/setup/initializing/domain_cloudflare.dart diff --git a/lib/logic/cubit/forms/initializing/hetzner_form_cubit.dart b/lib/logic/cubit/forms/setup/initializing/hetzner_form_cubit.dart similarity index 100% rename from lib/logic/cubit/forms/initializing/hetzner_form_cubit.dart rename to lib/logic/cubit/forms/setup/initializing/hetzner_form_cubit.dart diff --git a/lib/logic/cubit/forms/initializing/root_user_form_cubit.dart b/lib/logic/cubit/forms/setup/initializing/root_user_form_cubit.dart similarity index 100% rename from lib/logic/cubit/forms/initializing/root_user_form_cubit.dart rename to lib/logic/cubit/forms/setup/initializing/root_user_form_cubit.dart diff --git a/lib/logic/cubit/forms/setup/recovering/recovery_domain_form_cubit.dart b/lib/logic/cubit/forms/setup/recovering/recovery_domain_form_cubit.dart new file mode 100644 index 00000000..8ab5cb88 --- /dev/null +++ b/lib/logic/cubit/forms/setup/recovering/recovery_domain_form_cubit.dart @@ -0,0 +1,51 @@ +import 'dart:async'; + +import 'package:cubit_form/cubit_form.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:selfprivacy/logic/api_maps/hetzner.dart'; +import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart'; +import 'package:selfprivacy/logic/cubit/forms/validations/validations.dart'; + +class RecoveryDomainFormCubit extends FormCubit { + RecoveryDomainFormCubit(this.initializingCubit) { + var regExp = RegExp(r"\s+|[-!$%^&*()@+|~=`{}\[\]:<>?,.\/]"); + apiKey = FieldCubit( + initalValue: '', + validations: [ + RequiredStringValidation('validations.required'.tr()), + ValidationModel( + (s) => regExp.hasMatch(s), 'validations.key_format'.tr()), + LengthStringNotEqualValidation(64) + ], + ); + + super.addFields([apiKey]); + } + + @override + FutureOr onSubmit() async { + initializingCubit.setHetznerKey(apiKey.state.value); + } + + final AppConfigCubit initializingCubit; + + late final FieldCubit apiKey; + + @override + FutureOr asyncValidation() async { + late bool isKeyValid; + HetznerApi apiClient = HetznerApi(isWithToken: false); + + try { + isKeyValid = await apiClient.isValid(apiKey.state.value); + } catch (e) { + addError(e); + } + + if (!isKeyValid) { + apiKey.setError('bad key'); + return false; + } + return true; + } +} diff --git a/lib/logic/get_it/api_config.dart b/lib/logic/get_it/api_config.dart index 1bc15eb1..5a2d622a 100644 --- a/lib/logic/get_it/api_config.dart +++ b/lib/logic/get_it/api_config.dart @@ -11,12 +11,14 @@ class ApiConfigModel { HetznerServerDetails? get hetznerServer => _hetznerServer; String? get hetznerKey => _hetznerKey; String? get cloudFlareKey => _cloudFlareKey; + String? get serverDomain => _serverDomain; BackblazeCredential? get backblazeCredential => _backblazeCredential; CloudFlareDomain? get cloudFlareDomain => _cloudFlareDomain; BackblazeBucket? get backblazeBucket => _backblazeBucket; String? _hetznerKey; String? _cloudFlareKey; + String? _serverDomain; HetznerServerDetails? _hetznerServer; BackblazeCredential? _backblazeCredential; CloudFlareDomain? _cloudFlareDomain; @@ -32,6 +34,11 @@ class ApiConfigModel { _cloudFlareKey = value; } + Future storeServerDomain(String value) async { + await _box.put(BNames.serverDomain, value); + _serverDomain = value; + } + Future storeBackblazeCredential(BackblazeCredential value) async { await _box.put(BNames.backblazeKey, value); diff --git a/lib/ui/components/brand_header/brand_header.dart b/lib/ui/components/brand_header/brand_header.dart index f9613e08..6795fdb4 100644 --- a/lib/ui/components/brand_header/brand_header.dart +++ b/lib/ui/components/brand_header/brand_header.dart @@ -6,7 +6,7 @@ import 'package:selfprivacy/ui/components/pre_styled_buttons/pre_styled_buttons. class BrandHeader extends StatelessWidget { const BrandHeader({ Key? key, - required this.title, + this.title = "", this.hasBackButton = false, this.hasFlashButton = false, }) : super(key: key); diff --git a/lib/ui/pages/setup/initializing.dart b/lib/ui/pages/setup/initializing.dart index e696e51e..d64d7c3b 100644 --- a/lib/ui/pages/setup/initializing.dart +++ b/lib/ui/pages/setup/initializing.dart @@ -4,11 +4,11 @@ import 'package:flutter/material.dart'; import 'package:selfprivacy/config/brand_theme.dart'; import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart'; import 'package:selfprivacy/logic/cubit/forms/factories/field_cubit_factory.dart'; -import 'package:selfprivacy/logic/cubit/forms/initializing/backblaze_form_cubit.dart'; -import 'package:selfprivacy/logic/cubit/forms/initializing/cloudflare_form_cubit.dart'; -import 'package:selfprivacy/logic/cubit/forms/initializing/domain_cloudflare.dart'; -import 'package:selfprivacy/logic/cubit/forms/initializing/hetzner_form_cubit.dart'; -import 'package:selfprivacy/logic/cubit/forms/initializing/root_user_form_cubit.dart'; +import 'package:selfprivacy/logic/cubit/forms/setup/initializing/backblaze_form_cubit.dart'; +import 'package:selfprivacy/logic/cubit/forms/setup/initializing/cloudflare_form_cubit.dart'; +import 'package:selfprivacy/logic/cubit/forms/setup/initializing/domain_cloudflare.dart'; +import 'package:selfprivacy/logic/cubit/forms/setup/initializing/hetzner_form_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_cards/brand_cards.dart'; @@ -104,10 +104,8 @@ class InitializingPage extends StatelessWidget { child: BrandButton.text( title: 'basis.connect_to_existing'.tr(), onPressed: () { - Navigator.of(context).pushAndRemoveUntil( - materialRoute(RecoveryDomain()), - (predicate) => false, - ); + Navigator.of(context) + .push(materialRoute(RecoveryDomain())); }, ), ) diff --git a/lib/ui/pages/setup/recovering/recovery_domain.dart b/lib/ui/pages/setup/recovering/recovery_domain.dart index ffa38085..c9546ccc 100644 --- a/lib/ui/pages/setup/recovering/recovery_domain.dart +++ b/lib/ui/pages/setup/recovering/recovery_domain.dart @@ -1,54 +1,75 @@ -import 'package:cubit_form/cubit_form.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart'; import 'package:selfprivacy/ui/components/brand_button/brand_button.dart'; -import 'package:selfprivacy/ui/pages/rootRoute.dart'; -import 'package:selfprivacy/utils/route_transitions/basic.dart'; +import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; -class RecoveryDomain extends StatelessWidget { +class RecoveryDomain extends StatefulWidget { + @override + State createState() => _RecoveryDomainState(); +} + +class _RecoveryDomainState extends State { @override Widget build(BuildContext context) { - var cubit = context.watch(); - return BlocListener( - listener: (context, state) { - if (cubit.state is AppConfigFinished) { - Navigator.of(context).pushReplacement(materialRoute(RootPage())); - } - }, - child: SafeArea( - child: Scaffold( - body: SingleChildScrollView( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - ConstrainedBox( - constraints: BoxConstraints( - minHeight: MediaQuery.of(context).size.height - - MediaQuery.of(context).padding.top - - MediaQuery.of(context).padding.bottom - - 566, - ), - child: Container( - alignment: Alignment.center, - child: BrandButton.text( - title: cubit.state is AppConfigFinished - ? 'basis.close'.tr() - : 'basis.later'.tr(), - onPressed: () { - Navigator.of(context).pushAndRemoveUntil( - materialRoute(RootPage()), - (predicate) => false, - ); - }, - ), - ), - ), - ], - ), + return BrandHeroScreen( + children: [ + TextField( + decoration: InputDecoration( + border: OutlineInputBorder(), + labelText: "recovering.domain_recover_placeholder".tr(), ), ), + SizedBox(height: 16), + BrandButton.rised( + onPressed: () {}, + text: "more.continue".tr(), + ), + ], + heroTitle: "recovering.recovery_main_header".tr(), + heroSubtitle: "recovering.domain_recovery_description".tr(), + hasBackButton: true, + hasFlashButton: false, + heroIcon: Icons.link, + ); + } +} + +/*class RecoveryDomain extends StatelessWidget { + @override + Widget build(BuildContext context) { + return SafeArea( + child: Scaffold( + appBar: PreferredSize( + preferredSize: Size.fromHeight(52), + child: BrandHeader(hasBackButton: true), + ), + body: ListView( + padding: EdgeInsets.all(16), + children: [ + Text( + "recovering.recovery_main_header".tr(), + style: Theme.of(context).textTheme.headlineMedium, + ), + SizedBox(height: 18), + Text( + "recovering.domain_recovery_description".tr(), + style: Theme.of(context).textTheme.bodyLarge, + ), + SizedBox(height: 18), + TextField( + decoration: InputDecoration( + border: OutlineInputBorder(), + labelText: "recovering.domain_recover_placeholder".tr(), + ), + ), + SizedBox(height: 18), + BrandButton.rised( + onPressed: () {}, + text: "more.continue".tr(), + ), + ], + ), ), ); } -} +}*/ \ No newline at end of file From 4a42733d319b1e54b2fd31a1f57143f9ad99056b Mon Sep 17 00:00:00 2001 From: NaiJi Date: Fri, 13 May 2022 16:57:56 +0300 Subject: [PATCH 06/52] Refactor infrastructure: cubits and endpoints Co-authored-by: Inex Code --- .../res/drawable-v21/launch_background.xml | 12 + .../app/src/main/res/values-night/styles.xml | 18 + lib/config/hive_config.dart | 14 +- lib/logic/api_maps/cloudflare.dart | 10 +- lib/logic/api_maps/hetzner.dart | 24 +- lib/logic/api_maps/server.dart | 181 +++++----- .../cubit/app_config/app_config_cubit.dart | 331 +++++++----------- .../app_config/app_config_repository.dart | 120 ++++--- .../cubit/app_config/app_config_state.dart | 147 +++++--- lib/logic/cubit/backups/backups_cubit.dart | 4 +- .../cubit/dns_records/dns_records_cubit.dart | 12 +- .../forms/factories/field_cubit_factory.dart | 9 + .../setup/initializing/domain_cloudflare.dart | 5 +- .../recovery_domain_form_cubit.dart | 53 +-- .../server_detailed_info_cubit.dart | 2 +- lib/logic/get_it/api_config.dart | 37 +- lib/logic/models/server_details.dart | 14 +- lib/logic/models/server_details.g.dart | 18 +- ...udflare_domain.dart => server_domain.dart} | 15 +- ...are_domain.g.dart => server_domain.g.dart} | 20 +- lib/ui/pages/dns_details/dns_details.dart | 2 +- .../pages/more/app_settings/app_setting.dart | 2 +- lib/ui/pages/providers/providers.dart | 2 +- .../setup/recovering/recovery_domain.dart | 83 ++--- lib/utils/ui_helpers.dart | 5 +- 25 files changed, 569 insertions(+), 571 deletions(-) create mode 100644 android/app/src/main/res/drawable-v21/launch_background.xml create mode 100644 android/app/src/main/res/values-night/styles.xml rename lib/logic/models/{cloudflare_domain.dart => server_domain.dart} (55%) rename lib/logic/models/{cloudflare_domain.g.dart => server_domain.g.dart} (66%) diff --git a/android/app/src/main/res/drawable-v21/launch_background.xml b/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 00000000..f74085f3 --- /dev/null +++ b/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/android/app/src/main/res/values-night/styles.xml b/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 00000000..3db14bb5 --- /dev/null +++ b/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/lib/config/hive_config.dart b/lib/config/hive_config.dart index 9cd19459..3eeb1219 100644 --- a/lib/config/hive_config.dart +++ b/lib/config/hive_config.dart @@ -5,7 +5,7 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:selfprivacy/logic/models/backblaze_bucket.dart'; import 'package:selfprivacy/logic/models/backblaze_credential.dart'; -import 'package:selfprivacy/logic/models/cloudflare_domain.dart'; +import 'package:selfprivacy/logic/models/server_domain.dart'; import 'package:selfprivacy/logic/models/server_details.dart'; import 'package:selfprivacy/logic/models/user.dart'; @@ -14,7 +14,7 @@ class HiveConfig { await Hive.initFlutter(); Hive.registerAdapter(UserAdapter()); Hive.registerAdapter(HetznerServerDetailsAdapter()); - Hive.registerAdapter(CloudFlareDomainAdapter()); + Hive.registerAdapter(ServerDomainAdapter()); Hive.registerAdapter(BackblazeCredentialAdapter()); Hive.registerAdapter(BackblazeBucketAdapter()); Hive.registerAdapter(HetznerDataBaseAdapter()); @@ -56,13 +56,14 @@ class BNames { static String key = 'key'; static String sshEnckey = 'sshEngkey'; - static String cloudFlareDomain = 'cloudFlareDomain'; + static String hasFinalChecked = 'hasFinalChecked'; + static String isServerStarted = 'isServerStarted'; + + static String serverDomain = 'cloudFlareDomain'; static String hetznerKey = 'hetznerKey'; static String cloudFlareKey = 'cloudFlareKey'; static String rootUser = 'rootUser'; - static String hetznerServer = 'hetznerServer'; - static String hasFinalChecked = 'hasFinalChecked'; - static String isServerStarted = 'isServerStarted'; + static String serverDetails = 'hetznerServer'; static String backblazeKey = 'backblazeKey'; static String backblazeBucket = 'backblazeBucket'; static String isLoading = 'isLoading'; @@ -71,5 +72,4 @@ class BNames { static String sshConfig = 'sshConfig'; static String sshPrivateKey = "sshPrivateKey"; static String sshPublicKey = "sshPublicKey"; - static String serverDomain = "serverDomain"; } diff --git a/lib/logic/api_maps/cloudflare.dart b/lib/logic/api_maps/cloudflare.dart index 5aaf9dc7..5b81fdba 100644 --- a/lib/logic/api_maps/cloudflare.dart +++ b/lib/logic/api_maps/cloudflare.dart @@ -3,7 +3,7 @@ import 'dart:io'; import 'package:dio/dio.dart'; import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/logic/api_maps/api_map.dart'; -import 'package:selfprivacy/logic/models/cloudflare_domain.dart'; +import 'package:selfprivacy/logic/models/server_domain.dart'; import 'package:selfprivacy/logic/models/dns_records.dart'; class CloudflareApi extends ApiMap { @@ -63,7 +63,7 @@ class CloudflareApi extends ApiMap { Future removeSimilarRecords({ String? ip4, - required CloudFlareDomain cloudFlareDomain, + required ServerDomain cloudFlareDomain, }) async { var domainName = cloudFlareDomain.domainName; var domainZoneId = cloudFlareDomain.zoneId; @@ -89,7 +89,7 @@ class CloudflareApi extends ApiMap { } Future> getDnsRecords({ - required CloudFlareDomain cloudFlareDomain, + required ServerDomain cloudFlareDomain, }) async { var domainName = cloudFlareDomain.domainName; var domainZoneId = cloudFlareDomain.zoneId; @@ -120,7 +120,7 @@ class CloudflareApi extends ApiMap { Future createMultipleDnsRecords({ String? ip4, - required CloudFlareDomain cloudFlareDomain, + required ServerDomain cloudFlareDomain, }) async { var domainName = cloudFlareDomain.domainName; var domainZoneId = cloudFlareDomain.zoneId; @@ -186,7 +186,7 @@ class CloudflareApi extends ApiMap { } Future setDkim( - String dkimRecordString, CloudFlareDomain cloudFlareDomain) async { + String dkimRecordString, ServerDomain cloudFlareDomain) async { final domainZoneId = cloudFlareDomain.zoneId; final url = '$rootAddress/zones/$domainZoneId/dns_records'; diff --git a/lib/logic/api_maps/hetzner.dart b/lib/logic/api_maps/hetzner.dart index 77afa5d8..304650f2 100644 --- a/lib/logic/api_maps/hetzner.dart +++ b/lib/logic/api_maps/hetzner.dart @@ -68,7 +68,7 @@ class HetznerApi extends ApiMap { return server == null; } - Future createVolume() async { + Future createVolume() async { var client = await getClient(); Response dbCreateResponse = await client.post( '/volumes', @@ -82,17 +82,17 @@ class HetznerApi extends ApiMap { }, ); var dbId = dbCreateResponse.data['volume']['id']; - return HetznerDataBase( + return ServerVolume( id: dbId, name: dbCreateResponse.data['volume']['name'], ); } - Future createServer({ + Future createServer({ required String cloudFlareKey, required User rootUser, required String domainName, - required HetznerDataBase dataBase, + required ServerVolume dataBase, }) async { var client = await getClient(); @@ -136,7 +136,7 @@ class HetznerApi extends ApiMap { print(serverCreateResponse.data); client.close(); - return HetznerServerDetails( + return ServerHostingDetails( id: serverCreateResponse.data['server']['id'], ip4: serverCreateResponse.data['server']['public_net']['ipv4']['ip'], createTime: DateTime.now(), @@ -189,8 +189,8 @@ class HetznerApi extends ApiMap { close(client); } - Future reset() async { - var server = getIt().hetznerServer!; + Future reset() async { + var server = getIt().serverDetails!; var client = await getClient(); await client.post('/servers/${server.id}/actions/reset'); @@ -199,8 +199,8 @@ class HetznerApi extends ApiMap { return server.copyWith(startTime: DateTime.now()); } - Future powerOn() async { - var server = getIt().hetznerServer!; + Future powerOn() async { + var server = getIt().serverDetails!; var client = await getClient(); await client.post('/servers/${server.id}/actions/poweron'); @@ -211,7 +211,7 @@ class HetznerApi extends ApiMap { Future> getMetrics( DateTime start, DateTime end, String type) async { - var hetznerServer = getIt().hetznerServer; + var hetznerServer = getIt().serverDetails; var client = await getClient(); Map queryParameters = { @@ -228,7 +228,7 @@ class HetznerApi extends ApiMap { } Future getInfo() async { - var hetznerServer = getIt().hetznerServer; + var hetznerServer = getIt().serverDetails; var client = await getClient(); Response response = await client.get('/servers/${hetznerServer!.id}'); close(client); @@ -240,7 +240,7 @@ class HetznerApi extends ApiMap { required String ip4, required String domainName, }) async { - var hetznerServer = getIt().hetznerServer; + var hetznerServer = getIt().serverDetails; var client = await getClient(); await client.post( '/servers/${hetznerServer!.id}/actions/change_dns_ptr', diff --git a/lib/logic/api_maps/server.dart b/lib/logic/api_maps/server.dart index aac1add0..74c2137f 100644 --- a/lib/logic/api_maps/server.dart +++ b/lib/logic/api_maps/server.dart @@ -33,65 +33,68 @@ class ApiResponse { class ServerApi extends ApiMap { bool hasLogger; bool isWithToken; + String? overrideDomain; - ServerApi({this.hasLogger = false, this.isWithToken = true}); + ServerApi( + {this.hasLogger = false, this.isWithToken = true, this.overrideDomain}); BaseOptions get options { var options = BaseOptions(); if (isWithToken) { - var cloudFlareDomain = getIt().cloudFlareDomain; + var cloudFlareDomain = getIt().serverDomain; var domainName = cloudFlareDomain!.domainName; - var apiToken = getIt().hetznerServer?.apiToken; + var apiToken = getIt().serverDetails?.apiToken; options = BaseOptions(baseUrl: 'https://api.$domainName', headers: { 'Authorization': 'Bearer $apiToken', }); } + if (overrideDomain != null) { + options = BaseOptions(baseUrl: 'https://api.$overrideDomain'); + } + return options; } + Future getApiVersion() async { + Response response; + + var client = await getClient(); + String? apiVersion = null; + + try { + response = await client.get('/api/version'); + apiVersion = response.data['version']; + } on DioError catch (e) { + print(e.message); + } finally { + close(client); + return apiVersion; + } + } + Future isHttpServerWorking() async { - bool res; + bool res = false; Response response; var client = await getClient(); try { - response = await client.get( - '/services/status', - options: Options( - contentType: 'application/json', - receiveDataWhenStatusError: true, - followRedirects: false, - validateStatus: (status) { - return (status != null) && - (status < HttpStatus.internalServerError); - }), - ); + response = await client.get('/services/status'); res = response.statusCode == HttpStatus.ok; - } catch (e) { - res = false; + } on DioError catch (e) { + print(e.message); + } finally { + close(client); + return res; } - close(client); - return res; } Future> createUser(User user) async { var client = await getClient(); - var makeErrorApiReponse = (int status) { - return ApiResponse( - statusCode: status, - data: User( - login: user.login, - password: user.password, - isFoundOnServer: false, - ), - ); - }; - - late Response response; + Response response; try { response = await client.post( @@ -100,68 +103,75 @@ class ServerApi extends ApiMap { 'username': user.login, 'password': user.password, }, - options: Options( - contentType: 'application/json', - receiveDataWhenStatusError: true, - followRedirects: false, - validateStatus: (status) { - return (status != null) && - (status < HttpStatus.internalServerError); - }), ); - } catch (e) { - return makeErrorApiReponse(HttpStatus.internalServerError); + } on DioError catch (e) { + print(e.message); + return ApiResponse( + errorMessage: e.error.toString(), + statusCode: e.response?.statusCode ?? HttpStatus.internalServerError, + data: User( + login: user.login, + password: user.password, + isFoundOnServer: false, + ), + ); } finally { close(client); } - if ((response.statusCode != null) && - (response.statusCode == HttpStatus.created)) { - return ApiResponse( - statusCode: response.statusCode!, - data: User( - login: user.login, - password: user.password, - isFoundOnServer: true, - ), - ); + bool isFoundOnServer = false; + int statusCode = 0; + + final bool isUserCreated = (response.statusCode != null) && + (response.statusCode == HttpStatus.created); + + if (isUserCreated) { + isFoundOnServer = true; + statusCode = response.statusCode!; } else { - print(response.statusCode.toString() + - ": " + - (response.statusMessage ?? "")); - return makeErrorApiReponse( - response.statusCode ?? HttpStatus.internalServerError); + isFoundOnServer = false; + statusCode = HttpStatus.notAcceptable; } + + return ApiResponse( + statusCode: statusCode, + data: User( + login: user.login, + password: user.password, + isFoundOnServer: isFoundOnServer, + ), + ); } Future>> getUsersList() async { List res = []; Response response; + String? message; + int code = 0; var client = await getClient(); - response = await client.get( - '/users', - options: Options( - contentType: 'application/json', - receiveDataWhenStatusError: true, - followRedirects: false, - validateStatus: (status) { - return (status != null) && - (status < HttpStatus.internalServerError); - }), - ); try { + response = await client.get('/users'); for (var user in response.data) { res.add(user.toString()); } + } on DioError catch (e) { + print(e.message); + message = e.message; + code = e.response?.statusCode ?? HttpStatus.internalServerError; + res = []; } catch (e) { print(e); + message = e.toString(); + code = HttpStatus.internalServerError; res = []; + } finally { + close(client); } - close(client); - return ApiResponse>( - statusCode: response.statusCode ?? HttpStatus.internalServerError, + return ApiResponse( + errorMessage: message, + statusCode: code, data: res, ); } @@ -170,22 +180,23 @@ class ServerApi extends ApiMap { Response response; var client = await getClient(); - response = await client.post( - '/services/ssh/keys/${user.login}', - data: { - 'public_key': sshKey, - }, - options: Options( - contentType: 'application/json', - receiveDataWhenStatusError: true, - followRedirects: false, - validateStatus: (status) { - return (status != null) && - (status < HttpStatus.internalServerError); - }), - ); + try { + response = await client.post( + '/services/ssh/keys/${user.login}', + data: { + 'public_key': sshKey, + }, + ); + } on DioError catch (e) { + print(e.message); + return ApiResponse( + data: null, + errorMessage: e.message, + statusCode: e.response?.statusCode ?? HttpStatus.internalServerError); + } finally { + close(client); + } - close(client); return ApiResponse( statusCode: response.statusCode ?? HttpStatus.internalServerError, data: null, diff --git a/lib/logic/cubit/app_config/app_config_cubit.dart b/lib/logic/cubit/app_config/app_config_cubit.dart index 69259976..030e4afe 100644 --- a/lib/logic/cubit/app_config/app_config_cubit.dart +++ b/lib/logic/cubit/app_config/app_config_cubit.dart @@ -5,7 +5,7 @@ import 'package:equatable/equatable.dart'; import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/logic/get_it/ssh.dart'; import 'package:selfprivacy/logic/models/backblaze_credential.dart'; -import 'package:selfprivacy/logic/models/cloudflare_domain.dart'; +import 'package:selfprivacy/logic/models/server_domain.dart'; import 'package:selfprivacy/logic/models/server_details.dart'; import 'package:selfprivacy/logic/models/user.dart'; @@ -15,35 +15,6 @@ export 'package:provider/provider.dart'; part 'app_config_state.dart'; -/// Initializing steps: -/// -/// The set phase. -/// 1.1. Hetzner key |setHetznerKey -/// 1.2. Cloudflare key |setCloudflareKey -/// 1.3. Backblaze Id + Key |setBackblazeKey -/// 1.4. Set Domain address |setDomain -/// 1.5. Set Root user name password |setRootUser -/// 1.6. Set Create server ans set DNS-Records |createServerAndSetDnsRecords -/// (without start) -/// -/// The check phase. -/// -/// 2.1. a. wait 60sec checkDnsAndStartServer |startServerIfDnsIsOkay -/// b. checkDns -/// c. if dns is okay start server -/// -/// 2.2. a. wait 60sec |resetServerIfServerIsOkay -/// b. checkServer -/// c. if server is ok wait 30 sec -/// d. reset server -/// -/// 2.3. a. wait 60sec |oneMoreReset() -/// d. reset server -/// -/// 2.4. a. wait 30sec |finishCheckIfServerIsOkay -/// b. checkServer -/// c. if server is okay set that fully checked - class AppConfigCubit extends Cubit { AppConfigCubit() : super(AppConfigEmpty()); @@ -55,180 +26,142 @@ class AppConfigCubit extends Cubit { if (state is AppConfigFinished) { emit(state); } else if (state is AppConfigNotFinished) { - if (state.progress == 6) { - startServerIfDnsIsOkay(state: state, isImmediate: true); - } else if (state.progress == 7) { - resetServerIfServerIsOkay(state: state, isImmediate: true); - } else if (state.progress == 8) { - oneMoreReset(state: state, isImmediate: true); - } else if (state.progress == 9) { - finishCheckIfServerIsOkay(state: state, isImmediate: true); + if (state.progress == ServerSetupProgress.serverCreated) { + startServerIfDnsIsOkay(state: state); + } else if (state.progress == ServerSetupProgress.serverStarted) { + resetServerIfServerIsOkay(state: state); + } else if (state.progress == ServerSetupProgress.serverResetedFirstTime) { + oneMoreReset(state: state); + } else if (state.progress == + ServerSetupProgress.serverResetedSecondTime) { + finishCheckIfServerIsOkay(state: state); } else { emit(state); } + } else if (state is AppConfigRecovery) { + emit(state); } else { throw 'wrong state'; } } - void startServerIfDnsIsOkay({ - AppConfigNotFinished? state, - bool isImmediate = false, - }) async { - state = state ?? this.state as AppConfigNotFinished; + void runDelayed( + void Function() work, Duration delay, AppConfigNotFinished? state) async { + final dataState = state ?? this.state as AppConfigNotFinished; - final work = () async { - emit(TimerState(dataState: state!, isLoading: true)); + emit(TimerState( + dataState: dataState, + timerStart: DateTime.now(), + duration: delay, + isLoading: false, + )); + timer = Timer(delay, work); + } - var ip4 = state.hetznerServer!.ip4; - var domainName = state.cloudFlareDomain!.domainName; + void startServerIfDnsIsOkay({AppConfigNotFinished? state}) async { + final dataState = state ?? this.state as AppConfigNotFinished; - var matches = await repository.isDnsAddressesMatch( - domainName, ip4, state.dnsMatches); + emit(TimerState(dataState: dataState, isLoading: true)); - if (matches.values.every((value) => value)) { - var server = await repository.startServer( - state.hetznerServer!, - ); - await repository.saveServerDetails(server); - await repository.saveIsServerStarted(true); + var ip4 = dataState.serverDetails!.ip4; + var domainName = dataState.serverDomain!.domainName; - emit( - state.copyWith( - isServerStarted: true, - isLoading: false, - hetznerServer: server, - ), - ); - resetServerIfServerIsOkay(); - } else { - emit( - state.copyWith( - isLoading: false, - dnsMatches: matches, - ), - ); - startServerIfDnsIsOkay(); - } - }; + var matches = await repository.isDnsAddressesMatch( + domainName, ip4, dataState.dnsMatches); - if (isImmediate) { - work(); + if (matches.values.every((value) => value)) { + var server = await repository.startServer( + dataState.serverDetails!, + ); + await repository.saveServerDetails(server); + await repository.saveIsServerStarted(true); + + emit( + dataState.copyWith( + isServerStarted: true, + isLoading: false, + serverDetails: server, + ), + ); + runDelayed(resetServerIfServerIsOkay, Duration(seconds: 60), dataState); } else { - var pauseDuration = Duration(seconds: 30); - emit(TimerState( - dataState: state, - timerStart: DateTime.now(), - duration: pauseDuration, - isLoading: false, - )); - timer = Timer(pauseDuration, work); + emit( + dataState.copyWith( + isLoading: false, + dnsMatches: matches, + ), + ); + runDelayed(startServerIfDnsIsOkay, Duration(seconds: 30), dataState); } } - void oneMoreReset({ - AppConfigNotFinished? state, - bool isImmediate = false, - }) async { - var dataState = state ?? this.state as AppConfigNotFinished; + void oneMoreReset({AppConfigNotFinished? state}) async { + final dataState = state ?? this.state as AppConfigNotFinished; - var work = () async { - emit(TimerState(dataState: dataState, isLoading: true)); + emit(TimerState(dataState: dataState, isLoading: true)); - var isServerWorking = await repository.isHttpServerWorking(); + var isServerWorking = await repository.isHttpServerWorking(); - if (isServerWorking) { - var pauseDuration = Duration(seconds: 30); - emit(TimerState( - dataState: dataState, - timerStart: DateTime.now(), - isLoading: false, - duration: pauseDuration, - )); - timer = Timer(pauseDuration, () async { - var hetznerServerDetails = await repository.restart(); - await repository.saveIsServerResetedSecondTime(true); - await repository.saveServerDetails(hetznerServerDetails); + if (isServerWorking) { + var pauseDuration = Duration(seconds: 30); + emit(TimerState( + dataState: dataState, + timerStart: DateTime.now(), + isLoading: false, + duration: pauseDuration, + )); + timer = Timer(pauseDuration, () async { + var hetznerServerDetails = await repository.restart(); + await repository.saveIsServerResetedSecondTime(true); + await repository.saveServerDetails(hetznerServerDetails); - emit( - dataState.copyWith( - isServerResetedSecondTime: true, - hetznerServer: hetznerServerDetails, - isLoading: false, - ), - ); - finishCheckIfServerIsOkay(); - }); - } else { - oneMoreReset(); - } - }; - if (isImmediate) { - work(); + emit( + dataState.copyWith( + isServerResetedSecondTime: true, + serverDetails: hetznerServerDetails, + isLoading: false, + ), + ); + runDelayed(finishCheckIfServerIsOkay, Duration(seconds: 60), dataState); + }); } else { - var pauseDuration = Duration(seconds: 60); - emit( - TimerState( - dataState: dataState, - timerStart: DateTime.now(), - duration: pauseDuration, - isLoading: false, - ), - ); - timer = Timer(pauseDuration, work); + runDelayed(oneMoreReset, Duration(seconds: 60), dataState); } } void resetServerIfServerIsOkay({ AppConfigNotFinished? state, - bool isImmediate = false, }) async { - var dataState = state ?? this.state as AppConfigNotFinished; + final dataState = state ?? this.state as AppConfigNotFinished; - var work = () async { - emit(TimerState(dataState: dataState, isLoading: true)); + emit(TimerState(dataState: dataState, isLoading: true)); - var isServerWorking = await repository.isHttpServerWorking(); + var isServerWorking = await repository.isHttpServerWorking(); - if (isServerWorking) { - var pauseDuration = Duration(seconds: 30); - emit(TimerState( - dataState: dataState, - timerStart: DateTime.now(), - isLoading: false, - duration: pauseDuration, - )); - timer = Timer(pauseDuration, () async { - var hetznerServerDetails = await repository.restart(); - await repository.saveIsServerResetedFirstTime(true); - await repository.saveServerDetails(hetznerServerDetails); + if (isServerWorking) { + var pauseDuration = Duration(seconds: 30); + emit(TimerState( + dataState: dataState, + timerStart: DateTime.now(), + isLoading: false, + duration: pauseDuration, + )); + timer = Timer(pauseDuration, () async { + var hetznerServerDetails = await repository.restart(); + await repository.saveIsServerResetedFirstTime(true); + await repository.saveServerDetails(hetznerServerDetails); - emit( - dataState.copyWith( - isServerResetedFirstTime: true, - hetznerServer: hetznerServerDetails, - isLoading: false, - ), - ); - oneMoreReset(); - }); - } else { - resetServerIfServerIsOkay(); - } - }; - if (isImmediate) { - work(); + emit( + dataState.copyWith( + isServerResetedFirstTime: true, + serverDetails: hetznerServerDetails, + isLoading: false, + ), + ); + runDelayed(oneMoreReset, Duration(seconds: 60), dataState); + }); } else { - var pauseDuration = Duration(seconds: 60); - emit( - TimerState( - dataState: dataState, - timerStart: DateTime.now(), - duration: pauseDuration, - isLoading: false, - ), - ); - timer = Timer(pauseDuration, work); + runDelayed(resetServerIfServerIsOkay, Duration(seconds: 60), dataState); } } @@ -236,37 +169,20 @@ class AppConfigCubit extends Cubit { void finishCheckIfServerIsOkay({ AppConfigNotFinished? state, - bool isImmediate = false, }) async { - state = state ?? this.state as AppConfigNotFinished; + final dataState = state ?? this.state as AppConfigNotFinished; - var work = () async { - emit(TimerState(dataState: state!, isLoading: true)); + emit(TimerState(dataState: dataState, isLoading: true)); - var isServerWorking = await repository.isHttpServerWorking(); + var isServerWorking = await repository.isHttpServerWorking(); - if (isServerWorking) { - await repository.createDkimRecord(state.cloudFlareDomain!); - await repository.saveHasFinalChecked(true); + if (isServerWorking) { + await repository.createDkimRecord(dataState.serverDomain!); + await repository.saveHasFinalChecked(true); - emit(state.finish()); - } else { - finishCheckIfServerIsOkay(); - } - }; - if (isImmediate) { - work(); + emit(dataState.finish()); } else { - var pauseDuration = Duration(seconds: 60); - emit( - TimerState( - dataState: state, - timerStart: DateTime.now(), - duration: pauseDuration, - isLoading: false, - ), - ); - timer = Timer(pauseDuration, work); + runDelayed(finishCheckIfServerIsOkay, Duration(seconds: 60), dataState); } } @@ -280,18 +196,18 @@ class AppConfigCubit extends Cubit { Future serverDelete() async { closeTimer(); - if (state.hetznerServer != null) { - await repository.deleteServer(state.cloudFlareDomain!); + if (state.serverDetails != null) { + await repository.deleteServer(state.serverDomain!); await getIt().clear(); } await repository.deleteRecords(); emit(AppConfigNotFinished( hetznerKey: state.hetznerKey, - cloudFlareDomain: state.cloudFlareDomain, + serverDomain: state.serverDomain, cloudFlareKey: state.cloudFlareKey, backblazeCredential: state.backblazeCredential, rootUser: state.rootUser, - hetznerServer: null, + serverDetails: null, isServerStarted: false, isServerResetedFirstTime: false, isServerResetedSecondTime: false, @@ -321,10 +237,9 @@ class AppConfigCubit extends Cubit { .copyWith(backblazeCredential: backblazeCredential)); } - void setDomain(CloudFlareDomain cloudFlareDomain) async { - await repository.saveDomain(cloudFlareDomain); - emit((state as AppConfigNotFinished) - .copyWith(cloudFlareDomain: cloudFlareDomain)); + void setDomain(ServerDomain serverDomain) async { + await repository.saveDomain(serverDomain); + emit((state as AppConfigNotFinished).copyWith(serverDomain: serverDomain)); } void setRootUser(User rootUser) async { @@ -334,17 +249,17 @@ class AppConfigCubit extends Cubit { void createServerAndSetDnsRecords() async { AppConfigNotFinished _stateCopy = state as AppConfigNotFinished; - var onSuccess = (HetznerServerDetails serverDetails) async { + var onSuccess = (ServerHostingDetails serverDetails) async { await repository.createDnsRecords( serverDetails.ip4, - state.cloudFlareDomain!, + state.serverDomain!, ); emit((state as AppConfigNotFinished).copyWith( isLoading: false, - hetznerServer: serverDetails, + serverDetails: serverDetails, )); - startServerIfDnsIsOkay(); + runDelayed(startServerIfDnsIsOkay, Duration(seconds: 30), null); }; var onCancel = @@ -354,7 +269,7 @@ class AppConfigCubit extends Cubit { emit((state as AppConfigNotFinished).copyWith(isLoading: true)); await repository.createServer( state.rootUser!, - state.cloudFlareDomain!.domainName, + state.serverDomain!.domainName, state.cloudFlareKey!, state.backblazeCredential!, onCancel: onCancel, diff --git a/lib/logic/cubit/app_config/app_config_repository.dart b/lib/logic/cubit/app_config/app_config_repository.dart index c4095cae..363f1134 100644 --- a/lib/logic/cubit/app_config/app_config_repository.dart +++ b/lib/logic/cubit/app_config/app_config_repository.dart @@ -8,7 +8,7 @@ import 'package:selfprivacy/logic/api_maps/cloudflare.dart'; import 'package:selfprivacy/logic/api_maps/hetzner.dart'; import 'package:selfprivacy/logic/api_maps/server.dart'; import 'package:selfprivacy/logic/models/backblaze_credential.dart'; -import 'package:selfprivacy/logic/models/cloudflare_domain.dart'; +import 'package:selfprivacy/logic/models/server_domain.dart'; import 'package:selfprivacy/logic/models/message.dart'; import 'package:selfprivacy/logic/models/server_details.dart'; import 'package:selfprivacy/logic/models/user.dart'; @@ -21,14 +21,19 @@ class AppConfigRepository { Box box = Hive.box(BNames.appConfig); Future load() async { - late AppConfigState res; + final hetznerToken = getIt().hetznerKey; + final cloudflareToken = getIt().cloudFlareKey; + final serverDomain = getIt().serverDomain; + final backblazeCredential = getIt().backblazeCredential; + final serverDetails = getIt().serverDetails; + if (box.get(BNames.hasFinalChecked, defaultValue: false)) { - res = AppConfigFinished( - hetznerKey: getIt().hetznerKey!, - cloudFlareKey: getIt().cloudFlareKey!, - cloudFlareDomain: getIt().cloudFlareDomain!, - backblazeCredential: getIt().backblazeCredential!, - hetznerServer: getIt().hetznerServer!, + return AppConfigFinished( + hetznerKey: hetznerToken!, + cloudFlareKey: cloudflareToken!, + serverDomain: serverDomain!, + backblazeCredential: backblazeCredential!, + serverDetails: serverDetails!, rootUser: box.get(BNames.rootUser), isServerStarted: box.get(BNames.isServerStarted, defaultValue: false), isServerResetedFirstTime: @@ -36,33 +41,62 @@ class AppConfigRepository { isServerResetedSecondTime: box.get(BNames.isServerResetedSecondTime, defaultValue: false), ); - } else { - res = AppConfigNotFinished( - hetznerKey: getIt().hetznerKey, - cloudFlareKey: getIt().cloudFlareKey, - cloudFlareDomain: getIt().cloudFlareDomain, - backblazeCredential: getIt().backblazeCredential, - hetznerServer: getIt().hetznerServer, - rootUser: box.get(BNames.rootUser), - isServerStarted: box.get(BNames.isServerStarted, defaultValue: false), - isServerResetedFirstTime: - box.get(BNames.isServerResetedFirstTime, defaultValue: false), - isServerResetedSecondTime: - box.get(BNames.isServerResetedSecondTime, defaultValue: false), - isLoading: box.get(BNames.isLoading, defaultValue: false), - dnsMatches: null, - ); } - return res; + if (getIt().serverDomain?.provider == DnsProvider.Unknown) { + return AppConfigRecovery( + hetznerKey: hetznerToken, + cloudFlareKey: cloudflareToken, + serverDomain: serverDomain, + backblazeCredential: backblazeCredential, + serverDetails: serverDetails, + rootUser: box.get(BNames.rootUser), + currentStep: getCurrentRecoveryStep( + hetznerToken, cloudflareToken, serverDomain!, serverDetails), + ); + } + + return AppConfigNotFinished( + hetznerKey: hetznerToken, + cloudFlareKey: cloudflareToken, + serverDomain: serverDomain, + backblazeCredential: backblazeCredential, + serverDetails: serverDetails, + rootUser: box.get(BNames.rootUser), + isServerStarted: box.get(BNames.isServerStarted, defaultValue: false), + isServerResetedFirstTime: + box.get(BNames.isServerResetedFirstTime, defaultValue: false), + isServerResetedSecondTime: + box.get(BNames.isServerResetedSecondTime, defaultValue: false), + isLoading: box.get(BNames.isLoading, defaultValue: false), + dnsMatches: null, + ); + } + + RecoveryStep getCurrentRecoveryStep( + String? hetznerToken, + String? cloudflareToken, + ServerDomain serverDomain, + ServerHostingDetails? serverDetails, + ) { + if (serverDetails != null) { + if (hetznerToken != null) { + if (cloudflareToken != null) { + return RecoveryStep.BackblazeToken; + } + return RecoveryStep.CloudflareToken; + } + return RecoveryStep.HetznerToken; + } + return RecoveryStep.Selecting; } void clearAppConfig() { box.clear(); } - Future startServer( - HetznerServerDetails hetznerServer, + Future startServer( + ServerHostingDetails hetznerServer, ) async { var hetznerApi = HetznerApi(); var serverDetails = await hetznerApi.powerOn(); @@ -122,11 +156,11 @@ class AppConfigRepository { String cloudFlareKey, BackblazeCredential backblazeCredential, { required void Function() onCancel, - required Future Function(HetznerServerDetails serverDetails) + required Future Function(ServerHostingDetails serverDetails) onSuccess, }) async { var hetznerApi = HetznerApi(); - late HetznerDataBase dataBase; + late ServerVolume dataBase; try { dataBase = await hetznerApi.createVolume(); @@ -180,7 +214,7 @@ class AppConfigRepository { Future createDnsRecords( String ip4, - CloudFlareDomain cloudFlareDomain, + ServerDomain cloudFlareDomain, ) async { var cloudflareApi = CloudflareApi(); @@ -200,7 +234,7 @@ class AppConfigRepository { ); } - Future createDkimRecord(CloudFlareDomain cloudFlareDomain) async { + Future createDkimRecord(ServerDomain cloudFlareDomain) async { var cloudflareApi = CloudflareApi(); var api = ServerApi(); @@ -220,17 +254,17 @@ class AppConfigRepository { return isHttpServerWorking; } - Future restart() async { + Future restart() async { var hetznerApi = HetznerApi(); return await hetznerApi.reset(); } - Future powerOn() async { + Future powerOn() async { var hetznerApi = HetznerApi(); return await hetznerApi.powerOn(); } - Future saveServerDetails(HetznerServerDetails serverDetails) async { + Future saveServerDetails(ServerHostingDetails serverDetails) async { await getIt().storeServerDetails(serverDetails); } @@ -239,10 +273,6 @@ class AppConfigRepository { await getIt().storeHetznerKey(key); } - Future saveServerDomain(String domain) async { - await getIt().storeServerDomain(domain); - } - Future saveBackblazeKey(BackblazeCredential backblazeCredential) async { await getIt().storeBackblazeCredential(backblazeCredential); } @@ -251,8 +281,8 @@ class AppConfigRepository { await getIt().storeCloudFlareKey(key); } - Future saveDomain(CloudFlareDomain cloudFlareDomain) async { - await getIt().storeCloudFlareDomain(cloudFlareDomain); + Future saveDomain(ServerDomain serverDomain) async { + await getIt().storeServerDomain(serverDomain); } Future saveIsServerStarted(bool value) async { @@ -275,12 +305,12 @@ class AppConfigRepository { await box.put(BNames.hasFinalChecked, value); } - Future deleteServer(CloudFlareDomain cloudFlareDomain) async { + Future deleteServer(ServerDomain serverDomain) async { var hetznerApi = HetznerApi(); var cloudFlare = CloudflareApi(); await hetznerApi.deleteSelfprivacyServerAndAllVolumes( - domainName: cloudFlareDomain.domainName, + domainName: serverDomain.domainName, ); await box.put(BNames.hasFinalChecked, false); @@ -288,14 +318,14 @@ class AppConfigRepository { await box.put(BNames.isServerResetedFirstTime, false); await box.put(BNames.isServerResetedSecondTime, false); await box.put(BNames.isLoading, false); - await box.put(BNames.hetznerServer, null); + await box.put(BNames.serverDetails, null); - await cloudFlare.removeSimilarRecords(cloudFlareDomain: cloudFlareDomain); + await cloudFlare.removeSimilarRecords(cloudFlareDomain: serverDomain); } Future deleteRecords() async { await box.deleteAll([ - BNames.hetznerServer, + BNames.serverDetails, BNames.isServerStarted, BNames.isServerResetedFirstTime, BNames.isServerResetedSecondTime, diff --git a/lib/logic/cubit/app_config/app_config_state.dart b/lib/logic/cubit/app_config/app_config_state.dart index ac456e17..9d620e9e 100644 --- a/lib/logic/cubit/app_config/app_config_state.dart +++ b/lib/logic/cubit/app_config/app_config_state.dart @@ -5,9 +5,9 @@ abstract class AppConfigState extends Equatable { required this.hetznerKey, required this.cloudFlareKey, required this.backblazeCredential, - required this.cloudFlareDomain, + required this.serverDomain, required this.rootUser, - required this.hetznerServer, + required this.serverDetails, required this.isServerStarted, required this.isServerResetedFirstTime, required this.isServerResetedSecondTime, @@ -18,9 +18,9 @@ abstract class AppConfigState extends Equatable { hetznerKey, cloudFlareKey, backblazeCredential, - cloudFlareDomain, + serverDomain, rootUser, - hetznerServer, + serverDetails, isServerStarted, isServerResetedFirstTime, ]; @@ -28,9 +28,9 @@ abstract class AppConfigState extends Equatable { final String? hetznerKey; final String? cloudFlareKey; final BackblazeCredential? backblazeCredential; - final CloudFlareDomain? cloudFlareDomain; + final ServerDomain? serverDomain; final User? rootUser; - final HetznerServerDetails? hetznerServer; + final ServerHostingDetails? serverDetails; final bool isServerStarted; final bool isServerResetedFirstTime; final bool isServerResetedSecondTime; @@ -38,17 +38,18 @@ abstract class AppConfigState extends Equatable { bool get isHetznerFilled => hetznerKey != null; bool get isCloudFlareFilled => cloudFlareKey != null; bool get isBackblazeFilled => backblazeCredential != null; - bool get isDomainFilled => cloudFlareDomain != null; + bool get isDomainFilled => serverDomain != null; bool get isUserFilled => rootUser != null; - bool get isServerCreated => hetznerServer != null; + bool get isServerCreated => serverDetails != null; bool get isFullyInitilized => _fulfilementList.every((el) => el!); - int get progress => _fulfilementList.where((el) => el!).length; + ServerSetupProgress get progress => + ServerSetupProgress.values[_fulfilementList.where((el) => el!).length]; int get porgressBar { - if (progress < 6) { - return progress; - } else if (progress < 10) { + if (progress.index < 6) { + return progress.index; + } else if (progress.index < 10) { return 6; } else { return 7; @@ -82,9 +83,9 @@ class TimerState extends AppConfigNotFinished { hetznerKey: dataState.hetznerKey, cloudFlareKey: dataState.cloudFlareKey, backblazeCredential: dataState.backblazeCredential, - cloudFlareDomain: dataState.cloudFlareDomain, + serverDomain: dataState.serverDomain, rootUser: dataState.rootUser, - hetznerServer: dataState.hetznerServer, + serverDetails: dataState.serverDetails, isServerStarted: dataState.isServerStarted, isServerResetedFirstTime: dataState.isServerResetedFirstTime, isServerResetedSecondTime: dataState.isServerResetedSecondTime, @@ -104,6 +105,19 @@ class TimerState extends AppConfigNotFinished { ]; } +enum ServerSetupProgress { + nothingYet, + hetznerFilled, + cloudFlareFilled, + backblazeFilled, + domainFilled, + userFilled, + serverCreated, + serverStarted, + serverResetedFirstTime, + serverResetedSecondTime, +} + class AppConfigNotFinished extends AppConfigState { final bool isLoading; final Map? dnsMatches; @@ -112,9 +126,9 @@ class AppConfigNotFinished extends AppConfigState { String? hetznerKey, String? cloudFlareKey, BackblazeCredential? backblazeCredential, - CloudFlareDomain? cloudFlareDomain, + ServerDomain? serverDomain, User? rootUser, - HetznerServerDetails? hetznerServer, + ServerHostingDetails? serverDetails, required bool isServerStarted, required bool isServerResetedFirstTime, required bool isServerResetedSecondTime, @@ -124,9 +138,9 @@ class AppConfigNotFinished extends AppConfigState { hetznerKey: hetznerKey, cloudFlareKey: cloudFlareKey, backblazeCredential: backblazeCredential, - cloudFlareDomain: cloudFlareDomain, + serverDomain: serverDomain, rootUser: rootUser, - hetznerServer: hetznerServer, + serverDetails: serverDetails, isServerStarted: isServerStarted, isServerResetedFirstTime: isServerResetedFirstTime, isServerResetedSecondTime: isServerResetedSecondTime, @@ -137,9 +151,9 @@ class AppConfigNotFinished extends AppConfigState { hetznerKey, cloudFlareKey, backblazeCredential, - cloudFlareDomain, + serverDomain, rootUser, - hetznerServer, + serverDetails, isServerStarted, isServerResetedFirstTime, isLoading, @@ -150,9 +164,9 @@ class AppConfigNotFinished extends AppConfigState { String? hetznerKey, String? cloudFlareKey, BackblazeCredential? backblazeCredential, - CloudFlareDomain? cloudFlareDomain, + ServerDomain? serverDomain, User? rootUser, - HetznerServerDetails? hetznerServer, + ServerHostingDetails? serverDetails, bool? isServerStarted, bool? isServerResetedFirstTime, bool? isServerResetedSecondTime, @@ -163,9 +177,9 @@ class AppConfigNotFinished extends AppConfigState { hetznerKey: hetznerKey ?? this.hetznerKey, cloudFlareKey: cloudFlareKey ?? this.cloudFlareKey, backblazeCredential: backblazeCredential ?? this.backblazeCredential, - cloudFlareDomain: cloudFlareDomain ?? this.cloudFlareDomain, + serverDomain: serverDomain ?? this.serverDomain, rootUser: rootUser ?? this.rootUser, - hetznerServer: hetznerServer ?? this.hetznerServer, + serverDetails: serverDetails ?? this.serverDetails, isServerStarted: isServerStarted ?? this.isServerStarted, isServerResetedFirstTime: isServerResetedFirstTime ?? this.isServerResetedFirstTime, @@ -179,9 +193,9 @@ class AppConfigNotFinished extends AppConfigState { hetznerKey: hetznerKey!, cloudFlareKey: cloudFlareKey!, backblazeCredential: backblazeCredential!, - cloudFlareDomain: cloudFlareDomain!, + serverDomain: serverDomain!, rootUser: rootUser!, - hetznerServer: hetznerServer!, + serverDetails: serverDetails!, isServerStarted: isServerStarted, isServerResetedFirstTime: isServerResetedFirstTime, isServerResetedSecondTime: isServerResetedSecondTime, @@ -194,9 +208,9 @@ class AppConfigEmpty extends AppConfigNotFinished { hetznerKey: null, cloudFlareKey: null, backblazeCredential: null, - cloudFlareDomain: null, + serverDomain: null, rootUser: null, - hetznerServer: null, + serverDetails: null, isServerStarted: false, isServerResetedFirstTime: false, isServerResetedSecondTime: false, @@ -210,9 +224,9 @@ class AppConfigFinished extends AppConfigState { required String hetznerKey, required String cloudFlareKey, required BackblazeCredential backblazeCredential, - required CloudFlareDomain cloudFlareDomain, + required ServerDomain serverDomain, required User rootUser, - required HetznerServerDetails hetznerServer, + required ServerHostingDetails serverDetails, required bool isServerStarted, required bool isServerResetedFirstTime, required bool isServerResetedSecondTime, @@ -220,9 +234,9 @@ class AppConfigFinished extends AppConfigState { hetznerKey: hetznerKey, cloudFlareKey: cloudFlareKey, backblazeCredential: backblazeCredential, - cloudFlareDomain: cloudFlareDomain, + serverDomain: serverDomain, rootUser: rootUser, - hetznerServer: hetznerServer, + serverDetails: serverDetails, isServerStarted: isServerStarted, isServerResetedFirstTime: isServerResetedFirstTime, isServerResetedSecondTime: isServerResetedSecondTime, @@ -233,35 +247,45 @@ class AppConfigFinished extends AppConfigState { hetznerKey, cloudFlareKey, backblazeCredential, - cloudFlareDomain, + serverDomain, rootUser, - hetznerServer, + serverDetails, isServerStarted, isServerResetedFirstTime, ]; } -class AppRecovery extends AppConfigState { - const AppRecovery({ - required String hetznerKey, - required String cloudFlareKey, - required BackblazeCredential backblazeCredential, - required CloudFlareDomain cloudFlareDomain, - required User rootUser, - required HetznerServerDetails hetznerServer, - required bool isServerStarted, - required bool isServerResetedFirstTime, - required bool isServerResetedSecondTime, +enum RecoveryStep { + Selecting, + RecoveryKey, + NewDeviceKey, + OldToken, + HetznerToken, + CloudflareToken, + BackblazeToken, +} + +class AppConfigRecovery extends AppConfigState { + final RecoveryStep currentStep; + + const AppConfigRecovery({ + String? hetznerKey, + String? cloudFlareKey, + BackblazeCredential? backblazeCredential, + ServerDomain? serverDomain, + User? rootUser, + ServerHostingDetails? serverDetails, + required RecoveryStep this.currentStep, }) : super( hetznerKey: hetznerKey, cloudFlareKey: cloudFlareKey, backblazeCredential: backblazeCredential, - cloudFlareDomain: cloudFlareDomain, + serverDomain: serverDomain, rootUser: rootUser, - hetznerServer: hetznerServer, - isServerStarted: isServerStarted, - isServerResetedFirstTime: isServerResetedFirstTime, - isServerResetedSecondTime: isServerResetedSecondTime, + serverDetails: serverDetails, + isServerStarted: true, + isServerResetedFirstTime: true, + isServerResetedSecondTime: true, ); @override @@ -269,10 +293,29 @@ class AppRecovery extends AppConfigState { hetznerKey, cloudFlareKey, backblazeCredential, - cloudFlareDomain, + serverDomain, rootUser, - hetznerServer, + serverDetails, isServerStarted, isServerResetedFirstTime, + currentStep ]; + + AppConfigRecovery copyWith({ + String? hetznerKey, + String? cloudFlareKey, + BackblazeCredential? backblazeCredential, + ServerDomain? serverDomain, + User? rootUser, + ServerHostingDetails? serverDetails, + RecoveryStep? currentStep, + }) => + AppConfigRecovery( + hetznerKey: hetznerKey ?? this.hetznerKey, + cloudFlareKey: cloudFlareKey ?? this.cloudFlareKey, + backblazeCredential: backblazeCredential ?? this.backblazeCredential, + serverDomain: serverDomain ?? this.serverDomain, + rootUser: rootUser ?? this.rootUser, + serverDetails: serverDetails ?? this.serverDetails, + currentStep: currentStep ?? this.currentStep); } diff --git a/lib/logic/cubit/backups/backups_cubit.dart b/lib/logic/cubit/backups/backups_cubit.dart index e79978c2..a867dc87 100644 --- a/lib/logic/cubit/backups/backups_cubit.dart +++ b/lib/logic/cubit/backups/backups_cubit.dart @@ -85,9 +85,9 @@ class BackupsCubit extends AppConfigDependendCubit { Future createBucket() async { emit(state.copyWith(preventActions: true)); - final domain = appConfigCubit.state.cloudFlareDomain!.domainName + final domain = appConfigCubit.state.serverDomain!.domainName .replaceAll(RegExp(r'[^a-zA-Z0-9]'), '-'); - final serverId = appConfigCubit.state.hetznerServer!.id; + final serverId = appConfigCubit.state.serverDetails!.id; var bucketName = 'selfprivacy-$domain-$serverId'; // If bucket name is too long, shorten it if (bucketName.length > 49) { diff --git a/lib/logic/cubit/dns_records/dns_records_cubit.dart b/lib/logic/cubit/dns_records/dns_records_cubit.dart index de03e356..8efc7bfc 100644 --- a/lib/logic/cubit/dns_records/dns_records_cubit.dart +++ b/lib/logic/cubit/dns_records/dns_records_cubit.dart @@ -1,6 +1,6 @@ import 'package:cubit_form/cubit_form.dart'; import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart'; -import 'package:selfprivacy/logic/models/cloudflare_domain.dart'; +import 'package:selfprivacy/logic/models/server_domain.dart'; import 'package:selfprivacy/logic/models/dns_records.dart'; import '../../api_maps/cloudflare.dart'; @@ -20,11 +20,11 @@ class DnsRecordsCubit extends AppConfigDependendCubit { emit(DnsRecordsState( dnsState: DnsRecordsStatus.refreshing, dnsRecords: _getDesiredDnsRecords( - appConfigCubit.state.cloudFlareDomain?.domainName, "", ""))); + appConfigCubit.state.serverDomain?.domainName, "", ""))); print('Loading DNS status'); if (appConfigCubit.state is AppConfigFinished) { - final CloudFlareDomain? domain = appConfigCubit.state.cloudFlareDomain; - final String? ipAddress = appConfigCubit.state.hetznerServer?.ip4; + final ServerDomain? domain = appConfigCubit.state.serverDomain; + final String? ipAddress = appConfigCubit.state.serverDetails?.ip4; if (domain != null && ipAddress != null) { final List records = await cloudflare.getDnsRecords(cloudFlareDomain: domain); @@ -95,8 +95,8 @@ class DnsRecordsCubit extends AppConfigDependendCubit { Future fix() async { emit(state.copyWith(dnsState: DnsRecordsStatus.refreshing)); - final CloudFlareDomain? domain = appConfigCubit.state.cloudFlareDomain; - final String? ipAddress = appConfigCubit.state.hetznerServer?.ip4; + final ServerDomain? domain = appConfigCubit.state.serverDomain; + final String? ipAddress = appConfigCubit.state.serverDetails?.ip4; final String? dkimPublicKey = await api.getDkim(); await cloudflare.removeSimilarRecords(cloudFlareDomain: domain!); await cloudflare.createMultipleDnsRecords( diff --git a/lib/logic/cubit/forms/factories/field_cubit_factory.dart b/lib/logic/cubit/forms/factories/field_cubit_factory.dart index d3255a5f..ac75c2c9 100644 --- a/lib/logic/cubit/forms/factories/field_cubit_factory.dart +++ b/lib/logic/cubit/forms/factories/field_cubit_factory.dart @@ -52,5 +52,14 @@ class FieldCubitFactory { ); } + FieldCubit createServerDomainField() { + return FieldCubit( + initalValue: '', + validations: [ + RequiredStringValidation('validations.required'.tr()), + ], + ); + } + final BuildContext context; } diff --git a/lib/logic/cubit/forms/setup/initializing/domain_cloudflare.dart b/lib/logic/cubit/forms/setup/initializing/domain_cloudflare.dart index 78a244c8..1582a4cf 100644 --- a/lib/logic/cubit/forms/setup/initializing/domain_cloudflare.dart +++ b/lib/logic/cubit/forms/setup/initializing/domain_cloudflare.dart @@ -1,7 +1,7 @@ import 'package:cubit_form/cubit_form.dart'; import 'package:selfprivacy/logic/api_maps/cloudflare.dart'; import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart'; -import 'package:selfprivacy/logic/models/cloudflare_domain.dart'; +import 'package:selfprivacy/logic/models/server_domain.dart'; class DomainSetupCubit extends Cubit { DomainSetupCubit(this.initializingCubit) : super(Initial()); @@ -36,9 +36,10 @@ class DomainSetupCubit extends Cubit { var zoneId = await api.getZoneId(domainName); - var domain = CloudFlareDomain( + var domain = ServerDomain( domainName: domainName, zoneId: zoneId, + provider: DnsProvider.Cloudflare, ); initializingCubit.setDomain(domain); diff --git a/lib/logic/cubit/forms/setup/recovering/recovery_domain_form_cubit.dart b/lib/logic/cubit/forms/setup/recovering/recovery_domain_form_cubit.dart index 8ab5cb88..1b56b610 100644 --- a/lib/logic/cubit/forms/setup/recovering/recovery_domain_form_cubit.dart +++ b/lib/logic/cubit/forms/setup/recovering/recovery_domain_form_cubit.dart @@ -1,51 +1,32 @@ import 'dart:async'; import 'package:cubit_form/cubit_form.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:selfprivacy/logic/api_maps/hetzner.dart'; +import 'package:selfprivacy/logic/api_maps/server.dart'; import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart'; -import 'package:selfprivacy/logic/cubit/forms/validations/validations.dart'; +import 'package:selfprivacy/logic/cubit/forms/factories/field_cubit_factory.dart'; +import 'package:selfprivacy/logic/models/server_domain.dart'; class RecoveryDomainFormCubit extends FormCubit { - RecoveryDomainFormCubit(this.initializingCubit) { - var regExp = RegExp(r"\s+|[-!$%^&*()@+|~=`{}\[\]:<>?,.\/]"); - apiKey = FieldCubit( - initalValue: '', - validations: [ - RequiredStringValidation('validations.required'.tr()), - ValidationModel( - (s) => regExp.hasMatch(s), 'validations.key_format'.tr()), - LengthStringNotEqualValidation(64) - ], - ); + RecoveryDomainFormCubit( + this.initializingCubit, final FieldCubitFactory fieldFactory) { + serverDomainField = fieldFactory.createServerDomainField(); - super.addFields([apiKey]); + super.addFields([serverDomainField]); } @override FutureOr onSubmit() async { - initializingCubit.setHetznerKey(apiKey.state.value); + initializingCubit.setDomain(ServerDomain( + domainName: serverDomainField.state.value, + provider: DnsProvider.Unknown, + zoneId: "")); } + // @override + // FutureOr asyncValidation() async { + // ; //var client = + // } + final AppConfigCubit initializingCubit; - - late final FieldCubit apiKey; - - @override - FutureOr asyncValidation() async { - late bool isKeyValid; - HetznerApi apiClient = HetznerApi(isWithToken: false); - - try { - isKeyValid = await apiClient.isValid(apiKey.state.value); - } catch (e) { - addError(e); - } - - if (!isKeyValid) { - apiKey.setError('bad key'); - return false; - } - return true; - } + late final FieldCubit serverDomainField; } diff --git a/lib/logic/cubit/server_detailed_info/server_detailed_info_cubit.dart b/lib/logic/cubit/server_detailed_info/server_detailed_info_cubit.dart index b4969037..86aae1a9 100644 --- a/lib/logic/cubit/server_detailed_info/server_detailed_info_cubit.dart +++ b/lib/logic/cubit/server_detailed_info/server_detailed_info_cubit.dart @@ -14,7 +14,7 @@ class ServerDetailsCubit extends Cubit { ServerDetailsRepository repository = ServerDetailsRepository(); void check() async { - var isReadyToCheck = getIt().hetznerServer != null; + var isReadyToCheck = getIt().serverDetails != null; if (isReadyToCheck) { emit(ServerDetailsLoading()); var data = await repository.load(); diff --git a/lib/logic/get_it/api_config.dart b/lib/logic/get_it/api_config.dart index 5a2d622a..6ab1b564 100644 --- a/lib/logic/get_it/api_config.dart +++ b/lib/logic/get_it/api_config.dart @@ -2,26 +2,24 @@ import 'package:hive/hive.dart'; import 'package:selfprivacy/config/hive_config.dart'; import 'package:selfprivacy/logic/models/backblaze_bucket.dart'; import 'package:selfprivacy/logic/models/backblaze_credential.dart'; -import 'package:selfprivacy/logic/models/cloudflare_domain.dart'; +import 'package:selfprivacy/logic/models/server_domain.dart'; import 'package:selfprivacy/logic/models/server_details.dart'; class ApiConfigModel { Box _box = Hive.box(BNames.appConfig); - HetznerServerDetails? get hetznerServer => _hetznerServer; + ServerHostingDetails? get serverDetails => _serverDetails; String? get hetznerKey => _hetznerKey; String? get cloudFlareKey => _cloudFlareKey; - String? get serverDomain => _serverDomain; BackblazeCredential? get backblazeCredential => _backblazeCredential; - CloudFlareDomain? get cloudFlareDomain => _cloudFlareDomain; + ServerDomain? get serverDomain => _serverDomain; BackblazeBucket? get backblazeBucket => _backblazeBucket; String? _hetznerKey; String? _cloudFlareKey; - String? _serverDomain; - HetznerServerDetails? _hetznerServer; + ServerHostingDetails? _serverDetails; BackblazeCredential? _backblazeCredential; - CloudFlareDomain? _cloudFlareDomain; + ServerDomain? _serverDomain; BackblazeBucket? _backblazeBucket; Future storeHetznerKey(String value) async { @@ -34,25 +32,20 @@ class ApiConfigModel { _cloudFlareKey = value; } - Future storeServerDomain(String value) async { - await _box.put(BNames.serverDomain, value); - _serverDomain = value; - } - Future storeBackblazeCredential(BackblazeCredential value) async { await _box.put(BNames.backblazeKey, value); _backblazeCredential = value; } - Future storeCloudFlareDomain(CloudFlareDomain value) async { - await _box.put(BNames.cloudFlareDomain, value); - _cloudFlareDomain = value; + Future storeServerDomain(ServerDomain value) async { + await _box.put(BNames.serverDomain, value); + _serverDomain = value; } - Future storeServerDetails(HetznerServerDetails value) async { - await _box.put(BNames.hetznerServer, value); - _hetznerServer = value; + Future storeServerDetails(ServerHostingDetails value) async { + await _box.put(BNames.serverDetails, value); + _serverDetails = value; } Future storeBackblazeBucket(BackblazeBucket value) async { @@ -64,8 +57,8 @@ class ApiConfigModel { _hetznerKey = null; _cloudFlareKey = null; _backblazeCredential = null; - _cloudFlareDomain = null; - _hetznerServer = null; + _serverDomain = null; + _serverDetails = null; _backblazeBucket = null; } @@ -74,8 +67,8 @@ class ApiConfigModel { _cloudFlareKey = _box.get(BNames.cloudFlareKey); _backblazeCredential = _box.get(BNames.backblazeKey); - _cloudFlareDomain = _box.get(BNames.cloudFlareDomain); - _hetznerServer = _box.get(BNames.hetznerServer); + _serverDomain = _box.get(BNames.serverDomain); + _serverDetails = _box.get(BNames.serverDetails); _backblazeBucket = _box.get(BNames.backblazeBucket); } } diff --git a/lib/logic/models/server_details.dart b/lib/logic/models/server_details.dart index f928ebc8..d12c1b36 100644 --- a/lib/logic/models/server_details.dart +++ b/lib/logic/models/server_details.dart @@ -3,8 +3,8 @@ import 'package:hive/hive.dart'; part 'server_details.g.dart'; @HiveType(typeId: 2) -class HetznerServerDetails { - HetznerServerDetails({ +class ServerHostingDetails { + ServerHostingDetails({ required this.ip4, required this.id, required this.createTime, @@ -26,13 +26,13 @@ class HetznerServerDetails { final DateTime? startTime; @HiveField(4) - final HetznerDataBase dataBase; + final ServerVolume dataBase; @HiveField(5) final String apiToken; - HetznerServerDetails copyWith({DateTime? startTime}) { - return HetznerServerDetails( + ServerHostingDetails copyWith({DateTime? startTime}) { + return ServerHostingDetails( startTime: startTime ?? this.startTime, createTime: createTime, id: id, @@ -46,8 +46,8 @@ class HetznerServerDetails { } @HiveType(typeId: 5) -class HetznerDataBase { - HetznerDataBase({ +class ServerVolume { + ServerVolume({ required this.id, required this.name, }); diff --git a/lib/logic/models/server_details.g.dart b/lib/logic/models/server_details.g.dart index cba8848c..61252506 100644 --- a/lib/logic/models/server_details.g.dart +++ b/lib/logic/models/server_details.g.dart @@ -6,28 +6,28 @@ part of 'server_details.dart'; // TypeAdapterGenerator // ************************************************************************** -class HetznerServerDetailsAdapter extends TypeAdapter { +class HetznerServerDetailsAdapter extends TypeAdapter { @override final int typeId = 2; @override - HetznerServerDetails read(BinaryReader reader) { + ServerHostingDetails read(BinaryReader reader) { final numOfFields = reader.readByte(); final fields = { for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), }; - return HetznerServerDetails( + return ServerHostingDetails( ip4: fields[0] as String, id: fields[1] as int, createTime: fields[3] as DateTime?, - dataBase: fields[4] as HetznerDataBase, + dataBase: fields[4] as ServerVolume, apiToken: fields[5] as String, startTime: fields[2] as DateTime?, ); } @override - void write(BinaryWriter writer, HetznerServerDetails obj) { + void write(BinaryWriter writer, ServerHostingDetails obj) { writer ..writeByte(6) ..writeByte(0) @@ -55,24 +55,24 @@ class HetznerServerDetailsAdapter extends TypeAdapter { typeId == other.typeId; } -class HetznerDataBaseAdapter extends TypeAdapter { +class HetznerDataBaseAdapter extends TypeAdapter { @override final int typeId = 5; @override - HetznerDataBase read(BinaryReader reader) { + ServerVolume read(BinaryReader reader) { final numOfFields = reader.readByte(); final fields = { for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), }; - return HetznerDataBase( + return ServerVolume( id: fields[1] as int, name: fields[2] as String, ); } @override - void write(BinaryWriter writer, HetznerDataBase obj) { + void write(BinaryWriter writer, ServerVolume obj) { writer ..writeByte(2) ..writeByte(1) diff --git a/lib/logic/models/cloudflare_domain.dart b/lib/logic/models/server_domain.dart similarity index 55% rename from lib/logic/models/cloudflare_domain.dart rename to lib/logic/models/server_domain.dart index 9d85bfb1..81525d2f 100644 --- a/lib/logic/models/cloudflare_domain.dart +++ b/lib/logic/models/server_domain.dart @@ -1,12 +1,18 @@ import 'package:hive/hive.dart'; -part 'cloudflare_domain.g.dart'; +part 'server_domain.g.dart'; + +enum DnsProvider { + Unknown, + Cloudflare, +} @HiveType(typeId: 3) -class CloudFlareDomain { - CloudFlareDomain({ +class ServerDomain { + ServerDomain({ required this.domainName, required this.zoneId, + required this.provider, }); @HiveField(0) @@ -15,6 +21,9 @@ class CloudFlareDomain { @HiveField(1) final String zoneId; + @HiveField(2, defaultValue: DnsProvider.Cloudflare) + final DnsProvider provider; + @override String toString() { return '$domainName: $zoneId'; diff --git a/lib/logic/models/cloudflare_domain.g.dart b/lib/logic/models/server_domain.g.dart similarity index 66% rename from lib/logic/models/cloudflare_domain.g.dart rename to lib/logic/models/server_domain.g.dart index dcd95317..c154fba1 100644 --- a/lib/logic/models/cloudflare_domain.g.dart +++ b/lib/logic/models/server_domain.g.dart @@ -1,35 +1,39 @@ // GENERATED CODE - DO NOT MODIFY BY HAND -part of 'cloudflare_domain.dart'; +part of 'server_domain.dart'; // ************************************************************************** // TypeAdapterGenerator // ************************************************************************** -class CloudFlareDomainAdapter extends TypeAdapter { +class ServerDomainAdapter extends TypeAdapter { @override final int typeId = 3; @override - CloudFlareDomain read(BinaryReader reader) { + ServerDomain read(BinaryReader reader) { final numOfFields = reader.readByte(); final fields = { for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), }; - return CloudFlareDomain( + return ServerDomain( domainName: fields[0] as String, zoneId: fields[1] as String, + provider: + fields[2] == null ? DnsProvider.Cloudflare : fields[2] as DnsProvider, ); } @override - void write(BinaryWriter writer, CloudFlareDomain obj) { + void write(BinaryWriter writer, ServerDomain obj) { writer - ..writeByte(2) + ..writeByte(3) ..writeByte(0) ..write(obj.domainName) ..writeByte(1) - ..write(obj.zoneId); + ..write(obj.zoneId) + ..writeByte(2) + ..write(obj.provider); } @override @@ -38,7 +42,7 @@ class CloudFlareDomainAdapter extends TypeAdapter { @override bool operator ==(Object other) => identical(this, other) || - other is CloudFlareDomainAdapter && + other is ServerDomainAdapter && runtimeType == other.runtimeType && typeId == other.typeId; } diff --git a/lib/ui/pages/dns_details/dns_details.dart b/lib/ui/pages/dns_details/dns_details.dart index 8973e963..e07cc445 100644 --- a/lib/ui/pages/dns_details/dns_details.dart +++ b/lib/ui/pages/dns_details/dns_details.dart @@ -63,7 +63,7 @@ class _DnsDetailsPageState extends State { @override Widget build(BuildContext context) { var isReady = context.watch().state is AppConfigFinished; - final domain = getIt().cloudFlareDomain?.domainName ?? ''; + final domain = getIt().serverDomain?.domainName ?? ''; var dnsCubit = context.watch().state; print(dnsCubit.dnsState); diff --git a/lib/ui/pages/more/app_settings/app_setting.dart b/lib/ui/pages/more/app_settings/app_setting.dart index 895294c7..8c5a78c8 100644 --- a/lib/ui/pages/more/app_settings/app_setting.dart +++ b/lib/ui/pages/more/app_settings/app_setting.dart @@ -129,7 +129,7 @@ class _AppSettingsPageState extends State { Widget deleteServer(BuildContext context) { var isDisabled = - context.watch().state.hetznerServer == null; + context.watch().state.serverDetails == null; return Container( padding: EdgeInsets.only(top: 20, bottom: 5), decoration: BoxDecoration( diff --git a/lib/ui/pages/providers/providers.dart b/lib/ui/pages/providers/providers.dart index dc53ed2f..974f29ce 100644 --- a/lib/ui/pages/providers/providers.dart +++ b/lib/ui/pages/providers/providers.dart @@ -100,7 +100,7 @@ class _Card extends StatelessWidget { AppConfigState appConfig = context.watch().state; var domainName = - appConfig.isDomainFilled ? appConfig.cloudFlareDomain!.domainName : ''; + appConfig.isDomainFilled ? appConfig.serverDomain!.domainName : ''; switch (provider.type) { case ProviderType.server: diff --git a/lib/ui/pages/setup/recovering/recovery_domain.dart b/lib/ui/pages/setup/recovering/recovery_domain.dart index c9546ccc..8ba452d7 100644 --- a/lib/ui/pages/setup/recovering/recovery_domain.dart +++ b/lib/ui/pages/setup/recovering/recovery_domain.dart @@ -1,75 +1,48 @@ +import 'package:cubit_form/cubit_form.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:selfprivacy/logic/cubit/app_config/app_config_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'; -class RecoveryDomain extends StatefulWidget { - @override - State createState() => _RecoveryDomainState(); -} - -class _RecoveryDomainState extends State { +class RecoveryDomain extends StatelessWidget { @override Widget build(BuildContext context) { - return BrandHeroScreen( - children: [ - TextField( - decoration: InputDecoration( - border: OutlineInputBorder(), - labelText: "recovering.domain_recover_placeholder".tr(), - ), - ), - SizedBox(height: 16), - BrandButton.rised( - onPressed: () {}, - text: "more.continue".tr(), - ), - ], - heroTitle: "recovering.recovery_main_header".tr(), - heroSubtitle: "recovering.domain_recovery_description".tr(), - hasBackButton: true, - hasFlashButton: false, - heroIcon: Icons.link, - ); - } -} + var appConfig = context.watch(); -/*class RecoveryDomain extends StatelessWidget { - @override - Widget build(BuildContext context) { - return SafeArea( - child: Scaffold( - appBar: PreferredSize( - preferredSize: Size.fromHeight(52), - child: BrandHeader(hasBackButton: true), - ), - body: ListView( - padding: EdgeInsets.all(16), + return BlocProvider( + create: (context) => + RecoveryDomainFormCubit(appConfig, FieldCubitFactory(context)), + child: Builder(builder: (context) { + var formCubitState = context.watch().state; + + return BrandHeroScreen( children: [ - Text( - "recovering.recovery_main_header".tr(), - style: Theme.of(context).textTheme.headlineMedium, - ), - SizedBox(height: 18), - Text( - "recovering.domain_recovery_description".tr(), - style: Theme.of(context).textTheme.bodyLarge, - ), - SizedBox(height: 18), - TextField( + CubitFormTextField( + formFieldCubit: + context.read().serverDomainField, decoration: InputDecoration( border: OutlineInputBorder(), labelText: "recovering.domain_recover_placeholder".tr(), ), ), - SizedBox(height: 18), + SizedBox(height: 16), BrandButton.rised( - onPressed: () {}, + onPressed: formCubitState.isSubmitting + ? null + : () => context.read().trySubmit(), text: "more.continue".tr(), ), ], - ), - ), + heroTitle: "recovering.recovery_main_header".tr(), + heroSubtitle: "recovering.domain_recovery_description".tr(), + hasBackButton: true, + hasFlashButton: false, + heroIcon: Icons.link, + ); + }), ); } -}*/ \ No newline at end of file +} diff --git a/lib/utils/ui_helpers.dart b/lib/utils/ui_helpers.dart index 9ce41d0c..734bf4f9 100644 --- a/lib/utils/ui_helpers.dart +++ b/lib/utils/ui_helpers.dart @@ -3,7 +3,6 @@ import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart'; /// it's ui helpers use only for ui components, don't use for logic components. class UiHelpers { - static String getDomainName(AppConfigState config) => config.isDomainFilled - ? config.cloudFlareDomain!.domainName - : 'example.com'; + static String getDomainName(AppConfigState config) => + config.isDomainFilled ? config.serverDomain!.domainName : 'example.com'; } From 129c1bb4c6cbd91279e50fe488533a427633957b Mon Sep 17 00:00:00 2001 From: NaiJi Date: Sat, 14 May 2022 05:54:40 +0300 Subject: [PATCH 07/52] Refactor infrastructure Co-authored-by: Inex Code --- lib/config/hive_config.dart | 14 +- lib/logic/api_maps/backblaze.dart | 2 +- lib/logic/api_maps/cloudflare.dart | 4 +- lib/logic/api_maps/hetzner.dart | 9 +- lib/logic/api_maps/server.dart | 939 ++++++++---------- .../cubit/app_config/app_config_cubit.dart | 160 +-- .../app_config/app_config_repository.dart | 12 +- lib/logic/cubit/backups/backups_cubit.dart | 4 +- .../cubit/dns_records/dns_records_cubit.dart | 4 +- .../initializing/backblaze_form_cubit.dart | 2 +- .../setup/initializing/domain_cloudflare.dart | 2 +- .../initializing/root_user_form_cubit.dart | 2 +- .../recovery_domain_form_cubit.dart | 2 +- .../cubit/forms/user/ssh_form_cubit.dart | 2 +- .../cubit/forms/user/user_form_cubit.dart | 2 +- .../server_detailed_info_cubit.dart | 4 +- .../server_detailed_info_repository.dart | 4 +- lib/logic/cubit/users/users_cubit.dart | 2 +- lib/logic/get_it/api_config.dart | 8 +- lib/logic/models/hive/README.md | 13 + .../models/{ => hive}/backblaze_bucket.dart | 0 .../models/{ => hive}/backblaze_bucket.g.dart | 0 .../{ => hive}/backblaze_credential.dart | 0 .../{ => hive}/backblaze_credential.g.dart | 0 .../models/{ => hive}/server_details.dart | 19 +- .../models/{ => hive}/server_details.g.dart | 60 +- .../models/{ => hive}/server_domain.dart | 13 +- .../models/{ => hive}/server_domain.g.dart | 39 + lib/logic/models/{ => hive}/user.dart | 0 lib/logic/models/{ => hive}/user.g.dart | 0 lib/logic/models/job.dart | 2 +- lib/logic/models/{ => json}/api_token.dart | 0 lib/logic/models/{ => json}/api_token.g.dart | 0 .../{ => json}/auto_upgrade_settings.dart | 0 .../{ => json}/auto_upgrade_settings.g.dart | 0 lib/logic/models/{ => json}/backup.dart | 0 lib/logic/models/{ => json}/backup.g.dart | 0 lib/logic/models/{ => json}/device_token.dart | 0 .../models/{ => json}/device_token.g.dart | 0 lib/logic/models/{ => json}/dns_records.dart | 0 .../models/{ => json}/dns_records.g.dart | 0 .../{ => json}/hetzner_server_info.dart | 0 .../{ => json}/hetzner_server_info.g.dart | 0 .../{ => json}/recovery_token_status.dart | 0 .../{ => json}/recovery_token_status.g.dart | 0 .../{ => json}/server_configurations.dart | 0 .../{ => json}/server_configurations.g.dart | 0 .../pages/backup_details/backup_details.dart | 2 +- lib/ui/pages/setup/initializing.dart | 2 +- lib/ui/pages/ssh_keys/ssh_keys.dart | 2 +- lib/ui/pages/users/users.dart | 2 +- 51 files changed, 690 insertions(+), 642 deletions(-) create mode 100644 lib/logic/models/hive/README.md rename lib/logic/models/{ => hive}/backblaze_bucket.dart (100%) rename lib/logic/models/{ => hive}/backblaze_bucket.g.dart (100%) rename lib/logic/models/{ => hive}/backblaze_credential.dart (100%) rename lib/logic/models/{ => hive}/backblaze_credential.g.dart (100%) rename lib/logic/models/{ => hive}/server_details.dart (74%) rename lib/logic/models/{ => hive}/server_details.g.dart (60%) rename lib/logic/models/{ => hive}/server_domain.dart (90%) rename lib/logic/models/{ => hive}/server_domain.g.dart (60%) rename lib/logic/models/{ => hive}/user.dart (100%) rename lib/logic/models/{ => hive}/user.g.dart (100%) rename lib/logic/models/{ => json}/api_token.dart (100%) rename lib/logic/models/{ => json}/api_token.g.dart (100%) rename lib/logic/models/{ => json}/auto_upgrade_settings.dart (100%) rename lib/logic/models/{ => json}/auto_upgrade_settings.g.dart (100%) rename lib/logic/models/{ => json}/backup.dart (100%) rename lib/logic/models/{ => json}/backup.g.dart (100%) rename lib/logic/models/{ => json}/device_token.dart (100%) rename lib/logic/models/{ => json}/device_token.g.dart (100%) rename lib/logic/models/{ => json}/dns_records.dart (100%) rename lib/logic/models/{ => json}/dns_records.g.dart (100%) rename lib/logic/models/{ => json}/hetzner_server_info.dart (100%) rename lib/logic/models/{ => json}/hetzner_server_info.g.dart (100%) rename lib/logic/models/{ => json}/recovery_token_status.dart (100%) rename lib/logic/models/{ => json}/recovery_token_status.g.dart (100%) rename lib/logic/models/{ => json}/server_configurations.dart (100%) rename lib/logic/models/{ => json}/server_configurations.g.dart (100%) diff --git a/lib/config/hive_config.dart b/lib/config/hive_config.dart index 3eeb1219..e5ef6927 100644 --- a/lib/config/hive_config.dart +++ b/lib/config/hive_config.dart @@ -3,21 +3,21 @@ import 'dart:typed_data'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:hive_flutter/hive_flutter.dart'; -import 'package:selfprivacy/logic/models/backblaze_bucket.dart'; -import 'package:selfprivacy/logic/models/backblaze_credential.dart'; -import 'package:selfprivacy/logic/models/server_domain.dart'; -import 'package:selfprivacy/logic/models/server_details.dart'; -import 'package:selfprivacy/logic/models/user.dart'; +import 'package:selfprivacy/logic/models/hive/backblaze_bucket.dart'; +import 'package:selfprivacy/logic/models/hive/backblaze_credential.dart'; +import 'package:selfprivacy/logic/models/hive/server_domain.dart'; +import 'package:selfprivacy/logic/models/hive/server_details.dart'; +import 'package:selfprivacy/logic/models/hive/user.dart'; class HiveConfig { static Future init() async { await Hive.initFlutter(); Hive.registerAdapter(UserAdapter()); - Hive.registerAdapter(HetznerServerDetailsAdapter()); + Hive.registerAdapter(ServerHostingDetailsAdapter()); Hive.registerAdapter(ServerDomainAdapter()); Hive.registerAdapter(BackblazeCredentialAdapter()); Hive.registerAdapter(BackblazeBucketAdapter()); - Hive.registerAdapter(HetznerDataBaseAdapter()); + Hive.registerAdapter(ServerVolumeAdapter()); await Hive.openBox(BNames.appSettings); await Hive.openBox(BNames.users); diff --git a/lib/logic/api_maps/backblaze.dart b/lib/logic/api_maps/backblaze.dart index 6a56788c..678ec333 100644 --- a/lib/logic/api_maps/backblaze.dart +++ b/lib/logic/api_maps/backblaze.dart @@ -3,7 +3,7 @@ import 'dart:io'; import 'package:dio/dio.dart'; import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/logic/api_maps/api_map.dart'; -import 'package:selfprivacy/logic/models/backblaze_credential.dart'; +import 'package:selfprivacy/logic/models/hive/backblaze_credential.dart'; class BackblazeApiAuth { BackblazeApiAuth({required this.authorizationToken, required this.apiUrl}); diff --git a/lib/logic/api_maps/cloudflare.dart b/lib/logic/api_maps/cloudflare.dart index 5b81fdba..9da5ecee 100644 --- a/lib/logic/api_maps/cloudflare.dart +++ b/lib/logic/api_maps/cloudflare.dart @@ -3,8 +3,8 @@ import 'dart:io'; import 'package:dio/dio.dart'; import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/logic/api_maps/api_map.dart'; -import 'package:selfprivacy/logic/models/server_domain.dart'; -import 'package:selfprivacy/logic/models/dns_records.dart'; +import 'package:selfprivacy/logic/models/hive/server_domain.dart'; +import 'package:selfprivacy/logic/models/json/dns_records.dart'; class CloudflareApi extends ApiMap { CloudflareApi({this.hasLogger = false, this.isWithToken = true}); diff --git a/lib/logic/api_maps/hetzner.dart b/lib/logic/api_maps/hetzner.dart index 304650f2..2256f9e5 100644 --- a/lib/logic/api_maps/hetzner.dart +++ b/lib/logic/api_maps/hetzner.dart @@ -4,9 +4,9 @@ import 'dart:io'; import 'package:dio/dio.dart'; import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/logic/api_maps/api_map.dart'; -import 'package:selfprivacy/logic/models/hetzner_server_info.dart'; -import 'package:selfprivacy/logic/models/server_details.dart'; -import 'package:selfprivacy/logic/models/user.dart'; +import 'package:selfprivacy/logic/models/json/hetzner_server_info.dart'; +import 'package:selfprivacy/logic/models/hive/server_details.dart'; +import 'package:selfprivacy/logic/models/hive/user.dart'; import 'package:selfprivacy/utils/password_generator.dart'; class HetznerApi extends ApiMap { @@ -140,8 +140,9 @@ class HetznerApi extends ApiMap { id: serverCreateResponse.data['server']['id'], ip4: serverCreateResponse.data['server']['public_net']['ipv4']['ip'], createTime: DateTime.now(), - dataBase: dataBase, + volume: dataBase, apiToken: apiToken, + provider: ServerProvider.Hetzner, ); } diff --git a/lib/logic/api_maps/server.dart b/lib/logic/api_maps/server.dart index 74c2137f..27c0f5c2 100644 --- a/lib/logic/api_maps/server.dart +++ b/lib/logic/api_maps/server.dart @@ -5,14 +5,15 @@ import 'dart:io'; import 'package:dio/dio.dart'; import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/logic/common_enum/common_enum.dart'; -import 'package:selfprivacy/logic/models/api_token.dart'; -import 'package:selfprivacy/logic/models/auto_upgrade_settings.dart'; -import 'package:selfprivacy/logic/models/backblaze_bucket.dart'; -import 'package:selfprivacy/logic/models/backup.dart'; -import 'package:selfprivacy/logic/models/recovery_token_status.dart'; -import 'package:selfprivacy/logic/models/device_token.dart'; +import 'package:selfprivacy/logic/models/json/api_token.dart'; +import 'package:selfprivacy/logic/models/json/auto_upgrade_settings.dart'; +import 'package:selfprivacy/logic/models/hive/backblaze_bucket.dart'; +import 'package:selfprivacy/logic/models/json/backup.dart'; +import 'package:selfprivacy/logic/models/json/recovery_token_status.dart'; +import 'package:selfprivacy/logic/models/json/device_token.dart'; import 'package:selfprivacy/logic/models/timezone_settings.dart'; -import 'package:selfprivacy/logic/models/user.dart'; +import 'package:selfprivacy/logic/models/hive/user.dart'; +import 'package:timezone/browser.dart'; import 'api_map.dart'; @@ -92,10 +93,9 @@ class ServerApi extends ApiMap { } Future> createUser(User user) async { - var client = await getClient(); - Response response; + var client = await getClient(); try { response = await client.post( '/users', @@ -120,21 +120,21 @@ class ServerApi extends ApiMap { } bool isFoundOnServer = false; - int statusCode = 0; + int code = 0; final bool isUserCreated = (response.statusCode != null) && (response.statusCode == HttpStatus.created); if (isUserCreated) { isFoundOnServer = true; - statusCode = response.statusCode!; + code = response.statusCode!; } else { isFoundOnServer = false; - statusCode = HttpStatus.notAcceptable; + code = HttpStatus.notAcceptable; } return ApiResponse( - statusCode: statusCode, + statusCode: code, data: User( login: user.login, password: user.password, @@ -146,8 +146,6 @@ class ServerApi extends ApiMap { Future>> getUsersList() async { List res = []; Response response; - String? message; - int code = 0; var client = await getClient(); try { @@ -157,27 +155,32 @@ class ServerApi extends ApiMap { } } on DioError catch (e) { print(e.message); - message = e.message; - code = e.response?.statusCode ?? HttpStatus.internalServerError; - res = []; + return ApiResponse( + errorMessage: e.message, + statusCode: e.response?.statusCode ?? HttpStatus.internalServerError, + data: [], + ); } catch (e) { print(e); - message = e.toString(); - code = HttpStatus.internalServerError; - res = []; + return ApiResponse( + errorMessage: e.toString(), + statusCode: HttpStatus.internalServerError, + data: [], + ); } finally { close(client); } + final int code = response.statusCode ?? HttpStatus.internalServerError; + return ApiResponse( - errorMessage: message, statusCode: code, data: res, ); } Future> addUserSshKey(User user, String sshKey) async { - Response response; + late Response response; var client = await getClient(); try { @@ -189,47 +192,48 @@ class ServerApi extends ApiMap { ); } on DioError catch (e) { print(e.message); - return ApiResponse( - data: null, - errorMessage: e.message, - statusCode: e.response?.statusCode ?? HttpStatus.internalServerError); + return ApiResponse( + errorMessage: e.message, + statusCode: e.response?.statusCode ?? HttpStatus.internalServerError, + data: null, + ); } finally { close(client); } + final int code = response.statusCode ?? HttpStatus.internalServerError; + return ApiResponse( - statusCode: response.statusCode ?? HttpStatus.internalServerError, + statusCode: code, data: null, - errorMessage: response.data?.containsKey('error') ?? false - ? response.data['error'] - : null, ); } Future> addRootSshKey(String ssh) async { - Response response; + late Response response; var client = await getClient(); - response = await client.put( - '/services/ssh/key/send', - data: {"public_key": ssh}, - options: Options( - contentType: 'application/json', - receiveDataWhenStatusError: true, - followRedirects: false, - validateStatus: (status) { - return (status != null) && - (status < HttpStatus.internalServerError); - }), - ); - close(client); + try { + response = await client.put( + '/services/ssh/key/send', + data: {"public_key": ssh}, + ); + } on DioError catch (e) { + print(e.message); + return ApiResponse( + errorMessage: e.message, + statusCode: e.response?.statusCode ?? HttpStatus.internalServerError, + data: null, + ); + } finally { + close(client); + } + + final int code = response.statusCode ?? HttpStatus.internalServerError; return ApiResponse( - statusCode: response.statusCode ?? HttpStatus.internalServerError, + statusCode: code, data: null, - errorMessage: response.data?.containsKey('error') ?? false - ? response.data['error'] - : null, ); } @@ -238,27 +242,30 @@ class ServerApi extends ApiMap { Response response; var client = await getClient(); - response = await client.get( - '/services/ssh/keys/${user.login}', - options: Options( - contentType: 'application/json', - receiveDataWhenStatusError: true, - followRedirects: false, - validateStatus: (status) { - return (status != null) && - (status < HttpStatus.internalServerError); - }), - ); try { + response = await client.get('/services/ssh/keys/${user.login}'); res = (response.data as List).map((e) => e as String).toList(); + } on DioError catch (e) { + print(e.message); + return ApiResponse>( + errorMessage: e.message, + statusCode: e.response?.statusCode ?? HttpStatus.internalServerError, + data: [], + ); } catch (e) { - print(e); - res = []; + return ApiResponse>( + errorMessage: e.toString(), + statusCode: HttpStatus.internalServerError, + data: [], + ); + } finally { + close(client); } - close(client); + final int code = response.statusCode ?? HttpStatus.internalServerError; + return ApiResponse>( - statusCode: response.statusCode ?? HttpStatus.internalServerError, + statusCode: code, data: res, errorMessage: response.data is List ? null @@ -272,22 +279,26 @@ class ServerApi extends ApiMap { Response response; var client = await getClient(); - response = await client.delete( - '/services/ssh/keys/${user.login}', - data: {"public_key": sshKey}, - options: Options( - contentType: 'application/json', - receiveDataWhenStatusError: true, - followRedirects: false, - validateStatus: (status) { - return (status != null) && - (status < HttpStatus.internalServerError); - }), - ); - close(client); + try { + response = await client.delete( + '/services/ssh/keys/${user.login}', + data: {"public_key": sshKey}, + ); + } on DioError catch (e) { + print(e.message); + return ApiResponse( + errorMessage: e.message, + statusCode: e.response?.statusCode ?? HttpStatus.internalServerError, + data: null, + ); + } finally { + close(client); + } + + final int code = response.statusCode ?? HttpStatus.internalServerError; return ApiResponse( - statusCode: response.statusCode ?? HttpStatus.internalServerError, + statusCode: code, data: null, errorMessage: response.data?.containsKey('error') ?? false ? response.data['error'] @@ -296,95 +307,69 @@ class ServerApi extends ApiMap { } Future deleteUser(User user) async { - bool res; + bool res = false; Response response; var client = await getClient(); try { - response = await client.delete( - '/users/${user.login}', - options: Options( - contentType: 'application/json', - receiveDataWhenStatusError: true, - followRedirects: false, - validateStatus: (status) { - return (status != null) && - (status < HttpStatus.internalServerError); - }), - ); + response = await client.delete('/users/${user.login}'); res = response.statusCode == HttpStatus.ok || response.statusCode == HttpStatus.notFound; - } catch (e) { - print(e); + } on DioError catch (e) { + print(e.message); res = false; + } finally { + close(client); + return res; } - - close(client); - return res; } String get rootAddress => throw UnimplementedError('not used in with implementation'); Future apply() async { - bool res; + bool res = false; Response response; var client = await getClient(); try { - response = await client.get( - '/system/configuration/apply', - options: Options( - contentType: 'application/json', - receiveDataWhenStatusError: true, - followRedirects: false, - validateStatus: (status) { - return (status != null) && - (status < HttpStatus.internalServerError); - }), - ); - + response = await client.get('/system/configuration/apply'); res = response.statusCode == HttpStatus.ok; - } catch (e) { - print(e); + } on DioError catch (e) { + print(e.message); res = false; + } finally { + close(client); + return res; } - - close(client); - return res; } Future switchService(ServiceTypes type, bool needToTurnOn) async { var client = await getClient(); - client.post( - '/services/${type.url}/${needToTurnOn ? 'enable' : 'disable'}', - options: Options( - contentType: 'application/json', - receiveDataWhenStatusError: true, - followRedirects: false, - validateStatus: (status) { - return (status != null) && - (status < HttpStatus.internalServerError); - }), - ); - close(client); + try { + client.post( + '/services/${type.url}/${needToTurnOn ? 'enable' : 'disable'}', + ); + } on DioError catch (e) { + print(e.message); + } finally { + close(client); + } } Future> servicesPowerCheck() async { - var client = await getClient(); - Response response = await client.get( - '/services/status', - options: Options( - contentType: 'application/json', - receiveDataWhenStatusError: true, - followRedirects: false, - validateStatus: (status) { - return (status != null) && - (status < HttpStatus.internalServerError); - }), - ); + Response response; + + var client = await getClient(); + try { + response = await client.get('/services/status'); + } on DioError catch (e) { + print(e.message); + return {}; + } finally { + close(client); + } - close(client); return { ServiceTypes.passwordManager: response.data['bitwarden'] == 0, ServiceTypes.git: response.data['gitea'] == 0, @@ -396,225 +381,189 @@ class ServerApi extends ApiMap { Future uploadBackblazeConfig(BackblazeBucket bucket) async { var client = await getClient(); - client.put( - '/services/restic/backblaze/config', - data: { - 'accountId': bucket.applicationKeyId, - 'accountKey': bucket.applicationKey, - 'bucket': bucket.bucketName, - }, - options: Options( - contentType: 'application/json', - receiveDataWhenStatusError: true, - followRedirects: false, - validateStatus: (status) { - return (status != null) && - (status < HttpStatus.internalServerError); - }), - ); - close(client); + try { + client.put( + '/services/restic/backblaze/config', + data: { + 'accountId': bucket.applicationKeyId, + 'accountKey': bucket.applicationKey, + 'bucket': bucket.bucketName, + }, + ); + } on DioError catch (e) { + print(e.message); + } finally { + close(client); + } } Future startBackup() async { var client = await getClient(); - client.put( - '/services/restic/backup/create', - options: Options( - contentType: 'application/json', - receiveDataWhenStatusError: true, - followRedirects: false, - validateStatus: (status) { - return (status != null) && - (status < HttpStatus.internalServerError); - }), - ); - close(client); + try { + client.put('/services/restic/backup/create'); + } on DioError catch (e) { + print(e.message); + } finally { + close(client); + } } Future> getBackups() async { Response response; + List backups = []; var client = await getClient(); try { - response = await client.get( - '/services/restic/backup/list', - options: Options( - contentType: 'application/json', - receiveDataWhenStatusError: true, - followRedirects: false, - validateStatus: (status) { - return (status != null) && - (status < HttpStatus.internalServerError); - }), - ); - return response.data.map((e) => Backup.fromJson(e)).toList(); + response = await client.get('/services/restic/backup/list'); + backups = response.data.map((e) => Backup.fromJson(e)).toList(); + } on DioError catch (e) { + print(e.message); } catch (e) { print(e); + } finally { + close(client); + return backups; } - close(client); - return []; } Future getBackupStatus() async { Response response; - - var client = await getClient(); - try { - response = await client.get( - '/services/restic/backup/status', - options: Options( - contentType: 'application/json', - receiveDataWhenStatusError: true, - followRedirects: false, - validateStatus: (status) { - return (status != null) && - (status < HttpStatus.internalServerError); - }), - ); - return BackupStatus.fromJson(response.data); - } catch (e) { - print(e); - } - close(client); - - return BackupStatus( + BackupStatus status = BackupStatus( status: BackupStatusEnum.error, errorMessage: 'Network error', progress: 0, ); + + var client = await getClient(); + try { + response = await client.get('/services/restic/backup/status'); + status = BackupStatus.fromJson(response.data); + } on DioError catch (e) { + print(e.message); + } finally { + close(client); + return status; + } } Future forceBackupListReload() async { var client = await getClient(); - client.get( - '/services/restic/backup/reload', - options: Options( - contentType: 'application/json', - receiveDataWhenStatusError: true, - followRedirects: false, - validateStatus: (status) { - return (status != null) && - (status < HttpStatus.internalServerError); - }), - ); - close(client); + try { + client.get('/services/restic/backup/reload'); + } on DioError catch (e) { + print(e.message); + } finally { + close(client); + } } Future restoreBackup(String backupId) async { var client = await getClient(); - client.put( - '/services/restic/backup/restore', - data: {'backupId': backupId}, - options: Options( - contentType: 'application/json', - receiveDataWhenStatusError: true, - followRedirects: false, - validateStatus: (status) { - return (status != null) && - (status < HttpStatus.internalServerError); - }), - ); - close(client); + try { + client.put( + '/services/restic/backup/restore', + data: {'backupId': backupId}, + ); + } on DioError catch (e) { + print(e.message); + } finally { + close(client); + } } Future pullConfigurationUpdate() async { + Response response; + bool result = false; + var client = await getClient(); - Response response = await client.get( - '/system/configuration/pull', - options: Options( - contentType: 'application/json', - receiveDataWhenStatusError: true, - followRedirects: false, - validateStatus: (status) { - return (status != null) && - (status < HttpStatus.internalServerError); - }), - ); - close(client); - return response.statusCode == HttpStatus.ok; + try { + response = await client.get('/system/configuration/pull'); + result = (response.statusCode != null) + ? (response.statusCode == HttpStatus.ok) + : false; + } on DioError catch (e) { + print(e.message); + } finally { + close(client); + return result; + } } Future reboot() async { + Response response; + bool result = false; + var client = await getClient(); - Response response = await client.get( - '/system/reboot', - options: Options( - contentType: 'application/json', - receiveDataWhenStatusError: true, - followRedirects: false, - validateStatus: (status) { - return (status != null) && - (status < HttpStatus.internalServerError); - }), - ); - close(client); - return response.statusCode == HttpStatus.ok; + try { + response = await client.get('/system/reboot'); + result = (response.statusCode != null) + ? (response.statusCode == HttpStatus.ok) + : false; + } on DioError catch (e) { + print(e.message); + } finally { + close(client); + return result; + } } Future upgrade() async { + Response response; + bool result = false; + var client = await getClient(); - Response response = await client.get( - '/system/configuration/upgrade', - options: Options( - contentType: 'application/json', - receiveDataWhenStatusError: true, - followRedirects: false, - validateStatus: (status) { - return (status != null) && - (status < HttpStatus.internalServerError); - }), - ); - close(client); - return response.statusCode == HttpStatus.ok; + try { + response = await client.get('/system/configuration/upgrade'); + result = (response.statusCode != null) + ? (response.statusCode == HttpStatus.ok) + : false; + } on DioError catch (e) { + print(e.message); + } finally { + close(client); + return result; + } } Future getAutoUpgradeSettings() async { - var client = await getClient(); - Response response = await client.get( - '/system/configuration/autoUpgrade', - options: Options( - contentType: 'application/json', - receiveDataWhenStatusError: true, - followRedirects: false, - validateStatus: (status) { - return (status != null) && - (status < HttpStatus.internalServerError); - }), + Response response; + AutoUpgradeSettings settings = AutoUpgradeSettings( + enable: false, + allowReboot: false, ); - close(client); - return AutoUpgradeSettings.fromJson(response.data); + + var client = await getClient(); + try { + response = await client.get('/system/configuration/autoUpgrade'); + if (response.data != null) { + settings = AutoUpgradeSettings.fromJson(response.data); + } + } on DioError catch (e) { + print(e.message); + } finally { + close(client); + return settings; + } } Future updateAutoUpgradeSettings(AutoUpgradeSettings settings) async { var client = await getClient(); - await client.put( - '/system/configuration/autoUpgrade', - data: settings.toJson(), - options: Options( - contentType: 'application/json', - receiveDataWhenStatusError: true, - followRedirects: false, - validateStatus: (status) { - return (status != null) && - (status < HttpStatus.internalServerError); - }), - ); - close(client); + try { + await client.put( + '/system/configuration/autoUpgrade', + data: settings.toJson(), + ); + } on DioError catch (e) { + print(e.message); + } finally { + close(client); + } } Future getServerTimezone() async { + // I am not sure how to initialize TimeZoneSettings with default value... var client = await getClient(); - Response response = await client.get( - '/system/configuration/timezone', - options: Options( - contentType: 'application/json', - receiveDataWhenStatusError: true, - followRedirects: false, - validateStatus: (status) { - return (status != null) && - (status < HttpStatus.internalServerError); - }), - ); + Response response = await client.get('/system/configuration/timezone'); close(client); return TimeZoneSettings.fromString(response.data); @@ -622,35 +571,30 @@ class ServerApi extends ApiMap { Future updateServerTimezone(TimeZoneSettings settings) async { var client = await getClient(); - await client.put( - '/system/configuration/timezone', - data: settings.toJson(), - options: Options( - contentType: 'application/json', - receiveDataWhenStatusError: true, - followRedirects: false, - validateStatus: (status) { - return (status != null) && - (status < HttpStatus.internalServerError); - }), - ); - close(client); + try { + await client.put( + '/system/configuration/timezone', + data: settings.toString(), + ); + } on DioError catch (e) { + print(e.message); + } finally { + close(client); + } } Future getDkim() async { + Response response; + var client = await getClient(); - Response response = await client.get( - '/services/mailserver/dkim', - options: Options( - contentType: 'application/json', - receiveDataWhenStatusError: true, - followRedirects: false, - validateStatus: (status) { - return (status != null) && - (status < HttpStatus.internalServerError); - }), - ); - close(client); + try { + response = await client.get('/services/mailserver/dkim'); + } on DioError catch (e) { + print(e.message); + return null; + } finally { + close(client); + } if (response.statusCode == null) { return null; @@ -674,241 +618,232 @@ class ServerApi extends ApiMap { } Future> getRecoveryTokenStatus() async { - var client = await getClient(); - Response response = await client.get( - '/auth/recovery_token', - options: Options( - contentType: 'application/json', - receiveDataWhenStatusError: true, - followRedirects: false, - validateStatus: (status) { - return (status != null) && - (status < HttpStatus.internalServerError); - }), - ); - client.close(); + Response response; - if (response.statusCode != null) { + var client = await getClient(); + try { + response = await client.get('/auth/recovery_token'); + } on DioError catch (e) { + print(e.message); return ApiResponse( - statusCode: response.statusCode!, - data: response.data != null - ? response.data.fromJson(response.data) - : null); + errorMessage: e.message, + statusCode: e.response?.statusCode ?? HttpStatus.internalServerError, + data: RecoveryTokenStatus(exists: false, valid: false)); + } finally { + close(client); } + final int code = response.statusCode ?? HttpStatus.internalServerError; + return ApiResponse( - statusCode: HttpStatus.internalServerError, - data: RecoveryTokenStatus(exists: false, valid: false)); + statusCode: code, + data: response.data != null + ? response.data.fromJson(response.data) + : null); } Future> generateRecoveryToken( DateTime expiration, int uses) async { - var client = await getClient(); - Response response = await client.post( - '/auth/recovery_token', - data: { - 'expiration': expiration.toIso8601String(), - 'uses': uses, - }, - options: Options( - contentType: 'application/json', - receiveDataWhenStatusError: true, - followRedirects: false, - validateStatus: (status) { - return (status != null) && - (status < HttpStatus.internalServerError); - }), - ); - client.close(); + Response response; - if (response.statusCode != null) { + var client = await getClient(); + try { + response = await client.post( + '/auth/recovery_token', + data: { + 'expiration': expiration.toIso8601String(), + 'uses': uses, + }, + ); + } on DioError catch (e) { + print(e.message); return ApiResponse( - statusCode: response.statusCode!, - data: response.data != null ? response.data["token"] : ''); + errorMessage: e.message, + statusCode: e.response?.statusCode ?? HttpStatus.internalServerError, + data: ""); + } finally { + close(client); } - return ApiResponse(statusCode: HttpStatus.internalServerError, data: ''); + final int code = response.statusCode ?? HttpStatus.internalServerError; + + return ApiResponse( + statusCode: code, + data: response.data != null ? response.data["token"] : ''); } Future> useRecoveryToken(DeviceToken token) async { - var client = await getClient(); - Response response = await client.post( - '/auth/recovery_token/use', - data: { - 'token': token.token, - 'device': token.device, - }, - options: Options( - contentType: 'application/json', - receiveDataWhenStatusError: true, - followRedirects: false, - validateStatus: (status) { - return (status != null) && - (status < HttpStatus.internalServerError); - }), - ); - client.close(); + Response response; - if (response.statusCode != null) { + var client = await getClient(); + try { + response = await client.post( + '/auth/recovery_token/use', + data: { + 'token': token.token, + 'device': token.device, + }, + ); + } on DioError catch (e) { + print(e.message); return ApiResponse( - statusCode: response.statusCode!, - data: response.data != null ? response.data["token"] : ''); + errorMessage: e.message, + statusCode: e.response?.statusCode ?? HttpStatus.internalServerError, + data: ""); + } finally { + client.close(); } - return ApiResponse(statusCode: HttpStatus.internalServerError, data: ''); + final int code = response.statusCode ?? HttpStatus.internalServerError; + + return ApiResponse( + statusCode: code, + data: response.data != null ? response.data["token"] : ''); } Future> authorizeDevice(DeviceToken token) async { - var client = await getClient(); - Response response = await client.post( - '/auth/new_device/authorize', - data: { - 'token': token.token, - 'device': token.device, - }, - options: Options( - contentType: 'application/json', - receiveDataWhenStatusError: true, - followRedirects: false, - validateStatus: (status) { - return (status != null) && - (status < HttpStatus.internalServerError); - }), - ); - client.close(); + Response response; - if (response.statusCode != null) { + var client = await getClient(); + try { + response = await client.post( + '/auth/new_device/authorize', + data: { + 'token': token.token, + 'device': token.device, + }, + ); + } on DioError catch (e) { + print(e.message); return ApiResponse( - statusCode: response.statusCode!, - data: response.data != null ? response.data : ''); + errorMessage: e.message, + statusCode: e.response?.statusCode ?? HttpStatus.internalServerError, + data: ""); + } finally { + client.close(); } - return ApiResponse(statusCode: HttpStatus.internalServerError, data: ''); + final int code = response.statusCode ?? HttpStatus.internalServerError; + + return ApiResponse( + statusCode: code, data: response.data != null ? response.data : ''); } Future> createDeviceToken() async { - var client = await getClient(); - Response response = await client.post( - '/auth/new_device', - options: Options( - contentType: 'application/json', - receiveDataWhenStatusError: true, - followRedirects: false, - validateStatus: (status) { - return (status != null) && - (status < HttpStatus.internalServerError); - }), - ); - client.close(); + Response response; - if (response.statusCode != null) { + var client = await getClient(); + try { + response = await client.post('/auth/new_device'); + } on DioError catch (e) { + print(e.message); return ApiResponse( - statusCode: response.statusCode!, - data: response.data != null ? response.data["token"] : ''); + errorMessage: e.message, + statusCode: e.response?.statusCode ?? HttpStatus.internalServerError, + data: ""); + } finally { + client.close(); } - return ApiResponse(statusCode: HttpStatus.internalServerError, data: ''); + final int code = response.statusCode ?? HttpStatus.internalServerError; + + return ApiResponse( + statusCode: code, + data: response.data != null ? response.data["token"] : ''); } Future> deleteDeviceToken() async { - var client = await getClient(); - Response response = await client.delete( - '/auth/new_device', - options: Options( - contentType: 'application/json', - receiveDataWhenStatusError: true, - followRedirects: false, - validateStatus: (status) { - return (status != null) && - (status < HttpStatus.internalServerError); - }), - ); - client.close(); + Response response; - if (response.statusCode != null) { + var client = await getClient(); + try { + response = await client.delete('/auth/new_device'); + } on DioError catch (e) { + print(e.message); return ApiResponse( - statusCode: response.statusCode!, - data: response.data != null ? response.data : ''); + errorMessage: e.message, + statusCode: e.response?.statusCode ?? HttpStatus.internalServerError, + data: ""); + } finally { + client.close(); } - return ApiResponse(statusCode: HttpStatus.internalServerError, data: ''); + final int code = response.statusCode ?? HttpStatus.internalServerError; + + return ApiResponse( + statusCode: code, data: response.data != null ? response.data : ''); } Future>> getApiTokens() async { - var client = await getClient(); - Response response = await client.get( - '/auth/tokens', - options: Options( - contentType: 'application/json', - receiveDataWhenStatusError: true, - followRedirects: false, - validateStatus: (status) { - return (status != null) && - (status < HttpStatus.internalServerError); - }), - ); - client.close(); + Response response; - if (response.statusCode != null) { + var client = await getClient(); + try { + response = await client.get('/auth/tokens'); + } on DioError catch (e) { + print(e.message); return ApiResponse( - statusCode: response.statusCode!, - data: (response.data != null) - ? response.data - .map((e) => ApiToken.fromJson(e)) - .toList() - : []); + errorMessage: e.message, + statusCode: e.response?.statusCode ?? HttpStatus.internalServerError, + data: []); + } finally { + client.close(); } - return ApiResponse(statusCode: HttpStatus.internalServerError, data: []); + final int code = response.statusCode ?? HttpStatus.internalServerError; + + return ApiResponse( + statusCode: code, + data: (response.data != null) + ? response.data.map((e) => ApiToken.fromJson(e)).toList() + : []); } Future> refreshCurrentApiToken() async { - var client = await getClient(); - Response response = await client.post( - '/auth/tokens', - options: Options( - contentType: 'application/json', - receiveDataWhenStatusError: true, - followRedirects: false, - validateStatus: (status) { - return (status != null) && - (status < HttpStatus.internalServerError); - }), - ); - client.close(); + Response response; - if (response.statusCode != null) { + var client = await getClient(); + try { + response = await client.post('/auth/tokens'); + } on DioError catch (e) { + print(e.message); return ApiResponse( - statusCode: response.statusCode!, - data: (response.data != null) ? response.data["token"] : ''); + errorMessage: e.message, + statusCode: e.response?.statusCode ?? HttpStatus.internalServerError, + data: ""); + } finally { + client.close(); } - return ApiResponse(statusCode: HttpStatus.internalServerError, data: ''); + final int code = response.statusCode ?? HttpStatus.internalServerError; + + return ApiResponse( + statusCode: code, + data: response.data != null ? response.data["token"] : ''); } Future> deleteApiToken(String device) async { + Response response; var client = await getClient(); - Response response = await client.delete( - '/auth/tokens', - data: { - 'device': device, - }, - options: Options( - contentType: 'application/json', - receiveDataWhenStatusError: true, - followRedirects: false, - validateStatus: (status) { - return (status != null) && - (status < HttpStatus.internalServerError); - }), - ); - client.close(); - - if (response.statusCode != null) { - return ApiResponse(statusCode: response.statusCode!, data: null); + try { + response = await client.delete( + '/auth/tokens', + data: { + 'device': device, + }, + ); + } on DioError catch (e) { + print(e.message); + return ApiResponse( + errorMessage: e.message, + statusCode: e.response?.statusCode ?? HttpStatus.internalServerError, + data: null); + } finally { + client.close(); } - return ApiResponse(statusCode: HttpStatus.internalServerError, data: null); + final int code = response.statusCode ?? HttpStatus.internalServerError; + return ApiResponse(statusCode: code, data: null); } } diff --git a/lib/logic/cubit/app_config/app_config_cubit.dart b/lib/logic/cubit/app_config/app_config_cubit.dart index 030e4afe..f430f458 100644 --- a/lib/logic/cubit/app_config/app_config_cubit.dart +++ b/lib/logic/cubit/app_config/app_config_cubit.dart @@ -4,10 +4,10 @@ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/logic/get_it/ssh.dart'; -import 'package:selfprivacy/logic/models/backblaze_credential.dart'; -import 'package:selfprivacy/logic/models/server_domain.dart'; -import 'package:selfprivacy/logic/models/server_details.dart'; -import 'package:selfprivacy/logic/models/user.dart'; +import 'package:selfprivacy/logic/models/hive/backblaze_credential.dart'; +import 'package:selfprivacy/logic/models/hive/server_domain.dart'; +import 'package:selfprivacy/logic/models/hive/server_details.dart'; +import 'package:selfprivacy/logic/models/hive/user.dart'; import 'app_config_repository.dart'; @@ -20,6 +20,8 @@ class AppConfigCubit extends Cubit { final repository = AppConfigRepository(); + Timer? timer; + Future load() async { var state = await repository.load(); @@ -45,17 +47,68 @@ class AppConfigCubit extends Cubit { } } - void runDelayed( - void Function() work, Duration delay, AppConfigNotFinished? state) async { - final dataState = state ?? this.state as AppConfigNotFinished; + void setHetznerKey(String hetznerKey) async { + await repository.saveHetznerKey(hetznerKey); + emit((state as AppConfigNotFinished).copyWith(hetznerKey: hetznerKey)); + } - emit(TimerState( - dataState: dataState, - timerStart: DateTime.now(), - duration: delay, - isLoading: false, - )); - timer = Timer(delay, work); + void setCloudflareKey(String cloudFlareKey) async { + await repository.saveCloudFlareKey(cloudFlareKey); + emit( + (state as AppConfigNotFinished).copyWith(cloudFlareKey: cloudFlareKey)); + } + + void setBackblazeKey(String keyId, String applicationKey) async { + var backblazeCredential = BackblazeCredential( + keyId: keyId, + applicationKey: applicationKey, + ); + await repository.saveBackblazeKey(backblazeCredential); + emit((state as AppConfigNotFinished) + .copyWith(backblazeCredential: backblazeCredential)); + } + + void setDomain(ServerDomain serverDomain) async { + await repository.saveDomain(serverDomain); + emit((state as AppConfigNotFinished).copyWith(serverDomain: serverDomain)); + } + + void setRootUser(User rootUser) async { + await repository.saveRootUser(rootUser); + emit((state as AppConfigNotFinished).copyWith(rootUser: rootUser)); + } + + void createServerAndSetDnsRecords() async { + AppConfigNotFinished _stateCopy = state as AppConfigNotFinished; + var onSuccess = (ServerHostingDetails serverDetails) async { + await repository.createDnsRecords( + serverDetails.ip4, + state.serverDomain!, + ); + + emit((state as AppConfigNotFinished).copyWith( + isLoading: false, + serverDetails: serverDetails, + )); + runDelayed(startServerIfDnsIsOkay, Duration(seconds: 30), null); + }; + + var onCancel = + () => emit((state as AppConfigNotFinished).copyWith(isLoading: false)); + + try { + emit((state as AppConfigNotFinished).copyWith(isLoading: true)); + await repository.createServer( + state.rootUser!, + state.serverDomain!.domainName, + state.cloudFlareKey!, + state.backblazeCredential!, + onCancel: onCancel, + onSuccess: onSuccess, + ); + } catch (e) { + emit(_stateCopy); + } } void startServerIfDnsIsOkay({AppConfigNotFinished? state}) async { @@ -165,8 +218,6 @@ class AppConfigCubit extends Cubit { } } - Timer? timer; - void finishCheckIfServerIsOkay({ AppConfigNotFinished? state, }) async { @@ -186,6 +237,19 @@ class AppConfigCubit extends Cubit { } } + void runDelayed( + void Function() work, Duration delay, AppConfigNotFinished? state) async { + final dataState = state ?? this.state as AppConfigNotFinished; + + emit(TimerState( + dataState: dataState, + timerStart: DateTime.now(), + duration: delay, + isLoading: false, + )); + timer = Timer(delay, work); + } + void clearAppConfig() { closeTimer(); @@ -216,70 +280,6 @@ class AppConfigCubit extends Cubit { )); } - void setHetznerKey(String hetznerKey) async { - await repository.saveHetznerKey(hetznerKey); - emit((state as AppConfigNotFinished).copyWith(hetznerKey: hetznerKey)); - } - - void setCloudflareKey(String cloudFlareKey) async { - await repository.saveCloudFlareKey(cloudFlareKey); - emit( - (state as AppConfigNotFinished).copyWith(cloudFlareKey: cloudFlareKey)); - } - - void setBackblazeKey(String keyId, String applicationKey) async { - var backblazeCredential = BackblazeCredential( - keyId: keyId, - applicationKey: applicationKey, - ); - await repository.saveBackblazeKey(backblazeCredential); - emit((state as AppConfigNotFinished) - .copyWith(backblazeCredential: backblazeCredential)); - } - - void setDomain(ServerDomain serverDomain) async { - await repository.saveDomain(serverDomain); - emit((state as AppConfigNotFinished).copyWith(serverDomain: serverDomain)); - } - - void setRootUser(User rootUser) async { - await repository.saveRootUser(rootUser); - emit((state as AppConfigNotFinished).copyWith(rootUser: rootUser)); - } - - void createServerAndSetDnsRecords() async { - AppConfigNotFinished _stateCopy = state as AppConfigNotFinished; - var onSuccess = (ServerHostingDetails serverDetails) async { - await repository.createDnsRecords( - serverDetails.ip4, - state.serverDomain!, - ); - - emit((state as AppConfigNotFinished).copyWith( - isLoading: false, - serverDetails: serverDetails, - )); - runDelayed(startServerIfDnsIsOkay, Duration(seconds: 30), null); - }; - - var onCancel = - () => emit((state as AppConfigNotFinished).copyWith(isLoading: false)); - - try { - emit((state as AppConfigNotFinished).copyWith(isLoading: true)); - await repository.createServer( - state.rootUser!, - state.serverDomain!.domainName, - state.cloudFlareKey!, - state.backblazeCredential!, - onCancel: onCancel, - onSuccess: onSuccess, - ); - } catch (e) { - emit(_stateCopy); - } - } - close() { closeTimer(); return super.close(); diff --git a/lib/logic/cubit/app_config/app_config_repository.dart b/lib/logic/cubit/app_config/app_config_repository.dart index 363f1134..28c27db6 100644 --- a/lib/logic/cubit/app_config/app_config_repository.dart +++ b/lib/logic/cubit/app_config/app_config_repository.dart @@ -7,11 +7,11 @@ import 'package:selfprivacy/config/hive_config.dart'; import 'package:selfprivacy/logic/api_maps/cloudflare.dart'; import 'package:selfprivacy/logic/api_maps/hetzner.dart'; import 'package:selfprivacy/logic/api_maps/server.dart'; -import 'package:selfprivacy/logic/models/backblaze_credential.dart'; -import 'package:selfprivacy/logic/models/server_domain.dart'; +import 'package:selfprivacy/logic/models/hive/backblaze_credential.dart'; +import 'package:selfprivacy/logic/models/hive/server_domain.dart'; import 'package:selfprivacy/logic/models/message.dart'; -import 'package:selfprivacy/logic/models/server_details.dart'; -import 'package:selfprivacy/logic/models/user.dart'; +import 'package:selfprivacy/logic/models/hive/server_details.dart'; +import 'package:selfprivacy/logic/models/hive/user.dart'; import 'package:selfprivacy/ui/components/action_button/action_button.dart'; import 'package:selfprivacy/ui/components/brand_alert/brand_alert.dart'; @@ -51,7 +51,7 @@ class AppConfigRepository { backblazeCredential: backblazeCredential, serverDetails: serverDetails, rootUser: box.get(BNames.rootUser), - currentStep: getCurrentRecoveryStep( + currentStep: _getCurrentRecoveryStep( hetznerToken, cloudflareToken, serverDomain!, serverDetails), ); } @@ -73,7 +73,7 @@ class AppConfigRepository { ); } - RecoveryStep getCurrentRecoveryStep( + RecoveryStep _getCurrentRecoveryStep( String? hetznerToken, String? cloudflareToken, ServerDomain serverDomain, diff --git a/lib/logic/cubit/backups/backups_cubit.dart b/lib/logic/cubit/backups/backups_cubit.dart index a867dc87..8617f587 100644 --- a/lib/logic/cubit/backups/backups_cubit.dart +++ b/lib/logic/cubit/backups/backups_cubit.dart @@ -5,8 +5,8 @@ import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/logic/api_maps/backblaze.dart'; import 'package:selfprivacy/logic/api_maps/server.dart'; import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart'; -import 'package:selfprivacy/logic/models/backblaze_bucket.dart'; -import 'package:selfprivacy/logic/models/backup.dart'; +import 'package:selfprivacy/logic/models/hive/backblaze_bucket.dart'; +import 'package:selfprivacy/logic/models/json/backup.dart'; part 'backups_state.dart'; diff --git a/lib/logic/cubit/dns_records/dns_records_cubit.dart b/lib/logic/cubit/dns_records/dns_records_cubit.dart index 8efc7bfc..e32ca7ff 100644 --- a/lib/logic/cubit/dns_records/dns_records_cubit.dart +++ b/lib/logic/cubit/dns_records/dns_records_cubit.dart @@ -1,7 +1,7 @@ import 'package:cubit_form/cubit_form.dart'; import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart'; -import 'package:selfprivacy/logic/models/server_domain.dart'; -import 'package:selfprivacy/logic/models/dns_records.dart'; +import 'package:selfprivacy/logic/models/hive/server_domain.dart'; +import 'package:selfprivacy/logic/models/json/dns_records.dart'; import '../../api_maps/cloudflare.dart'; import '../../api_maps/server.dart'; diff --git a/lib/logic/cubit/forms/setup/initializing/backblaze_form_cubit.dart b/lib/logic/cubit/forms/setup/initializing/backblaze_form_cubit.dart index d8777fa8..7eb43c24 100644 --- a/lib/logic/cubit/forms/setup/initializing/backblaze_form_cubit.dart +++ b/lib/logic/cubit/forms/setup/initializing/backblaze_form_cubit.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'package:cubit_form/cubit_form.dart'; import 'package:selfprivacy/logic/api_maps/backblaze.dart'; import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart'; -import 'package:selfprivacy/logic/models/backblaze_credential.dart'; +import 'package:selfprivacy/logic/models/hive/backblaze_credential.dart'; import 'package:easy_localization/easy_localization.dart'; class BackblazeFormCubit extends FormCubit { diff --git a/lib/logic/cubit/forms/setup/initializing/domain_cloudflare.dart b/lib/logic/cubit/forms/setup/initializing/domain_cloudflare.dart index 1582a4cf..828d9b86 100644 --- a/lib/logic/cubit/forms/setup/initializing/domain_cloudflare.dart +++ b/lib/logic/cubit/forms/setup/initializing/domain_cloudflare.dart @@ -1,7 +1,7 @@ import 'package:cubit_form/cubit_form.dart'; import 'package:selfprivacy/logic/api_maps/cloudflare.dart'; import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart'; -import 'package:selfprivacy/logic/models/server_domain.dart'; +import 'package:selfprivacy/logic/models/hive/server_domain.dart'; class DomainSetupCubit extends Cubit { DomainSetupCubit(this.initializingCubit) : super(Initial()); diff --git a/lib/logic/cubit/forms/setup/initializing/root_user_form_cubit.dart b/lib/logic/cubit/forms/setup/initializing/root_user_form_cubit.dart index 102d7ac7..6d8b55c0 100644 --- a/lib/logic/cubit/forms/setup/initializing/root_user_form_cubit.dart +++ b/lib/logic/cubit/forms/setup/initializing/root_user_form_cubit.dart @@ -3,7 +3,7 @@ import 'dart:async'; import 'package:cubit_form/cubit_form.dart'; import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart'; import 'package:selfprivacy/logic/cubit/forms/factories/field_cubit_factory.dart'; -import 'package:selfprivacy/logic/models/user.dart'; +import 'package:selfprivacy/logic/models/hive/user.dart'; class RootUserFormCubit extends FormCubit { RootUserFormCubit( diff --git a/lib/logic/cubit/forms/setup/recovering/recovery_domain_form_cubit.dart b/lib/logic/cubit/forms/setup/recovering/recovery_domain_form_cubit.dart index 1b56b610..b592f5e1 100644 --- a/lib/logic/cubit/forms/setup/recovering/recovery_domain_form_cubit.dart +++ b/lib/logic/cubit/forms/setup/recovering/recovery_domain_form_cubit.dart @@ -4,7 +4,7 @@ import 'package:cubit_form/cubit_form.dart'; import 'package:selfprivacy/logic/api_maps/server.dart'; import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart'; import 'package:selfprivacy/logic/cubit/forms/factories/field_cubit_factory.dart'; -import 'package:selfprivacy/logic/models/server_domain.dart'; +import 'package:selfprivacy/logic/models/hive/server_domain.dart'; class RecoveryDomainFormCubit extends FormCubit { RecoveryDomainFormCubit( diff --git a/lib/logic/cubit/forms/user/ssh_form_cubit.dart b/lib/logic/cubit/forms/user/ssh_form_cubit.dart index d51cac6b..47bf2994 100644 --- a/lib/logic/cubit/forms/user/ssh_form_cubit.dart +++ b/lib/logic/cubit/forms/user/ssh_form_cubit.dart @@ -4,7 +4,7 @@ import 'package:cubit_form/cubit_form.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:selfprivacy/logic/cubit/jobs/jobs_cubit.dart'; import 'package:selfprivacy/logic/models/job.dart'; -import 'package:selfprivacy/logic/models/user.dart'; +import 'package:selfprivacy/logic/models/hive/user.dart'; class SshFormCubit extends FormCubit { SshFormCubit({ diff --git a/lib/logic/cubit/forms/user/user_form_cubit.dart b/lib/logic/cubit/forms/user/user_form_cubit.dart index b65cfb47..c877ff3f 100644 --- a/lib/logic/cubit/forms/user/user_form_cubit.dart +++ b/lib/logic/cubit/forms/user/user_form_cubit.dart @@ -4,7 +4,7 @@ import 'package:cubit_form/cubit_form.dart'; import 'package:selfprivacy/logic/cubit/forms/factories/field_cubit_factory.dart'; import 'package:selfprivacy/logic/cubit/jobs/jobs_cubit.dart'; import 'package:selfprivacy/logic/models/job.dart'; -import 'package:selfprivacy/logic/models/user.dart'; +import 'package:selfprivacy/logic/models/hive/user.dart'; import 'package:selfprivacy/utils/password_generator.dart'; class UserFormCubit extends FormCubit { diff --git a/lib/logic/cubit/server_detailed_info/server_detailed_info_cubit.dart b/lib/logic/cubit/server_detailed_info/server_detailed_info_cubit.dart index 86aae1a9..86b44be7 100644 --- a/lib/logic/cubit/server_detailed_info/server_detailed_info_cubit.dart +++ b/lib/logic/cubit/server_detailed_info/server_detailed_info_cubit.dart @@ -2,8 +2,8 @@ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/logic/cubit/server_detailed_info/server_detailed_info_repository.dart'; -import 'package:selfprivacy/logic/models/auto_upgrade_settings.dart'; -import 'package:selfprivacy/logic/models/hetzner_server_info.dart'; +import 'package:selfprivacy/logic/models/json/auto_upgrade_settings.dart'; +import 'package:selfprivacy/logic/models/json/hetzner_server_info.dart'; import 'package:selfprivacy/logic/models/timezone_settings.dart'; part 'server_detailed_info_state.dart'; diff --git a/lib/logic/cubit/server_detailed_info/server_detailed_info_repository.dart b/lib/logic/cubit/server_detailed_info/server_detailed_info_repository.dart index a5d6c07e..ee407da5 100644 --- a/lib/logic/cubit/server_detailed_info/server_detailed_info_repository.dart +++ b/lib/logic/cubit/server_detailed_info/server_detailed_info_repository.dart @@ -1,7 +1,7 @@ import 'package:selfprivacy/logic/api_maps/hetzner.dart'; import 'package:selfprivacy/logic/api_maps/server.dart'; -import 'package:selfprivacy/logic/models/auto_upgrade_settings.dart'; -import 'package:selfprivacy/logic/models/hetzner_server_info.dart'; +import 'package:selfprivacy/logic/models/json/auto_upgrade_settings.dart'; +import 'package:selfprivacy/logic/models/json/hetzner_server_info.dart'; import 'package:selfprivacy/logic/models/timezone_settings.dart'; class ServerDetailsRepository { diff --git a/lib/logic/cubit/users/users_cubit.dart b/lib/logic/cubit/users/users_cubit.dart index 0fd27064..22dabb46 100644 --- a/lib/logic/cubit/users/users_cubit.dart +++ b/lib/logic/cubit/users/users_cubit.dart @@ -2,7 +2,7 @@ import 'package:bloc/bloc.dart'; import 'package:hive/hive.dart'; import 'package:selfprivacy/config/hive_config.dart'; import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart'; -import 'package:selfprivacy/logic/models/user.dart'; +import 'package:selfprivacy/logic/models/hive/user.dart'; import '../../api_maps/server.dart'; diff --git a/lib/logic/get_it/api_config.dart b/lib/logic/get_it/api_config.dart index 6ab1b564..1dc5ce7d 100644 --- a/lib/logic/get_it/api_config.dart +++ b/lib/logic/get_it/api_config.dart @@ -1,9 +1,9 @@ import 'package:hive/hive.dart'; import 'package:selfprivacy/config/hive_config.dart'; -import 'package:selfprivacy/logic/models/backblaze_bucket.dart'; -import 'package:selfprivacy/logic/models/backblaze_credential.dart'; -import 'package:selfprivacy/logic/models/server_domain.dart'; -import 'package:selfprivacy/logic/models/server_details.dart'; +import 'package:selfprivacy/logic/models/hive/backblaze_bucket.dart'; +import 'package:selfprivacy/logic/models/hive/backblaze_credential.dart'; +import 'package:selfprivacy/logic/models/hive/server_domain.dart'; +import 'package:selfprivacy/logic/models/hive/server_details.dart'; class ApiConfigModel { Box _box = Hive.box(BNames.appConfig); diff --git a/lib/logic/models/hive/README.md b/lib/logic/models/hive/README.md new file mode 100644 index 00000000..afdd6276 --- /dev/null +++ b/lib/logic/models/hive/README.md @@ -0,0 +1,13 @@ +# Registered Hive Types + +1. User +2. ServerHostingDetails +3. ServerDomain +4. BackblazeCredential +5. ServerVolume +6. BackblazeBucket + + +## Enums +100. DnsProvider +101. ServerProvider \ No newline at end of file diff --git a/lib/logic/models/backblaze_bucket.dart b/lib/logic/models/hive/backblaze_bucket.dart similarity index 100% rename from lib/logic/models/backblaze_bucket.dart rename to lib/logic/models/hive/backblaze_bucket.dart diff --git a/lib/logic/models/backblaze_bucket.g.dart b/lib/logic/models/hive/backblaze_bucket.g.dart similarity index 100% rename from lib/logic/models/backblaze_bucket.g.dart rename to lib/logic/models/hive/backblaze_bucket.g.dart diff --git a/lib/logic/models/backblaze_credential.dart b/lib/logic/models/hive/backblaze_credential.dart similarity index 100% rename from lib/logic/models/backblaze_credential.dart rename to lib/logic/models/hive/backblaze_credential.dart diff --git a/lib/logic/models/backblaze_credential.g.dart b/lib/logic/models/hive/backblaze_credential.g.dart similarity index 100% rename from lib/logic/models/backblaze_credential.g.dart rename to lib/logic/models/hive/backblaze_credential.g.dart diff --git a/lib/logic/models/server_details.dart b/lib/logic/models/hive/server_details.dart similarity index 74% rename from lib/logic/models/server_details.dart rename to lib/logic/models/hive/server_details.dart index d12c1b36..e9bb5f38 100644 --- a/lib/logic/models/server_details.dart +++ b/lib/logic/models/hive/server_details.dart @@ -8,8 +8,9 @@ class ServerHostingDetails { required this.ip4, required this.id, required this.createTime, - required this.dataBase, + required this.volume, required this.apiToken, + required this.provider, this.startTime, }); @@ -26,19 +27,23 @@ class ServerHostingDetails { final DateTime? startTime; @HiveField(4) - final ServerVolume dataBase; + final ServerVolume volume; @HiveField(5) final String apiToken; + @HiveField(6, defaultValue: ServerProvider.Hetzner) + final ServerProvider provider; + ServerHostingDetails copyWith({DateTime? startTime}) { return ServerHostingDetails( startTime: startTime ?? this.startTime, createTime: createTime, id: id, ip4: ip4, - dataBase: dataBase, + volume: volume, apiToken: apiToken, + provider: provider, ); } @@ -57,3 +62,11 @@ class ServerVolume { @HiveField(2) String name; } + +@HiveType(typeId: 101) +enum ServerProvider { + @HiveField(0) + Unknown, + @HiveField(1) + Hetzner, +} diff --git a/lib/logic/models/server_details.g.dart b/lib/logic/models/hive/server_details.g.dart similarity index 60% rename from lib/logic/models/server_details.g.dart rename to lib/logic/models/hive/server_details.g.dart index 61252506..f52e6b37 100644 --- a/lib/logic/models/server_details.g.dart +++ b/lib/logic/models/hive/server_details.g.dart @@ -6,7 +6,7 @@ part of 'server_details.dart'; // TypeAdapterGenerator // ************************************************************************** -class HetznerServerDetailsAdapter extends TypeAdapter { +class ServerHostingDetailsAdapter extends TypeAdapter { @override final int typeId = 2; @@ -20,8 +20,11 @@ class HetznerServerDetailsAdapter extends TypeAdapter { ip4: fields[0] as String, id: fields[1] as int, createTime: fields[3] as DateTime?, - dataBase: fields[4] as ServerVolume, + volume: fields[4] as ServerVolume, apiToken: fields[5] as String, + provider: fields[6] == null + ? ServerProvider.Hetzner + : fields[6] as ServerProvider, startTime: fields[2] as DateTime?, ); } @@ -29,7 +32,7 @@ class HetznerServerDetailsAdapter extends TypeAdapter { @override void write(BinaryWriter writer, ServerHostingDetails obj) { writer - ..writeByte(6) + ..writeByte(7) ..writeByte(0) ..write(obj.ip4) ..writeByte(1) @@ -39,9 +42,11 @@ class HetznerServerDetailsAdapter extends TypeAdapter { ..writeByte(2) ..write(obj.startTime) ..writeByte(4) - ..write(obj.dataBase) + ..write(obj.volume) ..writeByte(5) - ..write(obj.apiToken); + ..write(obj.apiToken) + ..writeByte(6) + ..write(obj.provider); } @override @@ -50,12 +55,12 @@ class HetznerServerDetailsAdapter extends TypeAdapter { @override bool operator ==(Object other) => identical(this, other) || - other is HetznerServerDetailsAdapter && + other is ServerHostingDetailsAdapter && runtimeType == other.runtimeType && typeId == other.typeId; } -class HetznerDataBaseAdapter extends TypeAdapter { +class ServerVolumeAdapter extends TypeAdapter { @override final int typeId = 5; @@ -87,7 +92,46 @@ class HetznerDataBaseAdapter extends TypeAdapter { @override bool operator ==(Object other) => identical(this, other) || - other is HetznerDataBaseAdapter && + other is ServerVolumeAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + +class ServerProviderAdapter extends TypeAdapter { + @override + final int typeId = 101; + + @override + ServerProvider read(BinaryReader reader) { + switch (reader.readByte()) { + case 0: + return ServerProvider.Unknown; + case 1: + return ServerProvider.Hetzner; + default: + return ServerProvider.Unknown; + } + } + + @override + void write(BinaryWriter writer, ServerProvider obj) { + switch (obj) { + case ServerProvider.Unknown: + writer.writeByte(0); + break; + case ServerProvider.Hetzner: + writer.writeByte(1); + break; + } + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is ServerProviderAdapter && runtimeType == other.runtimeType && typeId == other.typeId; } diff --git a/lib/logic/models/server_domain.dart b/lib/logic/models/hive/server_domain.dart similarity index 90% rename from lib/logic/models/server_domain.dart rename to lib/logic/models/hive/server_domain.dart index 81525d2f..ab60019e 100644 --- a/lib/logic/models/server_domain.dart +++ b/lib/logic/models/hive/server_domain.dart @@ -2,11 +2,6 @@ import 'package:hive/hive.dart'; part 'server_domain.g.dart'; -enum DnsProvider { - Unknown, - Cloudflare, -} - @HiveType(typeId: 3) class ServerDomain { ServerDomain({ @@ -29,3 +24,11 @@ class ServerDomain { return '$domainName: $zoneId'; } } + +@HiveType(typeId: 100) +enum DnsProvider { + @HiveField(0) + Unknown, + @HiveField(1) + Cloudflare, +} diff --git a/lib/logic/models/server_domain.g.dart b/lib/logic/models/hive/server_domain.g.dart similarity index 60% rename from lib/logic/models/server_domain.g.dart rename to lib/logic/models/hive/server_domain.g.dart index c154fba1..7a3eb4de 100644 --- a/lib/logic/models/server_domain.g.dart +++ b/lib/logic/models/hive/server_domain.g.dart @@ -46,3 +46,42 @@ class ServerDomainAdapter extends TypeAdapter { runtimeType == other.runtimeType && typeId == other.typeId; } + +class DnsProviderAdapter extends TypeAdapter { + @override + final int typeId = 100; + + @override + DnsProvider read(BinaryReader reader) { + switch (reader.readByte()) { + case 0: + return DnsProvider.Unknown; + case 1: + return DnsProvider.Cloudflare; + default: + return DnsProvider.Unknown; + } + } + + @override + void write(BinaryWriter writer, DnsProvider obj) { + switch (obj) { + case DnsProvider.Unknown: + writer.writeByte(0); + break; + case DnsProvider.Cloudflare: + writer.writeByte(1); + break; + } + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is DnsProviderAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} diff --git a/lib/logic/models/user.dart b/lib/logic/models/hive/user.dart similarity index 100% rename from lib/logic/models/user.dart rename to lib/logic/models/hive/user.dart diff --git a/lib/logic/models/user.g.dart b/lib/logic/models/hive/user.g.dart similarity index 100% rename from lib/logic/models/user.g.dart rename to lib/logic/models/hive/user.g.dart diff --git a/lib/logic/models/job.dart b/lib/logic/models/job.dart index 698cd1fb..38ef2022 100644 --- a/lib/logic/models/job.dart +++ b/lib/logic/models/job.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; import 'package:selfprivacy/logic/common_enum/common_enum.dart'; import 'package:selfprivacy/utils/password_generator.dart'; -import 'user.dart'; +import 'hive/user.dart'; @immutable class Job extends Equatable { diff --git a/lib/logic/models/api_token.dart b/lib/logic/models/json/api_token.dart similarity index 100% rename from lib/logic/models/api_token.dart rename to lib/logic/models/json/api_token.dart diff --git a/lib/logic/models/api_token.g.dart b/lib/logic/models/json/api_token.g.dart similarity index 100% rename from lib/logic/models/api_token.g.dart rename to lib/logic/models/json/api_token.g.dart diff --git a/lib/logic/models/auto_upgrade_settings.dart b/lib/logic/models/json/auto_upgrade_settings.dart similarity index 100% rename from lib/logic/models/auto_upgrade_settings.dart rename to lib/logic/models/json/auto_upgrade_settings.dart diff --git a/lib/logic/models/auto_upgrade_settings.g.dart b/lib/logic/models/json/auto_upgrade_settings.g.dart similarity index 100% rename from lib/logic/models/auto_upgrade_settings.g.dart rename to lib/logic/models/json/auto_upgrade_settings.g.dart diff --git a/lib/logic/models/backup.dart b/lib/logic/models/json/backup.dart similarity index 100% rename from lib/logic/models/backup.dart rename to lib/logic/models/json/backup.dart diff --git a/lib/logic/models/backup.g.dart b/lib/logic/models/json/backup.g.dart similarity index 100% rename from lib/logic/models/backup.g.dart rename to lib/logic/models/json/backup.g.dart diff --git a/lib/logic/models/device_token.dart b/lib/logic/models/json/device_token.dart similarity index 100% rename from lib/logic/models/device_token.dart rename to lib/logic/models/json/device_token.dart diff --git a/lib/logic/models/device_token.g.dart b/lib/logic/models/json/device_token.g.dart similarity index 100% rename from lib/logic/models/device_token.g.dart rename to lib/logic/models/json/device_token.g.dart diff --git a/lib/logic/models/dns_records.dart b/lib/logic/models/json/dns_records.dart similarity index 100% rename from lib/logic/models/dns_records.dart rename to lib/logic/models/json/dns_records.dart diff --git a/lib/logic/models/dns_records.g.dart b/lib/logic/models/json/dns_records.g.dart similarity index 100% rename from lib/logic/models/dns_records.g.dart rename to lib/logic/models/json/dns_records.g.dart diff --git a/lib/logic/models/hetzner_server_info.dart b/lib/logic/models/json/hetzner_server_info.dart similarity index 100% rename from lib/logic/models/hetzner_server_info.dart rename to lib/logic/models/json/hetzner_server_info.dart diff --git a/lib/logic/models/hetzner_server_info.g.dart b/lib/logic/models/json/hetzner_server_info.g.dart similarity index 100% rename from lib/logic/models/hetzner_server_info.g.dart rename to lib/logic/models/json/hetzner_server_info.g.dart diff --git a/lib/logic/models/recovery_token_status.dart b/lib/logic/models/json/recovery_token_status.dart similarity index 100% rename from lib/logic/models/recovery_token_status.dart rename to lib/logic/models/json/recovery_token_status.dart diff --git a/lib/logic/models/recovery_token_status.g.dart b/lib/logic/models/json/recovery_token_status.g.dart similarity index 100% rename from lib/logic/models/recovery_token_status.g.dart rename to lib/logic/models/json/recovery_token_status.g.dart diff --git a/lib/logic/models/server_configurations.dart b/lib/logic/models/json/server_configurations.dart similarity index 100% rename from lib/logic/models/server_configurations.dart rename to lib/logic/models/json/server_configurations.dart diff --git a/lib/logic/models/server_configurations.g.dart b/lib/logic/models/json/server_configurations.g.dart similarity index 100% rename from lib/logic/models/server_configurations.g.dart rename to lib/logic/models/json/server_configurations.g.dart diff --git a/lib/ui/pages/backup_details/backup_details.dart b/lib/ui/pages/backup_details/backup_details.dart index 3040d1aa..a3562095 100644 --- a/lib/ui/pages/backup_details/backup_details.dart +++ b/lib/ui/pages/backup_details/backup_details.dart @@ -4,7 +4,7 @@ import 'package:selfprivacy/config/brand_colors.dart'; import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart'; import 'package:selfprivacy/logic/cubit/backups/backups_cubit.dart'; -import 'package:selfprivacy/logic/models/backup.dart'; +import 'package:selfprivacy/logic/models/json/backup.dart'; import 'package:selfprivacy/logic/models/state_types.dart'; import 'package:selfprivacy/ui/components/action_button/action_button.dart'; import 'package:selfprivacy/ui/components/brand_alert/brand_alert.dart'; diff --git a/lib/ui/pages/setup/initializing.dart b/lib/ui/pages/setup/initializing.dart index d64d7c3b..5048aa57 100644 --- a/lib/ui/pages/setup/initializing.dart +++ b/lib/ui/pages/setup/initializing.dart @@ -35,7 +35,7 @@ class InitializingPage extends StatelessWidget { () => _stepCheck(cubit), () => _stepCheck(cubit), () => Container(child: Center(child: Text('initializing.finish'.tr()))) - ][cubit.state.progress](); + ][cubit.state.progress.index](); return BlocListener( listener: (context, state) { if (cubit.state is AppConfigFinished) { diff --git a/lib/ui/pages/ssh_keys/ssh_keys.dart b/lib/ui/pages/ssh_keys/ssh_keys.dart index b67087c6..3967bf65 100644 --- a/lib/ui/pages/ssh_keys/ssh_keys.dart +++ b/lib/ui/pages/ssh_keys/ssh_keys.dart @@ -11,7 +11,7 @@ import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart'; import '../../../config/brand_colors.dart'; import '../../../config/brand_theme.dart'; import '../../../logic/cubit/jobs/jobs_cubit.dart'; -import '../../../logic/models/user.dart'; +import '../../../logic/models/hive/user.dart'; import '../../components/brand_button/brand_button.dart'; import '../../components/brand_header/brand_header.dart'; diff --git a/lib/ui/pages/users/users.dart b/lib/ui/pages/users/users.dart index 013f65b0..7110f986 100644 --- a/lib/ui/pages/users/users.dart +++ b/lib/ui/pages/users/users.dart @@ -11,7 +11,7 @@ import 'package:selfprivacy/logic/cubit/forms/user/user_form_cubit.dart'; import 'package:selfprivacy/logic/cubit/jobs/jobs_cubit.dart'; import 'package:selfprivacy/logic/cubit/users/users_cubit.dart'; import 'package:selfprivacy/logic/models/job.dart'; -import 'package:selfprivacy/logic/models/user.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_divider/brand_divider.dart'; From cc91b14b447e214ba1b56365f0fbc018d9f57cca Mon Sep 17 00:00:00 2001 From: Inex Code Date: Mon, 16 May 2022 23:30:14 +0300 Subject: [PATCH 08/52] Migrate to flutter 3 --- android/app/build.gradle | 14 +- android/build.gradle | 4 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- lib/logic/api_maps/server.dart | 1 - lib/logic/common_enum/common_enum.dart | 1 + .../cubit/forms/user/user_form_cubit.dart | 4 +- .../components/pre_styled_buttons/flash.dart | 2 +- .../server_details/time_zone/time_zone.dart | 2 +- pubspec.lock | 196 ++++++++++-------- pubspec.yaml | 8 +- 10 files changed, 129 insertions(+), 105 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 1b726e4e..0963724e 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -26,12 +26,22 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 31 + compileSdkVersion flutter.compileSdkVersion + ndkVersion flutter.ndkVersion sourceSets { main.java.srcDirs += 'src/main/kotlin' } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + lintOptions { disable 'InvalidPackage' } @@ -39,7 +49,7 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "pro.kherel.selfprivacy" - minSdkVersion 18 + minSdkVersion 21 targetSdkVersion 31 versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/android/build.gradle b/android/build.gradle index b3afb285..31e95773 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,12 +1,12 @@ buildscript { - ext.kotlin_version = '1.5.10' + ext.kotlin_version = '1.6.10' repositories { google() jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:4.1.0' + classpath 'com.android.tools.build:gradle:7.1.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index f8865307..cc5527d7 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.9.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip diff --git a/lib/logic/api_maps/server.dart b/lib/logic/api_maps/server.dart index 27c0f5c2..0cafbf6b 100644 --- a/lib/logic/api_maps/server.dart +++ b/lib/logic/api_maps/server.dart @@ -13,7 +13,6 @@ import 'package:selfprivacy/logic/models/json/recovery_token_status.dart'; import 'package:selfprivacy/logic/models/json/device_token.dart'; import 'package:selfprivacy/logic/models/timezone_settings.dart'; import 'package:selfprivacy/logic/models/hive/user.dart'; -import 'package:timezone/browser.dart'; import 'api_map.dart'; diff --git a/lib/logic/common_enum/common_enum.dart b/lib/logic/common_enum/common_enum.dart index 8568c383..41fe6528 100644 --- a/lib/logic/common_enum/common_enum.dart +++ b/lib/logic/common_enum/common_enum.dart @@ -13,6 +13,7 @@ enum InitializingSteps { startServer, checkSystemDnsAndDkimSet, } + enum Period { hour, day, month } enum ServiceTypes { diff --git a/lib/logic/cubit/forms/user/user_form_cubit.dart b/lib/logic/cubit/forms/user/user_form_cubit.dart index c877ff3f..dbe19ad5 100644 --- a/lib/logic/cubit/forms/user/user_form_cubit.dart +++ b/lib/logic/cubit/forms/user/user_form_cubit.dart @@ -16,10 +16,10 @@ class UserFormCubit extends FormCubit { var isEdit = user != null; login = fieldFactory.createUserLoginField(); - login.setValue(isEdit ? user!.login : ''); + login.setValue(isEdit ? user.login : ''); password = fieldFactory.createUserPasswordField(); password.setValue( - isEdit ? (user?.password ?? '') : StringGenerators.userPassword()); + isEdit ? (user.password ?? '') : StringGenerators.userPassword()); super.addFields([login, password]); } diff --git a/lib/ui/components/pre_styled_buttons/flash.dart b/lib/ui/components/pre_styled_buttons/flash.dart index 5e9b1875..3888af29 100644 --- a/lib/ui/components/pre_styled_buttons/flash.dart +++ b/lib/ui/components/pre_styled_buttons/flash.dart @@ -22,7 +22,7 @@ class _BrandFlashButtonState extends State<_BrandFlashButton> ).animate(_animationController); super.initState(); - WidgetsBinding.instance!.addPostFrameCallback(_afterLayout); + WidgetsBinding.instance.addPostFrameCallback(_afterLayout); } void _afterLayout(_) { 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 04f192c0..9802bbb8 100644 --- a/lib/ui/pages/server_details/time_zone/time_zone.dart +++ b/lib/ui/pages/server_details/time_zone/time_zone.dart @@ -16,7 +16,7 @@ class _SelectTimezoneState extends State { @override void initState() { - WidgetsBinding.instance!.addPostFrameCallback(_afterLayout); + WidgetsBinding.instance.addPostFrameCallback(_afterLayout); super.initState(); } diff --git a/pubspec.lock b/pubspec.lock index 681a805a..9d1e3744 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -7,28 +7,28 @@ packages: name: _fe_analyzer_shared url: "https://pub.dartlang.org" source: hosted - version: "31.0.0" + version: "38.0.0" analyzer: dependency: transitive description: name: analyzer url: "https://pub.dartlang.org" source: hosted - version: "2.8.0" + version: "3.4.1" archive: dependency: transitive description: name: archive url: "https://pub.dartlang.org" source: hosted - version: "3.2.0" + version: "3.3.0" args: dependency: transitive description: name: args url: "https://pub.dartlang.org" source: hosted - version: "2.3.0" + version: "2.3.1" asn1lib: dependency: transitive description: @@ -56,14 +56,14 @@ packages: name: basic_utils url: "https://pub.dartlang.org" source: hosted - version: "4.2.0" + version: "4.2.2" bloc: dependency: transitive description: name: bloc url: "https://pub.dartlang.org" source: hosted - version: "8.0.2" + version: "8.0.3" boolean_selector: dependency: transitive description: @@ -77,7 +77,7 @@ packages: name: build url: "https://pub.dartlang.org" source: hosted - version: "2.2.1" + version: "2.3.0" build_config: dependency: transitive description: @@ -91,21 +91,21 @@ packages: name: build_daemon url: "https://pub.dartlang.org" source: hosted - version: "3.0.1" + version: "3.1.0" build_resolvers: dependency: transitive description: name: build_resolvers url: "https://pub.dartlang.org" source: hosted - version: "2.0.6" + version: "2.0.8" build_runner: dependency: "direct dev" description: name: build_runner url: "https://pub.dartlang.org" source: hosted - version: "2.1.7" + version: "2.1.11" build_runner_core: dependency: transitive description: @@ -126,7 +126,7 @@ packages: name: built_value url: "https://pub.dartlang.org" source: hosted - version: "8.1.4" + version: "8.3.0" characters: dependency: transitive description: @@ -148,13 +148,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.1" - cli_util: - dependency: transitive - description: - name: cli_util - url: "https://pub.dartlang.org" - source: hosted - version: "0.3.5" clock: dependency: transitive description: @@ -175,7 +168,7 @@ packages: name: collection url: "https://pub.dartlang.org" source: hosted - version: "1.15.0" + version: "1.16.0" convert: dependency: transitive description: @@ -189,7 +182,7 @@ packages: name: coverage url: "https://pub.dartlang.org" source: hosted - version: "1.0.3" + version: "1.3.1" crypt: dependency: "direct main" description: @@ -203,7 +196,7 @@ packages: name: crypto url: "https://pub.dartlang.org" source: hosted - version: "3.0.1" + version: "3.0.2" cubit_form: dependency: "direct main" description: @@ -224,21 +217,21 @@ packages: name: dart_style url: "https://pub.dartlang.org" source: hosted - version: "2.2.1" + version: "2.2.3" dio: dependency: "direct main" description: name: dio url: "https://pub.dartlang.org" source: hosted - version: "4.0.4" + version: "4.0.6" easy_localization: dependency: "direct main" description: name: easy_localization url: "https://pub.dartlang.org" source: hosted - version: "3.0.0" + version: "3.0.1" easy_logger: dependency: transitive description: @@ -273,14 +266,14 @@ packages: name: fake_async url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.3.0" ffi: dependency: transitive description: name: ffi url: "https://pub.dartlang.org" source: hosted - version: "1.1.2" + version: "1.2.1" file: dependency: transitive description: @@ -294,14 +287,14 @@ packages: name: fixnum url: "https://pub.dartlang.org" source: hosted - version: "1.0.0" + version: "1.0.1" fl_chart: dependency: "direct main" description: name: fl_chart url: "https://pub.dartlang.org" source: hosted - version: "0.50.1" + version: "0.50.5" flutter: dependency: "direct main" description: flutter @@ -332,14 +325,14 @@ packages: name: flutter_markdown url: "https://pub.dartlang.org" source: hosted - version: "0.6.9" + version: "0.6.10" flutter_plugin_android_lifecycle: dependency: transitive description: name: flutter_plugin_android_lifecycle url: "https://pub.dartlang.org" source: hosted - version: "2.0.5" + version: "2.0.6" flutter_secure_storage: dependency: "direct main" description: @@ -426,7 +419,7 @@ packages: name: hive url: "https://pub.dartlang.org" source: hosted - version: "2.0.5" + version: "2.1.0" hive_flutter: dependency: "direct main" description: @@ -468,7 +461,7 @@ packages: name: image url: "https://pub.dartlang.org" source: hosted - version: "3.1.1" + version: "3.1.3" intl: dependency: transitive description: @@ -496,28 +489,49 @@ packages: name: js url: "https://pub.dartlang.org" source: hosted - version: "0.6.3" + version: "0.6.4" json_annotation: dependency: "direct main" description: name: json_annotation url: "https://pub.dartlang.org" source: hosted - version: "4.4.0" + version: "4.5.0" json_serializable: dependency: "direct dev" description: name: json_serializable url: "https://pub.dartlang.org" source: hosted - version: "6.1.4" + version: "6.2.0" local_auth: dependency: "direct main" description: name: local_auth url: "https://pub.dartlang.org" source: hosted - version: "1.1.11" + version: "2.0.2" + local_auth_android: + dependency: transitive + description: + name: local_auth_android + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.4" + local_auth_ios: + dependency: transitive + description: + name: local_auth_ios + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.5" + local_auth_platform_interface: + dependency: transitive + description: + name: local_auth_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.3" logging: dependency: transitive description: @@ -531,14 +545,14 @@ packages: name: markdown url: "https://pub.dartlang.org" source: hosted - version: "4.0.1" + version: "5.0.0" mask_text_input_formatter: dependency: transitive description: name: mask_text_input_formatter url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.3.0" matcher: dependency: transitive description: @@ -552,7 +566,7 @@ packages: name: material_color_utilities url: "https://pub.dartlang.org" source: hosted - version: "0.1.3" + version: "0.1.4" meta: dependency: transitive description: @@ -566,14 +580,14 @@ packages: name: mime url: "https://pub.dartlang.org" source: hosted - version: "1.0.1" + version: "1.0.2" modal_bottom_sheet: dependency: "direct main" description: name: modal_bottom_sheet url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.0.1" nanoid: dependency: "direct main" description: @@ -615,63 +629,63 @@ packages: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.8.0" + version: "1.8.1" path_provider: dependency: transitive description: name: path_provider url: "https://pub.dartlang.org" source: hosted - version: "2.0.9" + version: "2.0.10" path_provider_android: dependency: transitive description: name: path_provider_android url: "https://pub.dartlang.org" source: hosted - version: "2.0.11" + version: "2.0.14" path_provider_ios: dependency: transitive description: name: path_provider_ios url: "https://pub.dartlang.org" source: hosted - version: "2.0.7" + version: "2.0.9" path_provider_linux: dependency: transitive description: name: path_provider_linux url: "https://pub.dartlang.org" source: hosted - version: "2.1.5" + version: "2.1.6" path_provider_macos: dependency: transitive description: name: path_provider_macos url: "https://pub.dartlang.org" source: hosted - version: "2.0.5" + version: "2.0.6" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.0.3" + version: "2.0.4" path_provider_windows: dependency: transitive description: name: path_provider_windows url: "https://pub.dartlang.org" source: hosted - version: "2.0.5" + version: "2.0.6" petitparser: dependency: transitive description: name: petitparser url: "https://pub.dartlang.org" source: hosted - version: "4.4.0" + version: "5.0.0" platform: dependency: transitive description: @@ -692,7 +706,7 @@ packages: name: pointycastle url: "https://pub.dartlang.org" source: hosted - version: "3.5.1" + version: "3.6.0" pool: dependency: transitive description: @@ -727,7 +741,7 @@ packages: name: pub_semver url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.1.1" pubspec_parse: dependency: transitive description: @@ -741,7 +755,7 @@ packages: name: quiver url: "https://pub.dartlang.org" source: hosted - version: "3.0.1+1" + version: "3.1.0" rsa_encrypt: dependency: "direct main" description: @@ -755,77 +769,77 @@ packages: name: share_plus url: "https://pub.dartlang.org" source: hosted - version: "3.0.5" + version: "4.0.4" share_plus_linux: dependency: transitive description: name: share_plus_linux url: "https://pub.dartlang.org" source: hosted - version: "2.0.4" + version: "3.0.0" share_plus_macos: dependency: transitive description: name: share_plus_macos url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "3.0.0" share_plus_platform_interface: dependency: transitive description: name: share_plus_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.0.1" + version: "3.0.2" share_plus_web: dependency: transitive description: name: share_plus_web url: "https://pub.dartlang.org" source: hosted - version: "2.0.4" + version: "3.0.0" share_plus_windows: dependency: transitive description: name: share_plus_windows url: "https://pub.dartlang.org" source: hosted - version: "2.0.3" + version: "3.0.0" shared_preferences: dependency: transitive description: name: shared_preferences url: "https://pub.dartlang.org" source: hosted - version: "2.0.13" + version: "2.0.15" shared_preferences_android: dependency: transitive description: name: shared_preferences_android url: "https://pub.dartlang.org" source: hosted - version: "2.0.11" + version: "2.0.12" shared_preferences_ios: dependency: transitive description: name: shared_preferences_ios url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.1.1" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.1.1" shared_preferences_macos: dependency: transitive description: name: shared_preferences_macos url: "https://pub.dartlang.org" source: hosted - version: "2.0.3" + version: "2.0.4" shared_preferences_platform_interface: dependency: transitive description: @@ -839,21 +853,21 @@ packages: name: shared_preferences_web url: "https://pub.dartlang.org" source: hosted - version: "2.0.3" + version: "2.0.4" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.1.1" shelf: dependency: transitive description: name: shelf url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.3.0" shelf_packages_handler: dependency: transitive description: @@ -886,14 +900,14 @@ packages: name: source_gen url: "https://pub.dartlang.org" source: hosted - version: "1.2.1" + version: "1.2.2" source_helper: dependency: transitive description: name: source_helper url: "https://pub.dartlang.org" source: hosted - version: "1.3.1" + version: "1.3.2" source_map_stack_trace: dependency: transitive description: @@ -914,7 +928,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.8.1" + version: "1.8.2" ssh_key: dependency: "direct main" description: @@ -963,21 +977,21 @@ packages: name: test url: "https://pub.dartlang.org" source: hosted - version: "1.19.5" + version: "1.21.1" test_api: dependency: transitive description: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.8" + version: "0.4.9" test_core: dependency: transitive description: name: test_core url: "https://pub.dartlang.org" source: hosted - version: "0.4.9" + version: "0.4.13" timezone: dependency: "direct main" description: @@ -1005,42 +1019,42 @@ packages: name: typed_data url: "https://pub.dartlang.org" source: hosted - version: "1.3.0" + version: "1.3.1" url_launcher: dependency: "direct main" description: name: url_launcher url: "https://pub.dartlang.org" source: hosted - version: "6.0.20" + version: "6.1.2" url_launcher_android: dependency: transitive description: name: url_launcher_android url: "https://pub.dartlang.org" source: hosted - version: "6.0.15" + version: "6.0.17" url_launcher_ios: dependency: transitive description: name: url_launcher_ios url: "https://pub.dartlang.org" source: hosted - version: "6.0.15" + version: "6.0.16" url_launcher_linux: dependency: transitive description: name: url_launcher_linux url: "https://pub.dartlang.org" source: hosted - version: "3.0.0" + version: "3.0.1" url_launcher_macos: dependency: transitive description: name: url_launcher_macos url: "https://pub.dartlang.org" source: hosted - version: "3.0.0" + version: "3.0.1" url_launcher_platform_interface: dependency: transitive description: @@ -1054,35 +1068,35 @@ packages: name: url_launcher_web url: "https://pub.dartlang.org" source: hosted - version: "2.0.8" + version: "2.0.11" url_launcher_windows: dependency: transitive description: name: url_launcher_windows url: "https://pub.dartlang.org" source: hosted - version: "3.0.0" + version: "3.0.1" vector_math: dependency: transitive description: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.1.1" + version: "2.1.2" vm_service: dependency: transitive description: name: vm_service url: "https://pub.dartlang.org" source: hosted - version: "7.5.0" + version: "8.3.0" wakelock: dependency: "direct main" description: name: wakelock url: "https://pub.dartlang.org" source: hosted - version: "0.6.1+1" + version: "0.6.1+2" wakelock_macos: dependency: transitive description: @@ -1124,21 +1138,21 @@ packages: name: web_socket_channel url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.2.0" webkit_inspection_protocol: dependency: transitive description: name: webkit_inspection_protocol url: "https://pub.dartlang.org" source: hosted - version: "1.0.0" + version: "1.0.1" win32: dependency: transitive description: name: win32 url: "https://pub.dartlang.org" source: hosted - version: "2.4.1" + version: "2.6.1" xdg_directories: dependency: transitive description: @@ -1152,14 +1166,14 @@ packages: name: xml url: "https://pub.dartlang.org" source: hosted - version: "5.3.1" + version: "5.4.1" yaml: dependency: transitive description: name: yaml url: "https://pub.dartlang.org" source: hosted - version: "3.1.0" + version: "3.1.1" sdks: - dart: ">=2.15.0 <3.0.0" - flutter: ">=2.10.0" + dart: ">=2.17.0 <3.0.0" + flutter: ">=3.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index 29a3100c..2e71ab51 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,8 +4,8 @@ publish_to: 'none' version: 0.5.3+14 environment: - sdk: '>=2.13.4 <3.0.0' - flutter: ">=2.10.0" + sdk: '>=2.17.0 <3.0.0' + flutter: ">=3.0.0" dependencies: auto_size_text: ^3.0.0 @@ -28,7 +28,7 @@ dependencies: hive_flutter: ^1.1.0 ionicons: ^0.1.2 json_annotation: ^4.4.0 - local_auth: ^1.1.11 + local_auth: ^2.0.2 modal_bottom_sheet: ^2.0.0 nanoid: ^1.0.0 package_info: ^2.0.2 @@ -36,7 +36,7 @@ dependencies: pretty_dio_logger: ^1.2.0-beta-1 provider: ^6.0.2 rsa_encrypt: ^2.0.0 - share_plus: ^3.0.5 + share_plus: ^4.0.4 ssh_key: ^0.7.1 timezone: ^0.8.0 url_launcher: ^6.0.20 From 10488d6832d9128c1c3366a7b27b850ca31a8e0b Mon Sep 17 00:00:00 2001 From: NaiJi Date: Tue, 17 May 2022 01:15:16 +0300 Subject: [PATCH 09/52] Fix application failure on cloudflare 403 response Check error response and show modal dialogue if domain couldn't be registered --- assets/translations/en.json | 1 + assets/translations/ru.json | 1 + lib/logic/api_maps/cloudflare.dart | 30 +++++++------ lib/logic/api_maps/server.dart | 1 - .../cubit/app_config/app_config_cubit.dart | 7 +-- .../app_config/app_config_repository.dart | 44 ++++++++++++++++--- 6 files changed, 60 insertions(+), 24 deletions(-) diff --git a/assets/translations/en.json b/assets/translations/en.json index eb36af5a..c3245f9d 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -299,6 +299,7 @@ "7": "Yes", "8": "Remove task", "9": "Reboot", + "10": "You cannot use this API for domains with such TLD.", "yes": "Yes" }, "timer": { diff --git a/assets/translations/ru.json b/assets/translations/ru.json index 7198a040..7f46b137 100644 --- a/assets/translations/ru.json +++ b/assets/translations/ru.json @@ -295,6 +295,7 @@ "7": "Да, удалить", "8": "Удалить задачу", "9": "Перезагрузить", + "10": "API не поддерживает домены с таким TLD.", "yes": "Да" }, "timer": { diff --git a/lib/logic/api_maps/cloudflare.dart b/lib/logic/api_maps/cloudflare.dart index 9da5ecee..c0d7334b 100644 --- a/lib/logic/api_maps/cloudflare.dart +++ b/lib/logic/api_maps/cloudflare.dart @@ -125,23 +125,25 @@ class CloudflareApi extends ApiMap { var domainName = cloudFlareDomain.domainName; var domainZoneId = cloudFlareDomain.zoneId; var listDnsRecords = projectDnsRecords(domainName, ip4); - - var url = '$rootAddress/zones/$domainZoneId/dns_records'; - var allCreateFutures = []; + var client = await getClient(); - - for (var record in listDnsRecords) { - allCreateFutures.add( - client.post( - url, - data: record.toJson(), - ), - ); + try { + for (var record in listDnsRecords) { + allCreateFutures.add( + client.post( + '/zones/$domainZoneId/dns_records', + data: record.toJson(), + ), + ); + } + await Future.wait(allCreateFutures); + } on DioError catch (e) { + print(e.message); + throw e; + } finally { + close(client); } - - await Future.wait(allCreateFutures); - close(client); } List projectDnsRecords(String? domainName, String? ip4) { diff --git a/lib/logic/api_maps/server.dart b/lib/logic/api_maps/server.dart index 27c0f5c2..0cafbf6b 100644 --- a/lib/logic/api_maps/server.dart +++ b/lib/logic/api_maps/server.dart @@ -13,7 +13,6 @@ import 'package:selfprivacy/logic/models/json/recovery_token_status.dart'; import 'package:selfprivacy/logic/models/json/device_token.dart'; import 'package:selfprivacy/logic/models/timezone_settings.dart'; import 'package:selfprivacy/logic/models/hive/user.dart'; -import 'package:timezone/browser.dart'; import 'api_map.dart'; diff --git a/lib/logic/cubit/app_config/app_config_cubit.dart b/lib/logic/cubit/app_config/app_config_cubit.dart index f430f458..870154f1 100644 --- a/lib/logic/cubit/app_config/app_config_cubit.dart +++ b/lib/logic/cubit/app_config/app_config_cubit.dart @@ -80,10 +80,14 @@ class AppConfigCubit extends Cubit { void createServerAndSetDnsRecords() async { AppConfigNotFinished _stateCopy = state as AppConfigNotFinished; + var onCancel = + () => emit((state as AppConfigNotFinished).copyWith(isLoading: false)); + var onSuccess = (ServerHostingDetails serverDetails) async { await repository.createDnsRecords( serverDetails.ip4, state.serverDomain!, + onCancel: onCancel, ); emit((state as AppConfigNotFinished).copyWith( @@ -93,9 +97,6 @@ class AppConfigCubit extends Cubit { runDelayed(startServerIfDnsIsOkay, Duration(seconds: 30), null); }; - var onCancel = - () => emit((state as AppConfigNotFinished).copyWith(isLoading: false)); - try { emit((state as AppConfigNotFinished).copyWith(isLoading: true)); await repository.createServer( diff --git a/lib/logic/cubit/app_config/app_config_repository.dart b/lib/logic/cubit/app_config/app_config_repository.dart index 28c27db6..d8edaedc 100644 --- a/lib/logic/cubit/app_config/app_config_repository.dart +++ b/lib/logic/cubit/app_config/app_config_repository.dart @@ -214,8 +214,9 @@ class AppConfigRepository { Future createDnsRecords( String ip4, - ServerDomain cloudFlareDomain, - ) async { + ServerDomain cloudFlareDomain, { + required void Function() onCancel, + }) async { var cloudflareApi = CloudflareApi(); await cloudflareApi.removeSimilarRecords( @@ -223,10 +224,41 @@ class AppConfigRepository { cloudFlareDomain: cloudFlareDomain, ); - await cloudflareApi.createMultipleDnsRecords( - ip4: ip4, - cloudFlareDomain: cloudFlareDomain, - ); + try { + await cloudflareApi.createMultipleDnsRecords( + ip4: ip4, + cloudFlareDomain: cloudFlareDomain, + ); + } on DioError catch (e) { + var hetznerApi = HetznerApi(); + var nav = getIt.get(); + nav.showPopUpDialog( + BrandAlert( + title: e.response!.data["errors"][0]["code"] == 1038 + ? 'modals.10'.tr() + : 'providers.domain.states.error'.tr(), + contentText: 'modals.6'.tr(), + actions: [ + ActionButton( + text: 'basis.delete'.tr(), + isRed: true, + onPressed: () async { + await hetznerApi.deleteSelfprivacyServerAndAllVolumes( + domainName: cloudFlareDomain.domainName); + + onCancel(); + }, + ), + ActionButton( + text: 'basis.cancel'.tr(), + onPressed: () { + onCancel(); + }, + ), + ], + ), + ); + } await HetznerApi().createReverseDns( ip4: ip4, From b4145dc5c8f6e65182cade31f209f2aa7dfaf1be Mon Sep 17 00:00:00 2001 From: Inex Code Date: Tue, 17 May 2022 01:41:00 +0300 Subject: [PATCH 10/52] First steps to move to Material You --- lib/main.dart | 7 +- lib/theming/factory/app_theme_factory.dart | 19 ++-- .../components/brand_button/FilledButton.dart | 26 +++++ .../components/brand_button/brand_button.dart | 100 ++++-------------- .../brand_hero_screen/brand_hero_screen.dart | 23 ++-- .../brand_tab_bar/brand_tab_bar.dart | 12 +-- .../pre_styled_buttons/flashFab.dart | 93 ++++++++++++++++ lib/ui/pages/rootRoute.dart | 5 +- .../setup/recovering/recovery_domain.dart | 9 +- 9 files changed, 177 insertions(+), 117 deletions(-) create mode 100644 lib/ui/components/brand_button/FilledButton.dart create mode 100644 lib/ui/components/pre_styled_buttons/flashFab.dart diff --git a/lib/main.dart b/lib/main.dart index 716df28b..3f085bfc 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -13,7 +13,6 @@ import 'package:timezone/data/latest.dart' as tz; import 'config/bloc_config.dart'; import 'config/bloc_observer.dart'; -import 'config/brand_theme.dart'; import 'config/get_it_config.dart'; import 'config/localization.dart'; import 'logic/cubit/app_settings/app_settings_cubit.dart'; @@ -45,7 +44,8 @@ void main() async { ); BlocOverrides.runZoned( - () => runApp(Localization(child: MyApp( + () => runApp(Localization( + child: MyApp( lightThemeData: lightThemeData, darkThemeData: darkThemeData, ))), @@ -81,7 +81,8 @@ class MyApp extends StatelessWidget { title: 'SelfPrivacy', theme: lightThemeData, darkTheme: darkThemeData, - themeMode: appSettings.isDarkModeOn ? ThemeMode.dark : ThemeMode.light, + themeMode: + appSettings.isDarkModeOn ? ThemeMode.dark : ThemeMode.light, home: appSettings.isOnbordingShowing ? OnboardingPage(nextPage: InitializingPage()) : RootPage(), diff --git a/lib/theming/factory/app_theme_factory.dart b/lib/theming/factory/app_theme_factory.dart index 82407dfb..6d0ce1de 100644 --- a/lib/theming/factory/app_theme_factory.dart +++ b/lib/theming/factory/app_theme_factory.dart @@ -28,13 +28,18 @@ abstract class AppThemeFactory { if (Platform.isLinux) { GtkThemeData themeData = await GtkThemeData.initialize(); + final isGtkDark = + Color(themeData.theme_base_color).computeLuminance() < 0.5; + final isInverseNeeded = isGtkDark != isDark; gtkColorsScheme = ColorScheme.fromSeed( seedColor: Color(themeData.theme_selected_bg_color), - brightness: Color(themeData.theme_base_color).computeLuminance() > 0.5 - ? Brightness.light - : Brightness.dark, - background: Color(themeData.theme_bg_color), - surface: Color(themeData.theme_base_color), + brightness: brightness, + background: isInverseNeeded + ? Color(themeData.theme_base_color) + : Color(themeData.theme_bg_color), + surface: isInverseNeeded + ? Color(themeData.theme_bg_color) + : Color(themeData.theme_base_color), ); } @@ -46,7 +51,8 @@ abstract class AppThemeFactory { brightness: brightness, ); - final colorScheme = dynamicColorsScheme ?? gtkColorsScheme ?? fallbackColorScheme; + final colorScheme = + dynamicColorsScheme ?? gtkColorsScheme ?? fallbackColorScheme; final appTypography = Typography.material2021(); @@ -55,6 +61,7 @@ abstract class AppThemeFactory { brightness: colorScheme.brightness, typography: appTypography, useMaterial3: true, + scaffoldBackgroundColor: colorScheme.background, appBarTheme: AppBarTheme( elevation: 0, backgroundColor: colorScheme.primary, diff --git a/lib/ui/components/brand_button/FilledButton.dart b/lib/ui/components/brand_button/FilledButton.dart new file mode 100644 index 00000000..0ac34a42 --- /dev/null +++ b/lib/ui/components/brand_button/FilledButton.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; + +class FilledButton extends StatelessWidget { + const FilledButton({ + Key? key, + this.onPressed, + this.title, + this.child, + }) : super(key: key); + + final VoidCallback? onPressed; + final String? title; + final Widget? child; + + @override + Widget build(BuildContext context) { + return ElevatedButton( + onPressed: onPressed, + child: child ?? Text(title ?? ''), + style: ElevatedButton.styleFrom( + onPrimary: Theme.of(context).colorScheme.onPrimary, + primary: Theme.of(context).colorScheme.primary, + ).copyWith(elevation: ButtonStyleButton.allOrNull(0.0)), + ); + } +} diff --git a/lib/ui/components/brand_button/brand_button.dart b/lib/ui/components/brand_button/brand_button.dart index 86020121..b09b0442 100644 --- a/lib/ui/components/brand_button/brand_button.dart +++ b/lib/ui/components/brand_button/brand_button.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:selfprivacy/config/brand_colors.dart'; +import 'package:selfprivacy/ui/components/brand_button/FilledButton.dart'; import 'package:selfprivacy/ui/components/brand_text/brand_text.dart'; enum BrandButtonTypes { rised, text, iconText } @@ -13,11 +13,17 @@ class BrandButton { }) { assert(text == null || child == null, 'required title or child'); assert(text != null || child != null, 'required title or child'); - return _RisedButton( - key: key, - title: text, - onPressed: onPressed, - child: child, + return ConstrainedBox( + constraints: BoxConstraints( + minHeight: 48, + minWidth: double.infinity, + ), + child: FilledButton( + key: key, + title: text, + onPressed: onPressed, + child: child, + ), ); } @@ -26,10 +32,12 @@ class BrandButton { required VoidCallback onPressed, required String title, }) => - _TextButton( - key: key, - title: title, - onPressed: onPressed, + ConstrainedBox( + constraints: BoxConstraints( + minHeight: 48, + minWidth: double.infinity, + ), + child: TextButton(onPressed: onPressed, child: Text(title)), ); static emptyWithIconText({ @@ -46,78 +54,6 @@ class BrandButton { ); } -class _RisedButton extends StatelessWidget { - const _RisedButton({ - Key? key, - this.onPressed, - this.title, - this.child, - }) : super(key: key); - - final VoidCallback? onPressed; - final String? title; - final Widget? child; - - @override - Widget build(BuildContext context) { - return ClipRRect( - borderRadius: BorderRadius.circular(24), - child: ColoredBox( - color: onPressed == null - ? BrandColors.gray2 - : Theme.of(context).primaryColor, - child: Material( - color: Colors.transparent, - child: InkWell( - onTap: onPressed, - child: Container( - height: 48, - width: double.infinity, - alignment: Alignment.center, - padding: EdgeInsets.all(12), - child: child ?? BrandText.buttonTitleText(title), - ), - ), - ), - ), - ); - } -} - -class _TextButton extends StatelessWidget { - const _TextButton({ - Key? key, - this.onPressed, - this.title, - }) : super(key: key); - - final VoidCallback? onPressed; - final String? title; - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: onPressed, - behavior: HitTestBehavior.opaque, - child: Container( - height: 48, - width: double.infinity, - alignment: Alignment.center, - padding: EdgeInsets.all(12), - child: Text( - title!, - style: TextStyle( - color: BrandColors.blue, - fontSize: 16, - fontWeight: FontWeight.bold, - height: 1.5, - ), - ), - ), - ); - } -} - class _IconTextButton extends StatelessWidget { const _IconTextButton({Key? key, this.onPressed, this.title, this.icon}) : super(key: key); diff --git a/lib/ui/components/brand_hero_screen/brand_hero_screen.dart b/lib/ui/components/brand_hero_screen/brand_hero_screen.dart index 934c952f..efa75515 100644 --- a/lib/ui/components/brand_hero_screen/brand_hero_screen.dart +++ b/lib/ui/components/brand_hero_screen/brand_hero_screen.dart @@ -37,20 +37,27 @@ class BrandHeroScreen extends StatelessWidget { padding: EdgeInsets.all(16.0), children: [ if (heroIcon != null) - Icon( - heroIcon, - size: 48.0, + Container( + child: Icon( + heroIcon, + size: 48.0, + ), + alignment: Alignment.bottomLeft, ), - SizedBox(height: 16.0), + SizedBox(height: 8.0), if (heroTitle != null) - Text(heroTitle!, - style: Theme.of(context).textTheme.headline2, - textAlign: TextAlign.center), + Text( + heroTitle!, + style: Theme.of(context).textTheme.headlineMedium?.copyWith( + color: Colors.black, + ), + textAlign: TextAlign.start, + ), SizedBox(height: 8.0), if (heroSubtitle != null) Text(heroSubtitle!, style: Theme.of(context).textTheme.bodyMedium, - textAlign: TextAlign.center), + textAlign: TextAlign.start), SizedBox(height: 16.0), ...children, ], diff --git a/lib/ui/components/brand_tab_bar/brand_tab_bar.dart b/lib/ui/components/brand_tab_bar/brand_tab_bar.dart index 3b0b0dac..3975ee25 100644 --- a/lib/ui/components/brand_tab_bar/brand_tab_bar.dart +++ b/lib/ui/components/brand_tab_bar/brand_tab_bar.dart @@ -1,10 +1,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:selfprivacy/config/brand_colors.dart'; import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart'; -final _kBottomTabBarHeight = 51; - class BrandTabBar extends StatefulWidget { BrandTabBar({Key? key, this.controller}) : super(key: key); @@ -43,24 +40,17 @@ class _BrandTabBarState extends State { _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(), BrandIcons.menu, 3), + _getIconButton('basis.more'.tr(), Icons.menu_rounded, 3), ], onDestinationSelected: (index) { widget.controller!.animateTo(index); }, selectedIndex: currentIndex ?? 0, labelBehavior: NavigationDestinationLabelBehavior.onlyShowSelected, - ); } _getIconButton(String label, IconData iconData, int index) { - var activeColor = Theme.of(context).brightness == Brightness.dark - ? BrandColors.white - : BrandColors.black; - - var isActive = currentIndex == index; - var color = isActive ? activeColor : BrandColors.inactive; return NavigationDestination( icon: Icon(iconData), label: label, diff --git a/lib/ui/components/pre_styled_buttons/flashFab.dart b/lib/ui/components/pre_styled_buttons/flashFab.dart new file mode 100644 index 00000000..0af5bcab --- /dev/null +++ b/lib/ui/components/pre_styled_buttons/flashFab.dart @@ -0,0 +1,93 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:ionicons/ionicons.dart'; +import 'package:selfprivacy/config/brand_colors.dart'; +import 'package:selfprivacy/logic/cubit/jobs/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 { + BrandFab({Key? key}) : super(key: key); + + @override + _BrandFabState createState() => _BrandFabState(); +} + +class _BrandFabState extends State + with SingleTickerProviderStateMixin { + late AnimationController _animationController; + late Animation _colorTween; + + @override + void initState() { + _animationController = + AnimationController(vsync: this, duration: Duration(milliseconds: 800)); + _colorTween = ColorTween( + begin: BrandColors.black, + end: BrandColors.primary, + ).animate(_animationController); + + super.initState(); + WidgetsBinding.instance.addPostFrameCallback(_afterLayout); + } + + void _afterLayout(_) { + if (Theme.of(context).brightness == Brightness.dark) { + setState(() { + _colorTween = ColorTween( + begin: BrandColors.white, + end: BrandColors.primary, + ).animate(_animationController); + }); + } + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } + + bool wasPrevStateIsEmpty = true; + + @override + Widget build(BuildContext context) { + return BlocListener( + listener: (context, state) { + if (wasPrevStateIsEmpty && state is! JobsStateEmpty) { + wasPrevStateIsEmpty = false; + _animationController.forward(); + } else if (!wasPrevStateIsEmpty && state is JobsStateEmpty) { + wasPrevStateIsEmpty = true; + + _animationController.reverse(); + } + }, + child: FloatingActionButton( + onPressed: () { + showBrandBottomSheet( + context: context, + builder: (context) => BrandBottomSheet( + isExpended: true, + child: JobsContent(), + ), + ); + }, + child: AnimatedBuilder( + animation: _colorTween, + builder: (context, child) { + var v = _animationController.value; + var 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, + ), + ); + }), + ), + ); + } +} diff --git a/lib/ui/pages/rootRoute.dart b/lib/ui/pages/rootRoute.dart index 24865688..eabb0a10 100644 --- a/lib/ui/pages/rootRoute.dart +++ b/lib/ui/pages/rootRoute.dart @@ -1,14 +1,14 @@ -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:selfprivacy/logic/api_maps/server.dart'; -import 'package:selfprivacy/ui/components/brand_icons/brand_icons.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 '../components/pre_styled_buttons/flashFab.dart'; + class RootPage extends StatefulWidget { const RootPage({Key? key}) : super(key: key); @@ -53,6 +53,7 @@ class _RootPageState extends State bottomNavigationBar: BrandTabBar( controller: tabController, ), + floatingActionButton: BrandFab(), ), ); } diff --git a/lib/ui/pages/setup/recovering/recovery_domain.dart b/lib/ui/pages/setup/recovering/recovery_domain.dart index 8ba452d7..151b5314 100644 --- a/lib/ui/pages/setup/recovering/recovery_domain.dart +++ b/lib/ui/pages/setup/recovering/recovery_domain.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; import 'package:selfprivacy/logic/cubit/app_config/app_config_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_button/FilledButton.dart'; import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; class RecoveryDomain extends StatelessWidget { @@ -29,18 +29,17 @@ class RecoveryDomain extends StatelessWidget { ), ), SizedBox(height: 16), - BrandButton.rised( + FilledButton( + title: "more.continue".tr(), onPressed: formCubitState.isSubmitting ? null : () => context.read().trySubmit(), - text: "more.continue".tr(), - ), + ) ], heroTitle: "recovering.recovery_main_header".tr(), heroSubtitle: "recovering.domain_recovery_description".tr(), hasBackButton: true, hasFlashButton: false, - heroIcon: Icons.link, ); }), ); From a56af9dbecc7143038e60e4a1a7a952458b11ae7 Mon Sep 17 00:00:00 2001 From: NaiJi Date: Tue, 17 May 2022 02:42:46 +0300 Subject: [PATCH 11/52] Fix desktop theme --- lib/theming/factory/app_theme_factory.dart | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/lib/theming/factory/app_theme_factory.dart b/lib/theming/factory/app_theme_factory.dart index 6d0ce1de..c1f5903a 100644 --- a/lib/theming/factory/app_theme_factory.dart +++ b/lib/theming/factory/app_theme_factory.dart @@ -29,17 +29,13 @@ abstract class AppThemeFactory { if (Platform.isLinux) { GtkThemeData themeData = await GtkThemeData.initialize(); final isGtkDark = - Color(themeData.theme_base_color).computeLuminance() < 0.5; + Color(themeData.theme_bg_color).computeLuminance() < 0.5; final isInverseNeeded = isGtkDark != isDark; gtkColorsScheme = ColorScheme.fromSeed( seedColor: Color(themeData.theme_selected_bg_color), brightness: brightness, - background: isInverseNeeded - ? Color(themeData.theme_base_color) - : Color(themeData.theme_bg_color), - surface: isInverseNeeded - ? Color(themeData.theme_bg_color) - : Color(themeData.theme_base_color), + background: isInverseNeeded ? null : Color(themeData.theme_bg_color), + surface: isInverseNeeded ? null : Color(themeData.theme_base_color), ); } From ee53590ba0dc8ba2ed5f56f87d2285f5f97e8bbf Mon Sep 17 00:00:00 2001 From: NaiJi Date: Tue, 17 May 2022 15:33:30 +0300 Subject: [PATCH 12/52] Implement recovery method select page Co-authored-by: Inex Code --- assets/translations/en.json | 11 +++- assets/translations/ru.json | 14 +++++ lib/theming/factory/app_theme_factory.dart | 9 +++- .../components/brand_cards/brand_cards.dart | 6 +-- lib/ui/pages/setup/initializing.dart | 6 +-- .../setup/recovering/recovery_domain.dart | 52 ++++++++++--------- .../recovering/recovery_method_select.dart | 48 +++++++++++++++++ 7 files changed, 112 insertions(+), 34 deletions(-) create mode 100644 lib/ui/pages/setup/recovering/recovery_method_select.dart diff --git a/assets/translations/en.json b/assets/translations/en.json index c3245f9d..ef65c8e5 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -286,7 +286,16 @@ "recovering": { "recovery_main_header": "Connect to an existing server", "domain_recovery_description": "Enter a server domain you want to get access for", - "domain_recover_placeholder": "Your domain" + "domain_recover_placeholder": "Your domain", + "method_select_description": "Select a recovery method:", + "method_select_other_device": "I have access on another device", + "method_select_recovery_key": "I have a recovery key", + "method_select_nothing": "I don't have any of that", + "fallback_select_description": "What exactly do you have? Pick the first available option:", + "fallback_select_token_copy": "Copy of auth token from other version of the application.", + "fallback_select_root_ssh": "Root SSH access to the server.", + "fallback_select_provider_console": "Access to the server console of my prodiver.", + "fallback_select_provider_console_hint": "For example: Hetzner." }, "modals": { "_comment": "messages in modals", diff --git a/assets/translations/ru.json b/assets/translations/ru.json index 7f46b137..8503dc39 100644 --- a/assets/translations/ru.json +++ b/assets/translations/ru.json @@ -284,6 +284,20 @@ "finish": "Всё инициализировано.", "checks": "Проверок выполнено: \n{} / {}" }, + "recovering": { + "recovery_main_header": "Подключиться к существующему серверу", + "domain_recovery_description": "Введите домен, по которому вы хотите получить доступ к серверу:", + "domain_recover_placeholder": "Домен", + "method_select_description": "Выберите способ входа:", + "method_select_other_device": "У меня есть доступ на другом устройстве", + "method_select_recovery_key": "У меня есть ключ восстановления", + "method_select_nothing": "У меня ничего из этого нет", + "fallback_select_description": "Что у вас из этого есть? Выберите первое, что подходит:", + "fallback_select_token_copy": "Копия токена авторизации из другой версии приложения.", + "fallback_select_root_ssh": "Root доступ к серверу по SSH.", + "fallback_select_provider_console": "Доступ к консоли хостинга.", + "fallback_select_provider_console_hint": "Например, Hetzner." + }, "modals": { "_comment": "messages in modals", "1": "Сервер с таким именем уже существует", diff --git a/lib/theming/factory/app_theme_factory.dart b/lib/theming/factory/app_theme_factory.dart index c1f5903a..860dd0b2 100644 --- a/lib/theming/factory/app_theme_factory.dart +++ b/lib/theming/factory/app_theme_factory.dart @@ -39,8 +39,13 @@ abstract class AppThemeFactory { ); } - final accentColor = await SystemAccentColor(fallbackColor) - ..load(); + final accentColor = await SystemAccentColor(fallbackColor); + + try { + await accentColor.load(); + } on MissingPluginException catch (e) { + print("_createAppTheme: ${e.message}"); + } final fallbackColorScheme = ColorScheme.fromSeed( seedColor: accentColor.accent, diff --git a/lib/ui/components/brand_cards/brand_cards.dart b/lib/ui/components/brand_cards/brand_cards.dart index 398e7e89..67ad09af 100644 --- a/lib/ui/components/brand_cards/brand_cards.dart +++ b/lib/ui/components/brand_cards/brand_cards.dart @@ -67,12 +67,12 @@ class _OutlinedCard extends StatelessWidget { return Card( elevation: 0.0, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(4), + borderRadius: BorderRadius.all(Radius.circular(12)), side: BorderSide( - color: Colors.grey.withOpacity(0.2), - width: 1, + color: Theme.of(context).colorScheme.outline, ), ), + clipBehavior: Clip.antiAlias, child: child, ); } diff --git a/lib/ui/pages/setup/initializing.dart b/lib/ui/pages/setup/initializing.dart index 5048aa57..016131ae 100644 --- a/lib/ui/pages/setup/initializing.dart +++ b/lib/ui/pages/setup/initializing.dart @@ -17,7 +17,7 @@ import 'package:selfprivacy/ui/components/brand_text/brand_text.dart'; import 'package:selfprivacy/ui/components/brand_timer/brand_timer.dart'; import 'package:selfprivacy/ui/components/progress_bar/progress_bar.dart'; import 'package:selfprivacy/ui/pages/rootRoute.dart'; -import 'package:selfprivacy/ui/pages/setup/recovering/recovery_domain.dart'; +import 'package:selfprivacy/ui/pages/setup/recovering/recovery_method_select.dart'; import 'package:selfprivacy/utils/route_transitions/basic.dart'; class InitializingPage extends StatelessWidget { @@ -104,8 +104,8 @@ class InitializingPage extends StatelessWidget { child: BrandButton.text( title: 'basis.connect_to_existing'.tr(), onPressed: () { - Navigator.of(context) - .push(materialRoute(RecoveryDomain())); + Navigator.of(context).push( + materialRoute(RecoveryMethodSelect())); }, ), ) diff --git a/lib/ui/pages/setup/recovering/recovery_domain.dart b/lib/ui/pages/setup/recovering/recovery_domain.dart index 151b5314..b68d423c 100644 --- a/lib/ui/pages/setup/recovering/recovery_domain.dart +++ b/lib/ui/pages/setup/recovering/recovery_domain.dart @@ -15,33 +15,35 @@ class RecoveryDomain extends StatelessWidget { return BlocProvider( create: (context) => RecoveryDomainFormCubit(appConfig, FieldCubitFactory(context)), - child: Builder(builder: (context) { - var formCubitState = context.watch().state; + child: Builder( + builder: (context) { + var formCubitState = context.watch().state; - return BrandHeroScreen( - children: [ - CubitFormTextField( - formFieldCubit: - context.read().serverDomainField, - decoration: InputDecoration( - border: OutlineInputBorder(), - labelText: "recovering.domain_recover_placeholder".tr(), + return BrandHeroScreen( + heroTitle: "recovering.recovery_main_header".tr(), + heroSubtitle: "recovering.domain_recovery_description".tr(), + hasBackButton: true, + hasFlashButton: false, + children: [ + CubitFormTextField( + formFieldCubit: + context.read().serverDomainField, + decoration: InputDecoration( + border: OutlineInputBorder(), + labelText: "recovering.domain_recover_placeholder".tr(), + ), ), - ), - SizedBox(height: 16), - FilledButton( - title: "more.continue".tr(), - onPressed: formCubitState.isSubmitting - ? null - : () => context.read().trySubmit(), - ) - ], - heroTitle: "recovering.recovery_main_header".tr(), - heroSubtitle: "recovering.domain_recovery_description".tr(), - hasBackButton: true, - hasFlashButton: false, - ); - }), + SizedBox(height: 16), + FilledButton( + title: "more.continue".tr(), + onPressed: formCubitState.isSubmitting + ? null + : () => context.read().trySubmit(), + ) + ], + ); + }, + ), ); } } diff --git a/lib/ui/pages/setup/recovering/recovery_method_select.dart b/lib/ui/pages/setup/recovering/recovery_method_select.dart new file mode 100644 index 00000000..d71feaa0 --- /dev/null +++ b/lib/ui/pages/setup/recovering/recovery_method_select.dart @@ -0,0 +1,48 @@ +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_cards/brand_cards.dart'; +import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; +import 'package:selfprivacy/utils/route_transitions/basic.dart'; +import 'package:selfprivacy/ui/pages/rootRoute.dart'; + +class RecoveryMethodSelect extends StatelessWidget { + @override + Widget build(BuildContext context) { + return BrandHeroScreen( + heroTitle: "recovering.recovery_main_header".tr(), + heroSubtitle: "recovering.method_select_description".tr(), + hasBackButton: true, + hasFlashButton: false, + children: [ + BrandCards.outlined( + child: ListTile( + title: Text( + "recovering.method_select_other_device".tr(), + style: Theme.of(context).textTheme.titleMedium, + ), + leading: Icon(Icons.offline_share_outlined), + onTap: () => Navigator.of(context).push(materialRoute(RootPage())), + ), + ), + SizedBox(height: 16), + BrandCards.outlined( + child: ListTile( + title: Text( + "recovering.method_select_recovery_key".tr(), + style: Theme.of(context).textTheme.titleMedium, + ), + leading: Icon(Icons.password_outlined), + onTap: () => Navigator.of(context).push(materialRoute(RootPage())), + ), + ), + SizedBox(height: 16), + BrandButton.text( + title: "recovering.method_select_nothing".tr(), + onPressed: () => + Navigator.of(context).push(materialRoute(RootPage())), + ) + ], + ); + } +} From 7a719f15ce8ce741cd2bdfafc40692c7dc2a51f4 Mon Sep 17 00:00:00 2001 From: NaiJi Date: Tue, 17 May 2022 15:45:20 +0300 Subject: [PATCH 13/52] Implement first recovery device method page Co-authored-by: Inex Code --- assets/translations/en.json | 2 ++ assets/translations/ru.json | 2 ++ .../recovering/recovery_method_device_1.dart | 25 +++++++++++++++++++ .../recovering/recovery_method_select.dart | 4 ++- 4 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 lib/ui/pages/setup/recovering/recovery_method_device_1.dart diff --git a/assets/translations/en.json b/assets/translations/en.json index ef65c8e5..79d7231d 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -291,6 +291,8 @@ "method_select_other_device": "I have access on another device", "method_select_recovery_key": "I have a recovery key", "method_select_nothing": "I don't have any of that", + "method_device_description": "Open the application on another device, then go to the device page. Press \"Add device\" to receive your token.", + "method_device_button": "I have received my token", "fallback_select_description": "What exactly do you have? Pick the first available option:", "fallback_select_token_copy": "Copy of auth token from other version of the application.", "fallback_select_root_ssh": "Root SSH access to the server.", diff --git a/assets/translations/ru.json b/assets/translations/ru.json index 8503dc39..10f51f11 100644 --- a/assets/translations/ru.json +++ b/assets/translations/ru.json @@ -292,6 +292,8 @@ "method_select_other_device": "У меня есть доступ на другом устройстве", "method_select_recovery_key": "У меня есть ключ восстановления", "method_select_nothing": "У меня ничего из этого нет", + "method_device_description": "Откройте приложение на другом устройстве и откройте экран управления устройствами. Нажмите \"Добавить устройство\" чтобы получить токен для авторизации.", + "method_device_button": "Я получил токен", "fallback_select_description": "Что у вас из этого есть? Выберите первое, что подходит:", "fallback_select_token_copy": "Копия токена авторизации из другой версии приложения.", "fallback_select_root_ssh": "Root доступ к серверу по SSH.", diff --git a/lib/ui/pages/setup/recovering/recovery_method_device_1.dart b/lib/ui/pages/setup/recovering/recovery_method_device_1.dart new file mode 100644 index 00000000..dd2566ec --- /dev/null +++ b/lib/ui/pages/setup/recovering/recovery_method_device_1.dart @@ -0,0 +1,25 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:selfprivacy/ui/components/brand_button/FilledButton.dart'; +import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; +import 'package:selfprivacy/utils/route_transitions/basic.dart'; +import 'package:selfprivacy/ui/pages/rootRoute.dart'; + +class RecoveryMethodDevice1 extends StatelessWidget { + @override + Widget build(BuildContext context) { + return BrandHeroScreen( + heroTitle: "recovering.recovery_main_header".tr(), + heroSubtitle: "recovering.method_device_description".tr(), + hasBackButton: true, + hasFlashButton: false, + children: [ + FilledButton( + title: "recovering.method_device_button".tr(), + onPressed: () => + Navigator.of(context).push(materialRoute(RootPage())), + ) + ], + ); + } +} diff --git a/lib/ui/pages/setup/recovering/recovery_method_select.dart b/lib/ui/pages/setup/recovering/recovery_method_select.dart index d71feaa0..f94abc91 100644 --- a/lib/ui/pages/setup/recovering/recovery_method_select.dart +++ b/lib/ui/pages/setup/recovering/recovery_method_select.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:selfprivacy/ui/components/brand_button/brand_button.dart'; import 'package:selfprivacy/ui/components/brand_cards/brand_cards.dart'; import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; +import 'package:selfprivacy/ui/pages/setup/recovering/recovery_method_device_1.dart'; import 'package:selfprivacy/utils/route_transitions/basic.dart'; import 'package:selfprivacy/ui/pages/rootRoute.dart'; @@ -22,7 +23,8 @@ class RecoveryMethodSelect extends StatelessWidget { style: Theme.of(context).textTheme.titleMedium, ), leading: Icon(Icons.offline_share_outlined), - onTap: () => Navigator.of(context).push(materialRoute(RootPage())), + onTap: () => Navigator.of(context) + .push(materialRoute(RecoveryMethodDevice1())), ), ), SizedBox(height: 16), From 93215d90fbeef00948a6d11596c76916d04b4692 Mon Sep 17 00:00:00 2001 From: NaiJi Date: Tue, 17 May 2022 16:29:04 +0300 Subject: [PATCH 14/52] Implement fallback recovery method page Co-authored-by: Inex Code --- assets/translations/en.json | 3 + assets/translations/ru.json | 3 + .../setup/recovering/recovery_domain.dart | 4 +- .../recovering/recovery_fallback_select.dart | 57 +++++++++++++++++++ .../recovering/recovery_method_device_1.dart | 2 +- .../recovering/recovery_method_device_2.dart | 25 ++++++++ .../recovering/recovery_method_select.dart | 5 +- 7 files changed, 94 insertions(+), 5 deletions(-) create mode 100644 lib/ui/pages/setup/recovering/recovery_fallback_select.dart create mode 100644 lib/ui/pages/setup/recovering/recovery_method_device_2.dart diff --git a/assets/translations/en.json b/assets/translations/en.json index 79d7231d..22a6b0ee 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -293,6 +293,9 @@ "method_select_nothing": "I don't have any of that", "method_device_description": "Open the application on another device, then go to the device page. Press \"Add device\" to receive your token.", "method_device_button": "I have received my token", + "method_device_input_description": "Enter your authorization token", + "method_device_input_placeholder": "Token", + "method_recovery_input_description": "Enter your recovery token", "fallback_select_description": "What exactly do you have? Pick the first available option:", "fallback_select_token_copy": "Copy of auth token from other version of the application.", "fallback_select_root_ssh": "Root SSH access to the server.", diff --git a/assets/translations/ru.json b/assets/translations/ru.json index 10f51f11..8c1a8397 100644 --- a/assets/translations/ru.json +++ b/assets/translations/ru.json @@ -294,6 +294,9 @@ "method_select_nothing": "У меня ничего из этого нет", "method_device_description": "Откройте приложение на другом устройстве и откройте экран управления устройствами. Нажмите \"Добавить устройство\" чтобы получить токен для авторизации.", "method_device_button": "Я получил токен", + "method_device_input_description": "Введите ваш токен авторизации", + "method_device_input_placeholder": "Токен", + "method_recovery_input_description": "Введите ваш токен восстановления", "fallback_select_description": "Что у вас из этого есть? Выберите первое, что подходит:", "fallback_select_token_copy": "Копия токена авторизации из другой версии приложения.", "fallback_select_root_ssh": "Root доступ к серверу по SSH.", diff --git a/lib/ui/pages/setup/recovering/recovery_domain.dart b/lib/ui/pages/setup/recovering/recovery_domain.dart index b68d423c..5ea30da8 100644 --- a/lib/ui/pages/setup/recovering/recovery_domain.dart +++ b/lib/ui/pages/setup/recovering/recovery_domain.dart @@ -1,7 +1,7 @@ import 'package:cubit_form/cubit_form.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.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/FilledButton.dart'; @@ -10,7 +10,7 @@ import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.da class RecoveryDomain extends StatelessWidget { @override Widget build(BuildContext context) { - var appConfig = context.watch(); + var appConfig = context.watch(); return BlocProvider( create: (context) => diff --git a/lib/ui/pages/setup/recovering/recovery_fallback_select.dart b/lib/ui/pages/setup/recovering/recovery_fallback_select.dart new file mode 100644 index 00000000..2a1d7b21 --- /dev/null +++ b/lib/ui/pages/setup/recovering/recovery_fallback_select.dart @@ -0,0 +1,57 @@ +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_cards/brand_cards.dart'; +import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; +import 'package:selfprivacy/utils/route_transitions/basic.dart'; +import 'package:selfprivacy/ui/pages/rootRoute.dart'; + +class RecoveryFallbackSelect extends StatelessWidget { + @override + Widget build(BuildContext context) { + return BrandHeroScreen( + heroTitle: "recovering.recovery_main_header".tr(), + heroSubtitle: "recovering.fallback_select_description".tr(), + hasBackButton: true, + hasFlashButton: false, + children: [ + BrandCards.outlined( + child: ListTile( + title: Text( + "recovering.fallback_select_token_copy".tr(), + style: Theme.of(context).textTheme.titleMedium, + ), + leading: Icon(Icons.vpn_key), + onTap: () => Navigator.of(context).push(materialRoute(RootPage())), + ), + ), + SizedBox(height: 16), + BrandCards.outlined( + child: ListTile( + title: Text( + "recovering.fallback_select_root_ssh".tr(), + style: Theme.of(context).textTheme.titleMedium, + ), + leading: Icon(Icons.terminal), + onTap: () => Navigator.of(context).push(materialRoute(RootPage())), + ), + ), + SizedBox(height: 16), + BrandCards.outlined( + child: ListTile( + title: Text( + "recovering.fallback_select_provider_console".tr(), + style: Theme.of(context).textTheme.titleMedium, + ), + subtitle: Text( + "recovering.fallback_select_provider_console_hint".tr(), + style: Theme.of(context).textTheme.bodyMedium, + ), + leading: Icon(Icons.web), + onTap: () => Navigator.of(context).push(materialRoute(RootPage())), + ), + ), + ], + ); + } +} diff --git a/lib/ui/pages/setup/recovering/recovery_method_device_1.dart b/lib/ui/pages/setup/recovering/recovery_method_device_1.dart index dd2566ec..73b32354 100644 --- a/lib/ui/pages/setup/recovering/recovery_method_device_1.dart +++ b/lib/ui/pages/setup/recovering/recovery_method_device_1.dart @@ -2,8 +2,8 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:selfprivacy/ui/components/brand_button/FilledButton.dart'; import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; -import 'package:selfprivacy/utils/route_transitions/basic.dart'; import 'package:selfprivacy/ui/pages/rootRoute.dart'; +import 'package:selfprivacy/utils/route_transitions/basic.dart'; class RecoveryMethodDevice1 extends StatelessWidget { @override diff --git a/lib/ui/pages/setup/recovering/recovery_method_device_2.dart b/lib/ui/pages/setup/recovering/recovery_method_device_2.dart new file mode 100644 index 00000000..c5eac006 --- /dev/null +++ b/lib/ui/pages/setup/recovering/recovery_method_device_2.dart @@ -0,0 +1,25 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:selfprivacy/ui/components/brand_button/FilledButton.dart'; +import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; +import 'package:selfprivacy/utils/route_transitions/basic.dart'; +import 'package:selfprivacy/ui/pages/rootRoute.dart'; + +class RecoveryMethodDevice2 extends StatelessWidget { + @override + Widget build(BuildContext context) { + return BrandHeroScreen( + heroTitle: "recovering.recovery_main_header".tr(), + heroSubtitle: "recovering.method_device_input_description".tr(), + hasBackButton: true, + hasFlashButton: false, + children: [ + FilledButton( + title: "recovering.method_device_button".tr(), + onPressed: () => + Navigator.of(context).push(materialRoute(RootPage())), + ) + ], + ); + } +} diff --git a/lib/ui/pages/setup/recovering/recovery_method_select.dart b/lib/ui/pages/setup/recovering/recovery_method_select.dart index f94abc91..136ec36c 100644 --- a/lib/ui/pages/setup/recovering/recovery_method_select.dart +++ b/lib/ui/pages/setup/recovering/recovery_method_select.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:selfprivacy/ui/components/brand_button/brand_button.dart'; import 'package:selfprivacy/ui/components/brand_cards/brand_cards.dart'; import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; +import 'package:selfprivacy/ui/pages/setup/recovering/recovery_fallback_select.dart'; import 'package:selfprivacy/ui/pages/setup/recovering/recovery_method_device_1.dart'; import 'package:selfprivacy/utils/route_transitions/basic.dart'; import 'package:selfprivacy/ui/pages/rootRoute.dart'; @@ -41,8 +42,8 @@ class RecoveryMethodSelect extends StatelessWidget { SizedBox(height: 16), BrandButton.text( title: "recovering.method_select_nothing".tr(), - onPressed: () => - Navigator.of(context).push(materialRoute(RootPage())), + onPressed: () => Navigator.of(context) + .push(materialRoute(RecoveryFallbackSelect())), ) ], ); From 0d0a3a4feef2ae120a8fb5ac076970e4b2d77900 Mon Sep 17 00:00:00 2001 From: NaiJi Date: Tue, 17 May 2022 16:31:34 +0300 Subject: [PATCH 15/52] Refactor App Config Cubit infrastrucute Co-authored-by: Inex Code --- lib/config/bloc_config.dart | 14 ++-- .../authentication_dependend_cubit.dart | 18 ++--- .../authentication_dependend_state.dart | 4 +- lib/logic/cubit/backups/backups_cubit.dart | 6 +- lib/logic/cubit/backups/backups_state.dart | 2 +- .../cubit/dns_records/dns_records_cubit.dart | 7 +- .../cubit/dns_records/dns_records_state.dart | 2 +- .../initializing/backblaze_form_cubit.dart | 4 +- .../initializing/cloudflare_form_cubit.dart | 4 +- .../setup/initializing/domain_cloudflare.dart | 4 +- .../initializing/hetzner_form_cubit.dart | 4 +- .../initializing/root_user_form_cubit.dart | 4 +- .../recovery_domain_form_cubit.dart | 4 +- .../server_installation_cubit.dart} | 65 ++++++++++--------- .../server_installation_repository.dart} | 10 +-- .../server_installation_state.dart} | 36 +++++----- lib/logic/cubit/services/services_cubit.dart | 6 +- lib/logic/cubit/services/services_state.dart | 2 +- lib/logic/cubit/users/users_cubit.dart | 6 +- lib/logic/cubit/users/users_state.dart | 2 +- .../pages/backup_details/backup_details.dart | 5 +- lib/ui/pages/dns_details/dns_details.dart | 5 +- .../pages/more/app_settings/app_setting.dart | 8 +-- lib/ui/pages/providers/providers.dart | 11 ++-- .../pages/server_details/server_details.dart | 5 +- lib/ui/pages/services/services.dart | 12 ++-- lib/ui/pages/setup/initializing.dart | 32 ++++----- .../recovering/recovery_fallback_select.dart | 1 - lib/ui/pages/users/new_user.dart | 2 +- lib/ui/pages/users/user_details.dart | 2 +- lib/ui/pages/users/users.dart | 5 +- lib/utils/ui_helpers.dart | 4 +- 32 files changed, 155 insertions(+), 141 deletions(-) rename lib/logic/cubit/{app_config/app_config_cubit.dart => server_installation/server_installation_cubit.dart} (78%) rename lib/logic/cubit/{app_config/app_config_repository.dart => server_installation/server_installation_repository.dart} (97%) rename lib/logic/cubit/{app_config/app_config_state.dart => server_installation/server_installation_state.dart} (90%) diff --git a/lib/config/bloc_config.dart b/lib/config/bloc_config.dart index d45144a5..c13caeaa 100644 --- a/lib/config/bloc_config.dart +++ b/lib/config/bloc_config.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart'; +import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; import 'package:selfprivacy/logic/cubit/app_settings/app_settings_cubit.dart'; import 'package:selfprivacy/logic/cubit/backups/backups_cubit.dart'; import 'package:selfprivacy/logic/cubit/dns_records/dns_records_cubit.dart'; @@ -17,11 +17,11 @@ class BlocAndProviderConfig extends StatelessWidget { @override Widget build(BuildContext context) { var isDark = false; - var appConfigCubit = AppConfigCubit()..load(); - var usersCubit = UsersCubit(appConfigCubit); - var servicesCubit = ServicesCubit(appConfigCubit); - var backupsCubit = BackupsCubit(appConfigCubit); - var dnsRecordsCubit = DnsRecordsCubit(appConfigCubit); + var serverInstallationCubit = ServerInstallationCubit()..load(); + var usersCubit = UsersCubit(serverInstallationCubit); + var servicesCubit = ServicesCubit(serverInstallationCubit); + var backupsCubit = BackupsCubit(serverInstallationCubit); + var dnsRecordsCubit = DnsRecordsCubit(serverInstallationCubit); return MultiProvider( providers: [ BlocProvider( @@ -30,7 +30,7 @@ class BlocAndProviderConfig extends StatelessWidget { isOnbordingShowing: true, )..load(), ), - BlocProvider(create: (_) => appConfigCubit, lazy: false), + BlocProvider(create: (_) => serverInstallationCubit, lazy: false), BlocProvider(create: (_) => ProvidersCubit()), BlocProvider(create: (_) => usersCubit..load(), lazy: false), BlocProvider(create: (_) => servicesCubit..load(), lazy: false), diff --git a/lib/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart b/lib/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart index 70d1af8f..bf44cbe1 100644 --- a/lib/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart +++ b/lib/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart @@ -1,15 +1,15 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; -import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart'; +import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; -export 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart'; +export 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; part 'authentication_dependend_state.dart'; -abstract class AppConfigDependendCubit - extends Cubit { - AppConfigDependendCubit( +abstract class ServerInstallationDependendCubit< + T extends ServerInstallationDependendState> extends Cubit { + ServerInstallationDependendCubit( this.appConfigCubit, T initState, ) : super(initState) { @@ -17,16 +17,16 @@ abstract class AppConfigDependendCubit checkAuthStatus(appConfigCubit.state); } - void checkAuthStatus(AppConfigState state) { - if (state is AppConfigFinished) { + void checkAuthStatus(ServerInstallationState state) { + if (state is ServerInstallationFinished) { load(); - } else if (state is AppConfigEmpty) { + } else if (state is ServerInstallationEmpty) { clear(); } } late StreamSubscription authCubitSubscription; - final AppConfigCubit appConfigCubit; + final ServerInstallationCubit appConfigCubit; void load(); void clear(); diff --git a/lib/logic/cubit/app_config_dependent/authentication_dependend_state.dart b/lib/logic/cubit/app_config_dependent/authentication_dependend_state.dart index 43339c85..668d63d0 100644 --- a/lib/logic/cubit/app_config_dependent/authentication_dependend_state.dart +++ b/lib/logic/cubit/app_config_dependent/authentication_dependend_state.dart @@ -1,5 +1,5 @@ part of 'authentication_dependend_cubit.dart'; -abstract class AppConfigDependendState extends Equatable { - const AppConfigDependendState(); +abstract class ServerInstallationDependendState extends Equatable { + const ServerInstallationDependendState(); } diff --git a/lib/logic/cubit/backups/backups_cubit.dart b/lib/logic/cubit/backups/backups_cubit.dart index 8617f587..4dd85046 100644 --- a/lib/logic/cubit/backups/backups_cubit.dart +++ b/lib/logic/cubit/backups/backups_cubit.dart @@ -10,15 +10,15 @@ import 'package:selfprivacy/logic/models/json/backup.dart'; part 'backups_state.dart'; -class BackupsCubit extends AppConfigDependendCubit { - BackupsCubit(AppConfigCubit appConfigCubit) +class BackupsCubit extends ServerInstallationDependendCubit { + BackupsCubit(ServerInstallationCubit appConfigCubit) : super(appConfigCubit, BackupsState(preventActions: true)); final api = ServerApi(); final backblaze = BackblazeApi(); Future load() async { - if (appConfigCubit.state is AppConfigFinished) { + if (appConfigCubit.state is ServerInstallationFinished) { final bucket = getIt().backblazeBucket; if (bucket == null) { emit(BackupsState( diff --git a/lib/logic/cubit/backups/backups_state.dart b/lib/logic/cubit/backups/backups_state.dart index 6b0bc5e3..3d618a75 100644 --- a/lib/logic/cubit/backups/backups_state.dart +++ b/lib/logic/cubit/backups/backups_state.dart @@ -1,6 +1,6 @@ part of 'backups_cubit.dart'; -class BackupsState extends AppConfigDependendState { +class BackupsState extends ServerInstallationDependendState { const BackupsState({ this.isInitialized = false, this.backups = const [], diff --git a/lib/logic/cubit/dns_records/dns_records_cubit.dart b/lib/logic/cubit/dns_records/dns_records_cubit.dart index e32ca7ff..52e09a2c 100644 --- a/lib/logic/cubit/dns_records/dns_records_cubit.dart +++ b/lib/logic/cubit/dns_records/dns_records_cubit.dart @@ -8,8 +8,9 @@ import '../../api_maps/server.dart'; part 'dns_records_state.dart'; -class DnsRecordsCubit extends AppConfigDependendCubit { - DnsRecordsCubit(AppConfigCubit appConfigCubit) +class DnsRecordsCubit + extends ServerInstallationDependendCubit { + DnsRecordsCubit(ServerInstallationCubit appConfigCubit) : super(appConfigCubit, DnsRecordsState(dnsState: DnsRecordsStatus.refreshing)); @@ -22,7 +23,7 @@ class DnsRecordsCubit extends AppConfigDependendCubit { dnsRecords: _getDesiredDnsRecords( appConfigCubit.state.serverDomain?.domainName, "", ""))); print('Loading DNS status'); - if (appConfigCubit.state is AppConfigFinished) { + if (appConfigCubit.state is ServerInstallationFinished) { final ServerDomain? domain = appConfigCubit.state.serverDomain; final String? ipAddress = appConfigCubit.state.serverDetails?.ip4; if (domain != null && ipAddress != null) { diff --git a/lib/logic/cubit/dns_records/dns_records_state.dart b/lib/logic/cubit/dns_records/dns_records_state.dart index dc594e74..7055d698 100644 --- a/lib/logic/cubit/dns_records/dns_records_state.dart +++ b/lib/logic/cubit/dns_records/dns_records_state.dart @@ -13,7 +13,7 @@ enum DnsRecordsCategory { other, } -class DnsRecordsState extends AppConfigDependendState { +class DnsRecordsState extends ServerInstallationDependendState { const DnsRecordsState({ this.dnsState = DnsRecordsStatus.uninitialized, this.dnsRecords = const [], diff --git a/lib/logic/cubit/forms/setup/initializing/backblaze_form_cubit.dart b/lib/logic/cubit/forms/setup/initializing/backblaze_form_cubit.dart index 7eb43c24..50fc8e80 100644 --- a/lib/logic/cubit/forms/setup/initializing/backblaze_form_cubit.dart +++ b/lib/logic/cubit/forms/setup/initializing/backblaze_form_cubit.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'package:cubit_form/cubit_form.dart'; import 'package:selfprivacy/logic/api_maps/backblaze.dart'; -import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart'; +import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; import 'package:selfprivacy/logic/models/hive/backblaze_credential.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -33,7 +33,7 @@ class BackblazeFormCubit extends FormCubit { ); } - final AppConfigCubit initializingCubit; + final ServerInstallationCubit initializingCubit; late final FieldCubit keyId; late final FieldCubit applicationKey; diff --git a/lib/logic/cubit/forms/setup/initializing/cloudflare_form_cubit.dart b/lib/logic/cubit/forms/setup/initializing/cloudflare_form_cubit.dart index d811843b..c6dea6c5 100644 --- a/lib/logic/cubit/forms/setup/initializing/cloudflare_form_cubit.dart +++ b/lib/logic/cubit/forms/setup/initializing/cloudflare_form_cubit.dart @@ -3,7 +3,7 @@ import 'dart:async'; import 'package:cubit_form/cubit_form.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:selfprivacy/logic/api_maps/cloudflare.dart'; -import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart'; +import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; import 'package:selfprivacy/logic/cubit/forms/validations/validations.dart'; class CloudFlareFormCubit extends FormCubit { @@ -27,7 +27,7 @@ class CloudFlareFormCubit extends FormCubit { initializingCubit.setCloudflareKey(apiKey.state.value); } - final AppConfigCubit initializingCubit; + final ServerInstallationCubit initializingCubit; late final FieldCubit apiKey; diff --git a/lib/logic/cubit/forms/setup/initializing/domain_cloudflare.dart b/lib/logic/cubit/forms/setup/initializing/domain_cloudflare.dart index 828d9b86..363cee7c 100644 --- a/lib/logic/cubit/forms/setup/initializing/domain_cloudflare.dart +++ b/lib/logic/cubit/forms/setup/initializing/domain_cloudflare.dart @@ -1,12 +1,12 @@ import 'package:cubit_form/cubit_form.dart'; import 'package:selfprivacy/logic/api_maps/cloudflare.dart'; -import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart'; +import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; import 'package:selfprivacy/logic/models/hive/server_domain.dart'; class DomainSetupCubit extends Cubit { DomainSetupCubit(this.initializingCubit) : super(Initial()); - final AppConfigCubit initializingCubit; + final ServerInstallationCubit initializingCubit; Future load() async { emit(Loading(LoadingTypes.loadingDomain)); diff --git a/lib/logic/cubit/forms/setup/initializing/hetzner_form_cubit.dart b/lib/logic/cubit/forms/setup/initializing/hetzner_form_cubit.dart index ce3e5aa9..42517c64 100644 --- a/lib/logic/cubit/forms/setup/initializing/hetzner_form_cubit.dart +++ b/lib/logic/cubit/forms/setup/initializing/hetzner_form_cubit.dart @@ -3,7 +3,7 @@ import 'dart:async'; import 'package:cubit_form/cubit_form.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:selfprivacy/logic/api_maps/hetzner.dart'; -import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart'; +import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; import 'package:selfprivacy/logic/cubit/forms/validations/validations.dart'; class HetznerFormCubit extends FormCubit { @@ -27,7 +27,7 @@ class HetznerFormCubit extends FormCubit { initializingCubit.setHetznerKey(apiKey.state.value); } - final AppConfigCubit initializingCubit; + final ServerInstallationCubit initializingCubit; late final FieldCubit apiKey; diff --git a/lib/logic/cubit/forms/setup/initializing/root_user_form_cubit.dart b/lib/logic/cubit/forms/setup/initializing/root_user_form_cubit.dart index 6d8b55c0..3bff2aba 100644 --- a/lib/logic/cubit/forms/setup/initializing/root_user_form_cubit.dart +++ b/lib/logic/cubit/forms/setup/initializing/root_user_form_cubit.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'package:cubit_form/cubit_form.dart'; -import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.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/models/hive/user.dart'; @@ -25,7 +25,7 @@ class RootUserFormCubit extends FormCubit { initializingCubit.setRootUser(user); } - final AppConfigCubit initializingCubit; + final ServerInstallationCubit initializingCubit; late final FieldCubit userName; late final FieldCubit password; diff --git a/lib/logic/cubit/forms/setup/recovering/recovery_domain_form_cubit.dart b/lib/logic/cubit/forms/setup/recovering/recovery_domain_form_cubit.dart index b592f5e1..d9d494ff 100644 --- a/lib/logic/cubit/forms/setup/recovering/recovery_domain_form_cubit.dart +++ b/lib/logic/cubit/forms/setup/recovering/recovery_domain_form_cubit.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'package:cubit_form/cubit_form.dart'; import 'package:selfprivacy/logic/api_maps/server.dart'; -import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.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/models/hive/server_domain.dart'; @@ -27,6 +27,6 @@ class RecoveryDomainFormCubit extends FormCubit { // ; //var client = // } - final AppConfigCubit initializingCubit; + final ServerInstallationCubit initializingCubit; late final FieldCubit serverDomainField; } diff --git a/lib/logic/cubit/app_config/app_config_cubit.dart b/lib/logic/cubit/server_installation/server_installation_cubit.dart similarity index 78% rename from lib/logic/cubit/app_config/app_config_cubit.dart rename to lib/logic/cubit/server_installation/server_installation_cubit.dart index 870154f1..74a9c6e9 100644 --- a/lib/logic/cubit/app_config/app_config_cubit.dart +++ b/lib/logic/cubit/server_installation/server_installation_cubit.dart @@ -9,14 +9,14 @@ import 'package:selfprivacy/logic/models/hive/server_domain.dart'; import 'package:selfprivacy/logic/models/hive/server_details.dart'; import 'package:selfprivacy/logic/models/hive/user.dart'; -import 'app_config_repository.dart'; +import '../server_installation/server_installation_repository.dart'; export 'package:provider/provider.dart'; -part 'app_config_state.dart'; +part '../server_installation/server_installation_state.dart'; -class AppConfigCubit extends Cubit { - AppConfigCubit() : super(AppConfigEmpty()); +class ServerInstallationCubit extends Cubit { + ServerInstallationCubit() : super(ServerInstallationEmpty()); final repository = AppConfigRepository(); @@ -25,9 +25,9 @@ class AppConfigCubit extends Cubit { Future load() async { var state = await repository.load(); - if (state is AppConfigFinished) { + if (state is ServerInstallationFinished) { emit(state); - } else if (state is AppConfigNotFinished) { + } else if (state is ServerInstallationNotFinished) { if (state.progress == ServerSetupProgress.serverCreated) { startServerIfDnsIsOkay(state: state); } else if (state.progress == ServerSetupProgress.serverStarted) { @@ -40,7 +40,7 @@ class AppConfigCubit extends Cubit { } else { emit(state); } - } else if (state is AppConfigRecovery) { + } else if (state is ServerInstallationRecovery) { emit(state); } else { throw 'wrong state'; @@ -49,13 +49,14 @@ class AppConfigCubit extends Cubit { void setHetznerKey(String hetznerKey) async { await repository.saveHetznerKey(hetznerKey); - emit((state as AppConfigNotFinished).copyWith(hetznerKey: hetznerKey)); + emit((state as ServerInstallationNotFinished) + .copyWith(hetznerKey: hetznerKey)); } void setCloudflareKey(String cloudFlareKey) async { await repository.saveCloudFlareKey(cloudFlareKey); - emit( - (state as AppConfigNotFinished).copyWith(cloudFlareKey: cloudFlareKey)); + emit((state as ServerInstallationNotFinished) + .copyWith(cloudFlareKey: cloudFlareKey)); } void setBackblazeKey(String keyId, String applicationKey) async { @@ -64,24 +65,26 @@ class AppConfigCubit extends Cubit { applicationKey: applicationKey, ); await repository.saveBackblazeKey(backblazeCredential); - emit((state as AppConfigNotFinished) + emit((state as ServerInstallationNotFinished) .copyWith(backblazeCredential: backblazeCredential)); } void setDomain(ServerDomain serverDomain) async { await repository.saveDomain(serverDomain); - emit((state as AppConfigNotFinished).copyWith(serverDomain: serverDomain)); + emit((state as ServerInstallationNotFinished) + .copyWith(serverDomain: serverDomain)); } void setRootUser(User rootUser) async { await repository.saveRootUser(rootUser); - emit((state as AppConfigNotFinished).copyWith(rootUser: rootUser)); + emit((state as ServerInstallationNotFinished).copyWith(rootUser: rootUser)); } void createServerAndSetDnsRecords() async { - AppConfigNotFinished _stateCopy = state as AppConfigNotFinished; - var onCancel = - () => emit((state as AppConfigNotFinished).copyWith(isLoading: false)); + ServerInstallationNotFinished _stateCopy = + state as ServerInstallationNotFinished; + var onCancel = () => emit( + (state as ServerInstallationNotFinished).copyWith(isLoading: false)); var onSuccess = (ServerHostingDetails serverDetails) async { await repository.createDnsRecords( @@ -90,7 +93,7 @@ class AppConfigCubit extends Cubit { onCancel: onCancel, ); - emit((state as AppConfigNotFinished).copyWith( + emit((state as ServerInstallationNotFinished).copyWith( isLoading: false, serverDetails: serverDetails, )); @@ -98,7 +101,7 @@ class AppConfigCubit extends Cubit { }; try { - emit((state as AppConfigNotFinished).copyWith(isLoading: true)); + emit((state as ServerInstallationNotFinished).copyWith(isLoading: true)); await repository.createServer( state.rootUser!, state.serverDomain!.domainName, @@ -112,8 +115,8 @@ class AppConfigCubit extends Cubit { } } - void startServerIfDnsIsOkay({AppConfigNotFinished? state}) async { - final dataState = state ?? this.state as AppConfigNotFinished; + void startServerIfDnsIsOkay({ServerInstallationNotFinished? state}) async { + final dataState = state ?? this.state as ServerInstallationNotFinished; emit(TimerState(dataState: dataState, isLoading: true)); @@ -149,8 +152,8 @@ class AppConfigCubit extends Cubit { } } - void oneMoreReset({AppConfigNotFinished? state}) async { - final dataState = state ?? this.state as AppConfigNotFinished; + void oneMoreReset({ServerInstallationNotFinished? state}) async { + final dataState = state ?? this.state as ServerInstallationNotFinished; emit(TimerState(dataState: dataState, isLoading: true)); @@ -184,9 +187,9 @@ class AppConfigCubit extends Cubit { } void resetServerIfServerIsOkay({ - AppConfigNotFinished? state, + ServerInstallationNotFinished? state, }) async { - final dataState = state ?? this.state as AppConfigNotFinished; + final dataState = state ?? this.state as ServerInstallationNotFinished; emit(TimerState(dataState: dataState, isLoading: true)); @@ -220,9 +223,9 @@ class AppConfigCubit extends Cubit { } void finishCheckIfServerIsOkay({ - AppConfigNotFinished? state, + ServerInstallationNotFinished? state, }) async { - final dataState = state ?? this.state as AppConfigNotFinished; + final dataState = state ?? this.state as ServerInstallationNotFinished; emit(TimerState(dataState: dataState, isLoading: true)); @@ -238,9 +241,9 @@ class AppConfigCubit extends Cubit { } } - void runDelayed( - void Function() work, Duration delay, AppConfigNotFinished? state) async { - final dataState = state ?? this.state as AppConfigNotFinished; + void runDelayed(void Function() work, Duration delay, + ServerInstallationNotFinished? state) async { + final dataState = state ?? this.state as ServerInstallationNotFinished; emit(TimerState( dataState: dataState, @@ -255,7 +258,7 @@ class AppConfigCubit extends Cubit { closeTimer(); repository.clearAppConfig(); - emit(AppConfigEmpty()); + emit(ServerInstallationEmpty()); } Future serverDelete() async { @@ -266,7 +269,7 @@ class AppConfigCubit extends Cubit { await getIt().clear(); } await repository.deleteRecords(); - emit(AppConfigNotFinished( + emit(ServerInstallationNotFinished( hetznerKey: state.hetznerKey, serverDomain: state.serverDomain, cloudFlareKey: state.cloudFlareKey, diff --git a/lib/logic/cubit/app_config/app_config_repository.dart b/lib/logic/cubit/server_installation/server_installation_repository.dart similarity index 97% rename from lib/logic/cubit/app_config/app_config_repository.dart rename to lib/logic/cubit/server_installation/server_installation_repository.dart index d8edaedc..9a2ce1a2 100644 --- a/lib/logic/cubit/app_config/app_config_repository.dart +++ b/lib/logic/cubit/server_installation/server_installation_repository.dart @@ -15,12 +15,12 @@ import 'package:selfprivacy/logic/models/hive/user.dart'; import 'package:selfprivacy/ui/components/action_button/action_button.dart'; import 'package:selfprivacy/ui/components/brand_alert/brand_alert.dart'; -import 'app_config_cubit.dart'; +import '../server_installation/server_installation_cubit.dart'; class AppConfigRepository { Box box = Hive.box(BNames.appConfig); - Future load() async { + Future load() async { final hetznerToken = getIt().hetznerKey; final cloudflareToken = getIt().cloudFlareKey; final serverDomain = getIt().serverDomain; @@ -28,7 +28,7 @@ class AppConfigRepository { final serverDetails = getIt().serverDetails; if (box.get(BNames.hasFinalChecked, defaultValue: false)) { - return AppConfigFinished( + return ServerInstallationFinished( hetznerKey: hetznerToken!, cloudFlareKey: cloudflareToken!, serverDomain: serverDomain!, @@ -44,7 +44,7 @@ class AppConfigRepository { } if (getIt().serverDomain?.provider == DnsProvider.Unknown) { - return AppConfigRecovery( + return ServerInstallationRecovery( hetznerKey: hetznerToken, cloudFlareKey: cloudflareToken, serverDomain: serverDomain, @@ -56,7 +56,7 @@ class AppConfigRepository { ); } - return AppConfigNotFinished( + return ServerInstallationNotFinished( hetznerKey: hetznerToken, cloudFlareKey: cloudflareToken, serverDomain: serverDomain, diff --git a/lib/logic/cubit/app_config/app_config_state.dart b/lib/logic/cubit/server_installation/server_installation_state.dart similarity index 90% rename from lib/logic/cubit/app_config/app_config_state.dart rename to lib/logic/cubit/server_installation/server_installation_state.dart index 9d620e9e..e0fbd1bf 100644 --- a/lib/logic/cubit/app_config/app_config_state.dart +++ b/lib/logic/cubit/server_installation/server_installation_state.dart @@ -1,7 +1,7 @@ -part of 'app_config_cubit.dart'; +part of '../server_installation/server_installation_cubit.dart'; -abstract class AppConfigState extends Equatable { - const AppConfigState({ +abstract class ServerInstallationState extends Equatable { + const ServerInstallationState({ required this.hetznerKey, required this.cloudFlareKey, required this.backblazeCredential, @@ -73,7 +73,7 @@ abstract class AppConfigState extends Equatable { } } -class TimerState extends AppConfigNotFinished { +class TimerState extends ServerInstallationNotFinished { TimerState({ required this.dataState, this.timerStart, @@ -93,7 +93,7 @@ class TimerState extends AppConfigNotFinished { dnsMatches: dataState.dnsMatches, ); - final AppConfigNotFinished dataState; + final ServerInstallationNotFinished dataState; final DateTime? timerStart; final Duration? duration; @@ -118,11 +118,11 @@ enum ServerSetupProgress { serverResetedSecondTime, } -class AppConfigNotFinished extends AppConfigState { +class ServerInstallationNotFinished extends ServerInstallationState { final bool isLoading; final Map? dnsMatches; - AppConfigNotFinished({ + ServerInstallationNotFinished({ String? hetznerKey, String? cloudFlareKey, BackblazeCredential? backblazeCredential, @@ -160,7 +160,7 @@ class AppConfigNotFinished extends AppConfigState { dnsMatches, ]; - AppConfigNotFinished copyWith({ + ServerInstallationNotFinished copyWith({ String? hetznerKey, String? cloudFlareKey, BackblazeCredential? backblazeCredential, @@ -173,7 +173,7 @@ class AppConfigNotFinished extends AppConfigState { bool? isLoading, Map? dnsMatches, }) => - AppConfigNotFinished( + ServerInstallationNotFinished( hetznerKey: hetznerKey ?? this.hetznerKey, cloudFlareKey: cloudFlareKey ?? this.cloudFlareKey, backblazeCredential: backblazeCredential ?? this.backblazeCredential, @@ -189,7 +189,7 @@ class AppConfigNotFinished extends AppConfigState { dnsMatches: dnsMatches ?? this.dnsMatches, ); - AppConfigFinished finish() => AppConfigFinished( + ServerInstallationFinished finish() => ServerInstallationFinished( hetznerKey: hetznerKey!, cloudFlareKey: cloudFlareKey!, backblazeCredential: backblazeCredential!, @@ -202,8 +202,8 @@ class AppConfigNotFinished extends AppConfigState { ); } -class AppConfigEmpty extends AppConfigNotFinished { - AppConfigEmpty() +class ServerInstallationEmpty extends ServerInstallationNotFinished { + ServerInstallationEmpty() : super( hetznerKey: null, cloudFlareKey: null, @@ -219,8 +219,8 @@ class AppConfigEmpty extends AppConfigNotFinished { ); } -class AppConfigFinished extends AppConfigState { - const AppConfigFinished({ +class ServerInstallationFinished extends ServerInstallationState { + const ServerInstallationFinished({ required String hetznerKey, required String cloudFlareKey, required BackblazeCredential backblazeCredential, @@ -265,10 +265,10 @@ enum RecoveryStep { BackblazeToken, } -class AppConfigRecovery extends AppConfigState { +class ServerInstallationRecovery extends ServerInstallationState { final RecoveryStep currentStep; - const AppConfigRecovery({ + const ServerInstallationRecovery({ String? hetznerKey, String? cloudFlareKey, BackblazeCredential? backblazeCredential, @@ -301,7 +301,7 @@ class AppConfigRecovery extends AppConfigState { currentStep ]; - AppConfigRecovery copyWith({ + ServerInstallationRecovery copyWith({ String? hetznerKey, String? cloudFlareKey, BackblazeCredential? backblazeCredential, @@ -310,7 +310,7 @@ class AppConfigRecovery extends AppConfigState { ServerHostingDetails? serverDetails, RecoveryStep? currentStep, }) => - AppConfigRecovery( + ServerInstallationRecovery( hetznerKey: hetznerKey ?? this.hetznerKey, cloudFlareKey: cloudFlareKey ?? this.cloudFlareKey, backblazeCredential: backblazeCredential ?? this.backblazeCredential, diff --git a/lib/logic/cubit/services/services_cubit.dart b/lib/logic/cubit/services/services_cubit.dart index 6d5dfef7..b05cd014 100644 --- a/lib/logic/cubit/services/services_cubit.dart +++ b/lib/logic/cubit/services/services_cubit.dart @@ -6,14 +6,14 @@ import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_depe part 'services_state.dart'; -class ServicesCubit extends AppConfigDependendCubit { - ServicesCubit(AppConfigCubit appConfigCubit) +class ServicesCubit extends ServerInstallationDependendCubit { + ServicesCubit(ServerInstallationCubit appConfigCubit) : super(appConfigCubit, ServicesState.allOff()); Box box = Hive.box(BNames.servicesState); final api = ServerApi(); Future load() async { - if (appConfigCubit.state is AppConfigFinished) { + if (appConfigCubit.state is ServerInstallationFinished) { var statuses = await api.servicesPowerCheck(); emit( ServicesState( diff --git a/lib/logic/cubit/services/services_state.dart b/lib/logic/cubit/services/services_state.dart index 3595e6b1..2503a4c7 100644 --- a/lib/logic/cubit/services/services_state.dart +++ b/lib/logic/cubit/services/services_state.dart @@ -1,6 +1,6 @@ part of 'services_cubit.dart'; -class ServicesState extends AppConfigDependendState { +class ServicesState extends ServerInstallationDependendState { const ServicesState({ required this.isPasswordManagerEnable, required this.isCloudEnable, diff --git a/lib/logic/cubit/users/users_cubit.dart b/lib/logic/cubit/users/users_cubit.dart index 22dabb46..9b749963 100644 --- a/lib/logic/cubit/users/users_cubit.dart +++ b/lib/logic/cubit/users/users_cubit.dart @@ -10,8 +10,8 @@ export 'package:provider/provider.dart'; part 'users_state.dart'; -class UsersCubit extends AppConfigDependendCubit { - UsersCubit(AppConfigCubit appConfigCubit) +class UsersCubit extends ServerInstallationDependendCubit { + UsersCubit(ServerInstallationCubit appConfigCubit) : super( appConfigCubit, UsersState( @@ -22,7 +22,7 @@ class UsersCubit extends AppConfigDependendCubit { final api = ServerApi(); Future load() async { - if (appConfigCubit.state is AppConfigFinished) { + if (appConfigCubit.state is ServerInstallationFinished) { var loadedUsers = box.values.toList(); final primaryUser = configBox.get(BNames.rootUser, defaultValue: User(login: 'loading...')); diff --git a/lib/logic/cubit/users/users_state.dart b/lib/logic/cubit/users/users_state.dart index d15789c9..4262f26a 100644 --- a/lib/logic/cubit/users/users_state.dart +++ b/lib/logic/cubit/users/users_state.dart @@ -1,6 +1,6 @@ part of 'users_cubit.dart'; -class UsersState extends AppConfigDependendState { +class UsersState extends ServerInstallationDependendState { const UsersState(this.users, this.rootUser, this.primaryUser); final List users; diff --git a/lib/ui/pages/backup_details/backup_details.dart b/lib/ui/pages/backup_details/backup_details.dart index a3562095..8a5eb688 100644 --- a/lib/ui/pages/backup_details/backup_details.dart +++ b/lib/ui/pages/backup_details/backup_details.dart @@ -2,7 +2,7 @@ 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/cubit/app_config/app_config_cubit.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'; @@ -28,7 +28,8 @@ class _BackupDetailsState extends State with SingleTickerProviderStateMixin { @override Widget build(BuildContext context) { - var isReady = context.watch().state is AppConfigFinished; + var isReady = context.watch().state + is ServerInstallationFinished; var isBackupInitialized = context.watch().state.isInitialized; var backupStatus = context.watch().state.status; var providerState = isReady && isBackupInitialized diff --git a/lib/ui/pages/dns_details/dns_details.dart b/lib/ui/pages/dns_details/dns_details.dart index e07cc445..3b5fd555 100644 --- a/lib/ui/pages/dns_details/dns_details.dart +++ b/lib/ui/pages/dns_details/dns_details.dart @@ -1,7 +1,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:selfprivacy/config/get_it_config.dart'; -import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.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/brand_cards.dart'; import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; @@ -62,7 +62,8 @@ class _DnsDetailsPageState extends State { @override Widget build(BuildContext context) { - var isReady = context.watch().state is AppConfigFinished; + var isReady = context.watch().state + is ServerInstallationFinished; final domain = getIt().serverDomain?.domainName ?? ''; var dnsCubit = context.watch().state; diff --git a/lib/ui/pages/more/app_settings/app_setting.dart b/lib/ui/pages/more/app_settings/app_setting.dart index 8c5a78c8..37bbe83e 100644 --- a/lib/ui/pages/more/app_settings/app_setting.dart +++ b/lib/ui/pages/more/app_settings/app_setting.dart @@ -2,7 +2,7 @@ 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/app_config/app_config_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_divider/brand_divider.dart'; @@ -103,7 +103,7 @@ class _AppSettingsPageState extends State { isRed: true, onPressed: () { context - .read() + .read() .clearAppConfig(); Navigator.of(context).pop(); }), @@ -129,7 +129,7 @@ class _AppSettingsPageState extends State { Widget deleteServer(BuildContext context) { var isDisabled = - context.watch().state.serverDetails == null; + context.watch().state.serverDetails == null; return Container( padding: EdgeInsets.only(top: 20, bottom: 5), decoration: BoxDecoration( @@ -181,7 +181,7 @@ class _AppSettingsPageState extends State { ); }); await context - .read() + .read() .serverDelete(); Navigator.of(context).pop(); }), diff --git a/lib/ui/pages/providers/providers.dart b/lib/ui/pages/providers/providers.dart index 974f29ce..108c85e2 100644 --- a/lib/ui/pages/providers/providers.dart +++ b/lib/ui/pages/providers/providers.dart @@ -1,7 +1,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:selfprivacy/config/brand_theme.dart'; -import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.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/cubit/dns_records/dns_records_cubit.dart'; import 'package:selfprivacy/logic/cubit/providers/providers_cubit.dart'; @@ -30,7 +30,8 @@ class ProvidersPage extends StatefulWidget { class _ProvidersPageState extends State { @override Widget build(BuildContext context) { - var isReady = context.watch().state is AppConfigFinished; + var isReady = context.watch().state + is ServerInstallationFinished; var isBackupInitialized = context.watch().state.isInitialized; var dnsStatus = context.watch().state.dnsState; @@ -96,8 +97,10 @@ class _Card extends StatelessWidget { String? message; late String stableText; late VoidCallback onTap; - var isReady = context.watch().state is AppConfigFinished; - AppConfigState appConfig = context.watch().state; + var isReady = context.watch().state + is ServerInstallationFinished; + ServerInstallationState appConfig = + context.watch().state; var domainName = appConfig.isDomainFilled ? appConfig.serverDomain!.domainName : ''; diff --git a/lib/ui/pages/server_details/server_details.dart b/lib/ui/pages/server_details/server_details.dart index 5368df30..90d4144e 100644 --- a/lib/ui/pages/server_details/server_details.dart +++ b/lib/ui/pages/server_details/server_details.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import 'package:selfprivacy/config/brand_colors.dart'; import 'package:selfprivacy/config/brand_theme.dart'; import 'package:selfprivacy/logic/common_enum/common_enum.dart'; -import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart'; +import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; import 'package:selfprivacy/logic/cubit/hetzner_metrics/hetzner_metrics_cubit.dart'; import 'package:selfprivacy/logic/cubit/server_detailed_info/server_detailed_info_cubit.dart'; import 'package:selfprivacy/logic/models/state_types.dart'; @@ -60,7 +60,8 @@ class _ServerDetailsState extends State @override Widget build(BuildContext context) { - var isReady = context.watch().state is AppConfigFinished; + var isReady = context.watch().state + is ServerInstallationFinished; var providerState = isReady ? StateType.stable : StateType.uninitialized; return BlocProvider( diff --git a/lib/ui/pages/services/services.dart b/lib/ui/pages/services/services.dart index 5070bd81..7498e370 100644 --- a/lib/ui/pages/services/services.dart +++ b/lib/ui/pages/services/services.dart @@ -5,7 +5,7 @@ import 'package:selfprivacy/config/brand_colors.dart'; import 'package:selfprivacy/config/brand_theme.dart'; import 'package:selfprivacy/config/text_themes.dart'; import 'package:selfprivacy/logic/common_enum/common_enum.dart'; -import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart'; +import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; import 'package:selfprivacy/logic/cubit/jobs/jobs_cubit.dart'; import 'package:selfprivacy/logic/cubit/services/services_cubit.dart'; import 'package:selfprivacy/logic/models/job.dart'; @@ -58,7 +58,8 @@ void _launchURL(url) async { class _ServicesPageState extends State { @override Widget build(BuildContext context) { - var isReady = context.watch().state is AppConfigFinished; + var isReady = context.watch().state + is ServerInstallationFinished; return Scaffold( appBar: PreferredSize( @@ -94,7 +95,8 @@ class _Card extends StatelessWidget { final ServiceTypes serviceType; @override Widget build(BuildContext context) { - var isReady = context.watch().state is AppConfigFinished; + var isReady = context.watch().state + is ServerInstallationFinished; var changeTab = context.read().onPress; var serviceState = context.watch().state; @@ -111,7 +113,7 @@ class _Card extends StatelessWidget { (!switchableServices.contains(serviceType) || serviceState.isEnableByType(serviceType)); - var config = context.watch().state; + var config = context.watch().state; var domainName = UiHelpers.getDomainName(config); return GestureDetector( @@ -257,7 +259,7 @@ class _ServiceDetails extends StatelessWidget { Widget build(BuildContext context) { late Widget child; - var config = context.watch().state; + var config = context.watch().state; var domainName = UiHelpers.getDomainName(config); var linksStyle = body1Style.copyWith( diff --git a/lib/ui/pages/setup/initializing.dart b/lib/ui/pages/setup/initializing.dart index 016131ae..627f4014 100644 --- a/lib/ui/pages/setup/initializing.dart +++ b/lib/ui/pages/setup/initializing.dart @@ -2,7 +2,7 @@ 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/app_config/app_config_cubit.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/initializing/backblaze_form_cubit.dart'; import 'package:selfprivacy/logic/cubit/forms/setup/initializing/cloudflare_form_cubit.dart'; @@ -23,7 +23,7 @@ import 'package:selfprivacy/utils/route_transitions/basic.dart'; class InitializingPage extends StatelessWidget { @override Widget build(BuildContext context) { - var cubit = context.watch(); + var cubit = context.watch(); var actualInitializingPage = [ () => _stepHetzner(cubit), () => _stepCloudflare(cubit), @@ -36,9 +36,9 @@ class InitializingPage extends StatelessWidget { () => _stepCheck(cubit), () => Container(child: Center(child: Text('initializing.finish'.tr()))) ][cubit.state.progress.index](); - return BlocListener( + return BlocListener( listener: (context, state) { - if (cubit.state is AppConfigFinished) { + if (cubit.state is ServerInstallationFinished) { Navigator.of(context).pushReplacement(materialRoute(RootPage())); } }, @@ -86,7 +86,7 @@ class InitializingPage extends StatelessWidget { Container( alignment: Alignment.center, child: BrandButton.text( - title: cubit.state is AppConfigFinished + title: cubit.state is ServerInstallationFinished ? 'basis.close'.tr() : 'basis.later'.tr(), onPressed: () { @@ -97,7 +97,7 @@ class InitializingPage extends StatelessWidget { }, ), ), - (cubit.state is AppConfigFinished) + (cubit.state is ServerInstallationFinished) ? Container() : Container( alignment: Alignment.center, @@ -119,7 +119,7 @@ class InitializingPage extends StatelessWidget { ); } - Widget _stepHetzner(AppConfigCubit initializingCubit) { + Widget _stepHetzner(ServerInstallationCubit initializingCubit) { return BlocProvider( create: (context) => HetznerFormCubit(initializingCubit), child: Builder(builder: (context) { @@ -174,7 +174,7 @@ class InitializingPage extends StatelessWidget { ); } - Widget _stepCloudflare(AppConfigCubit initializingCubit) { + Widget _stepCloudflare(ServerInstallationCubit initializingCubit) { return BlocProvider( create: (context) => CloudFlareFormCubit(initializingCubit), child: Builder(builder: (context) { @@ -222,7 +222,7 @@ class InitializingPage extends StatelessWidget { ); } - Widget _stepBackblaze(AppConfigCubit initializingCubit) { + Widget _stepBackblaze(ServerInstallationCubit initializingCubit) { return BlocProvider( create: (context) => BackblazeFormCubit(initializingCubit), child: Builder(builder: (context) { @@ -277,7 +277,7 @@ class InitializingPage extends StatelessWidget { ); } - Widget _stepDomain(AppConfigCubit initializingCubit) { + Widget _stepDomain(ServerInstallationCubit initializingCubit) { return BlocProvider( create: (context) => DomainSetupCubit(initializingCubit)..load(), child: Builder(builder: (context) { @@ -369,7 +369,7 @@ class InitializingPage extends StatelessWidget { ); } - Widget _stepUser(AppConfigCubit initializingCubit) { + Widget _stepUser(ServerInstallationCubit initializingCubit) { return BlocProvider( create: (context) => RootUserFormCubit(initializingCubit, FieldCubitFactory(context)), @@ -432,8 +432,9 @@ class InitializingPage extends StatelessWidget { ); } - Widget _stepServer(AppConfigCubit appConfigCubit) { - var isLoading = (appConfigCubit.state as AppConfigNotFinished).isLoading; + Widget _stepServer(ServerInstallationCubit appConfigCubit) { + var isLoading = + (appConfigCubit.state as ServerInstallationNotFinished).isLoading; return Builder(builder: (context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -454,8 +455,9 @@ class InitializingPage extends StatelessWidget { }); } - Widget _stepCheck(AppConfigCubit appConfigCubit) { - assert(appConfigCubit.state is AppConfigNotFinished, 'wrong state'); + Widget _stepCheck(ServerInstallationCubit appConfigCubit) { + assert( + appConfigCubit.state is ServerInstallationNotFinished, 'wrong state'); var state = appConfigCubit.state as TimerState; late int doneCount; late String? text; diff --git a/lib/ui/pages/setup/recovering/recovery_fallback_select.dart b/lib/ui/pages/setup/recovering/recovery_fallback_select.dart index 2a1d7b21..bca3d07c 100644 --- a/lib/ui/pages/setup/recovering/recovery_fallback_select.dart +++ b/lib/ui/pages/setup/recovering/recovery_fallback_select.dart @@ -1,6 +1,5 @@ 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_cards/brand_cards.dart'; import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; import 'package:selfprivacy/utils/route_transitions/basic.dart'; diff --git a/lib/ui/pages/users/new_user.dart b/lib/ui/pages/users/new_user.dart index 58559c8f..05136b66 100644 --- a/lib/ui/pages/users/new_user.dart +++ b/lib/ui/pages/users/new_user.dart @@ -3,7 +3,7 @@ part of 'users.dart'; class _NewUser extends StatelessWidget { @override Widget build(BuildContext context) { - var config = context.watch().state; + var config = context.watch().state; var domainName = UiHelpers.getDomainName(config); diff --git a/lib/ui/pages/users/user_details.dart b/lib/ui/pages/users/user_details.dart index a40a7b9a..69f3b87e 100644 --- a/lib/ui/pages/users/user_details.dart +++ b/lib/ui/pages/users/user_details.dart @@ -11,7 +11,7 @@ class _UserDetails extends StatelessWidget { final bool isRootUser; @override Widget build(BuildContext context) { - var config = context.watch().state; + var config = context.watch().state; var domainName = UiHelpers.getDomainName(config); diff --git a/lib/ui/pages/users/users.dart b/lib/ui/pages/users/users.dart index 7110f986..f10a4afa 100644 --- a/lib/ui/pages/users/users.dart +++ b/lib/ui/pages/users/users.dart @@ -5,7 +5,7 @@ import 'package:flutter/material.dart'; import 'package:selfprivacy/config/brand_colors.dart'; import 'package:selfprivacy/config/brand_theme.dart'; import 'package:selfprivacy/config/text_themes.dart'; -import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.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/user/user_form_cubit.dart'; import 'package:selfprivacy/logic/cubit/jobs/jobs_cubit.dart'; @@ -38,7 +38,8 @@ class UsersPage extends StatelessWidget { @override Widget build(BuildContext context) { // final usersCubitState = context.watch().state; - var isReady = context.watch().state is AppConfigFinished; + var isReady = context.watch().state + is ServerInstallationFinished; // final primaryUser = usersCubitState.primaryUser; // final users = [primaryUser, ...usersCubitState.users]; // final isEmpty = users.isEmpty; diff --git a/lib/utils/ui_helpers.dart b/lib/utils/ui_helpers.dart index 734bf4f9..1958c90f 100644 --- a/lib/utils/ui_helpers.dart +++ b/lib/utils/ui_helpers.dart @@ -1,8 +1,8 @@ -import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart'; +import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; /// it's ui helpers use only for ui components, don't use for logic components. class UiHelpers { - static String getDomainName(AppConfigState config) => + static String getDomainName(ServerInstallationState config) => config.isDomainFilled ? config.serverDomain!.domainName : 'example.com'; } From 19bc780db1fa91bf227d08b956e888eb382399ac Mon Sep 17 00:00:00 2001 From: NaiJi Date: Tue, 17 May 2022 18:55:04 +0300 Subject: [PATCH 16/52] Implement async validation of domain field on recovering access Co-authored-by: Inex Code --- assets/translations/en.json | 3 ++- assets/translations/ru.json | 1 + .../recovery_domain_form_cubit.dart | 24 +++++++++++++++---- lib/ui/pages/setup/initializing.dart | 6 ++--- 4 files changed, 26 insertions(+), 8 deletions(-) diff --git a/assets/translations/en.json b/assets/translations/en.json index 22a6b0ee..78ba00c5 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -285,8 +285,9 @@ }, "recovering": { "recovery_main_header": "Connect to an existing server", - "domain_recovery_description": "Enter a server domain you want to get access for", + "domain_recovery_description": "Enter a server domain you want to get access for:", "domain_recover_placeholder": "Your domain", + "domain_recover_error": "Server with such domain is not found", "method_select_description": "Select a recovery method:", "method_select_other_device": "I have access on another device", "method_select_recovery_key": "I have a recovery key", diff --git a/assets/translations/ru.json b/assets/translations/ru.json index 8c1a8397..6a898516 100644 --- a/assets/translations/ru.json +++ b/assets/translations/ru.json @@ -288,6 +288,7 @@ "recovery_main_header": "Подключиться к существующему серверу", "domain_recovery_description": "Введите домен, по которому вы хотите получить доступ к серверу:", "domain_recover_placeholder": "Домен", + "domain_recover_error": "Не удалось найти сервер с таким доменом", "method_select_description": "Выберите способ входа:", "method_select_other_device": "У меня есть доступ на другом устройстве", "method_select_recovery_key": "У меня есть ключ восстановления", diff --git a/lib/logic/cubit/forms/setup/recovering/recovery_domain_form_cubit.dart b/lib/logic/cubit/forms/setup/recovering/recovery_domain_form_cubit.dart index d9d494ff..59e512d2 100644 --- a/lib/logic/cubit/forms/setup/recovering/recovery_domain_form_cubit.dart +++ b/lib/logic/cubit/forms/setup/recovering/recovery_domain_form_cubit.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:cubit_form/cubit_form.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:selfprivacy/logic/api_maps/server.dart'; import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; import 'package:selfprivacy/logic/cubit/forms/factories/field_cubit_factory.dart'; @@ -22,10 +23,25 @@ class RecoveryDomainFormCubit extends FormCubit { zoneId: "")); } - // @override - // FutureOr asyncValidation() async { - // ; //var client = - // } + @override + FutureOr asyncValidation() async { + var api = ServerApi( + hasLogger: false, + isWithToken: false, + overrideDomain: serverDomainField.state.value); + + // API version doesn't require access token, + // so if the entered domain is indeed valid + // and the server by it is reachable, we will + // be able to confirm the input + + final bool domainValid = await api.getApiVersion() != null; + if (!domainValid) { + serverDomainField.setError("recovering.domain_recover_error".tr()); + } + + return domainValid; + } final ServerInstallationCubit initializingCubit; late final FieldCubit serverDomainField; diff --git a/lib/ui/pages/setup/initializing.dart b/lib/ui/pages/setup/initializing.dart index 627f4014..8e6fd1ec 100644 --- a/lib/ui/pages/setup/initializing.dart +++ b/lib/ui/pages/setup/initializing.dart @@ -17,7 +17,7 @@ import 'package:selfprivacy/ui/components/brand_text/brand_text.dart'; import 'package:selfprivacy/ui/components/brand_timer/brand_timer.dart'; import 'package:selfprivacy/ui/components/progress_bar/progress_bar.dart'; import 'package:selfprivacy/ui/pages/rootRoute.dart'; -import 'package:selfprivacy/ui/pages/setup/recovering/recovery_method_select.dart'; +import 'package:selfprivacy/ui/pages/setup/recovering/recovery_domain.dart'; import 'package:selfprivacy/utils/route_transitions/basic.dart'; class InitializingPage extends StatelessWidget { @@ -104,8 +104,8 @@ class InitializingPage extends StatelessWidget { child: BrandButton.text( title: 'basis.connect_to_existing'.tr(), onPressed: () { - Navigator.of(context).push( - materialRoute(RecoveryMethodSelect())); + Navigator.of(context) + .push(materialRoute(RecoveryDomain())); }, ), ) From bf79fb1adf3b4d89189a36804e3a97fd2121c981 Mon Sep 17 00:00:00 2001 From: Inex Code Date: Tue, 17 May 2022 23:08:28 +0300 Subject: [PATCH 17/52] - Refactor Hive boxes - Delete SSH generation leftovers - Migrate users box to an encrypted box --- lib/config/bloc_config.dart | 2 +- lib/config/get_it_config.dart | 2 - lib/config/hive_config.dart | 79 ++++++++++++++----- .../authentication_dependend_cubit.dart | 9 ++- .../app_settings/app_settings_cubit.dart | 14 ++-- .../app_settings/app_settings_state.dart | 10 +-- lib/logic/cubit/backups/backups_cubit.dart | 10 +-- .../cubit/dns_records/dns_records_cubit.dart | 17 ++-- .../server_installation_cubit.dart | 5 +- .../server_installation_repository.dart | 4 +- lib/logic/cubit/services/services_cubit.dart | 11 +-- lib/logic/cubit/users/users_cubit.dart | 37 +++++---- lib/logic/get_it/api_config.dart | 6 +- lib/logic/get_it/ssh.dart | 39 --------- lib/logic/models/json/api_token.dart | 1 - lib/main.dart | 2 +- lib/ui/components/brand_md/brand_md.dart | 6 +- .../brand_span_button/brand_span_button.dart | 4 +- lib/ui/pages/services/services.dart | 15 ++-- .../setup/recovering/recovery_domain.dart | 6 +- pubspec.lock | 9 +-- pubspec.yaml | 2 - 22 files changed, 134 insertions(+), 156 deletions(-) delete mode 100644 lib/logic/get_it/ssh.dart diff --git a/lib/config/bloc_config.dart b/lib/config/bloc_config.dart index c13caeaa..3456b2de 100644 --- a/lib/config/bloc_config.dart +++ b/lib/config/bloc_config.dart @@ -27,7 +27,7 @@ class BlocAndProviderConfig extends StatelessWidget { BlocProvider( create: (_) => AppSettingsCubit( isDarkModeOn: isDark, - isOnbordingShowing: true, + isOnboardingShowing: true, )..load(), ), BlocProvider(create: (_) => serverInstallationCubit, lazy: false), diff --git a/lib/config/get_it_config.dart b/lib/config/get_it_config.dart index eb5c0902..1b9fd1f0 100644 --- a/lib/config/get_it_config.dart +++ b/lib/config/get_it_config.dart @@ -2,7 +2,6 @@ import 'package:get_it/get_it.dart'; import 'package:selfprivacy/logic/get_it/api_config.dart'; import 'package:selfprivacy/logic/get_it/console.dart'; import 'package:selfprivacy/logic/get_it/navigation.dart'; -import 'package:selfprivacy/logic/get_it/ssh.dart'; import 'package:selfprivacy/logic/get_it/timer.dart'; export 'package:selfprivacy/logic/get_it/api_config.dart'; @@ -17,7 +16,6 @@ Future getItSetup() async { getIt.registerSingleton(ConsoleModel()); getIt.registerSingleton(TimerModel()); - getIt.registerSingleton(SSHModel()..init()); getIt.registerSingleton(ApiConfigModel()..init()); await getIt.allReady(); diff --git a/lib/config/hive_config.dart b/lib/config/hive_config.dart index e5ef6927..21f329b2 100644 --- a/lib/config/hive_config.dart +++ b/lib/config/hive_config.dart @@ -19,15 +19,22 @@ class HiveConfig { Hive.registerAdapter(BackblazeBucketAdapter()); Hive.registerAdapter(ServerVolumeAdapter()); - await Hive.openBox(BNames.appSettings); - await Hive.openBox(BNames.users); - await Hive.openBox(BNames.servicesState); + await Hive.openBox(BNames.appSettingsBox); - var cipher = HiveAesCipher(await getEncryptedKey(BNames.key)); - await Hive.openBox(BNames.appConfig, encryptionCipher: cipher); + var cipher = HiveAesCipher( + await getEncryptedKey(BNames.serverInstallationEncryptionKey)); - var sshCipher = HiveAesCipher(await getEncryptedKey(BNames.sshEnckey)); - await Hive.openBox(BNames.sshConfig, encryptionCipher: sshCipher); + await Hive.openBox(BNames.usersDeprecated); + await Hive.openBox(BNames.users, encryptionCipher: cipher); + + Box deprecatedUsers = Hive.box(BNames.usersDeprecated); + if (deprecatedUsers.isNotEmpty) { + Box users = Hive.box(BNames.users); + users.addAll(deprecatedUsers.values.toList()); + deprecatedUsers.clear(); + } + + await Hive.openBox(BNames.serverInstallation, encryptionCipher: cipher); } static Future getEncryptedKey(String encKey) async { @@ -43,33 +50,65 @@ class HiveConfig { } } +/// Mappings for the different boxes and their keys class BNames { - static String appConfig = 'appConfig'; + /// App settings box. Contains app settings like [isDarkModeOn], [isOnboardingShowing] + static String appSettingsBox = 'appSettings'; + + /// A boolean field of [appSettingsBox] box. static String isDarkModeOn = 'isDarkModeOn'; - static String isOnbordingShowing = 'isOnbordingShowing'; - static String users = 'users'; + + /// A boolean field of [appSettingsBox] box. + static String isOnboardingShowing = 'isOnboardingShowing'; + + /// Encryption key to decrypt [serverInstallation] and [users] box. + static String serverInstallationEncryptionKey = 'key'; + + /// Server installation box. Contains server details and provider tokens. + static String serverInstallation = 'appConfig'; + + /// A List field of [serverInstallation] box. static String rootKeys = 'rootKeys'; - static String appSettings = 'appSettings'; - static String servicesState = 'servicesState'; - - static String key = 'key'; - static String sshEnckey = 'sshEngkey'; - + /// A boolean field of [serverInstallation] box. static String hasFinalChecked = 'hasFinalChecked'; + + /// A boolean field of [serverInstallation] box. static String isServerStarted = 'isServerStarted'; + /// A [ServerDomain] field of [serverInstallation] box. static String serverDomain = 'cloudFlareDomain'; + + /// A String field of [serverInstallation] box. static String hetznerKey = 'hetznerKey'; + + /// A String field of [serverInstallation] box. static String cloudFlareKey = 'cloudFlareKey'; + + /// A [User] field of [serverInstallation] box. static String rootUser = 'rootUser'; + + /// A [ServerHostingDetails] field of [serverInstallation] box. static String serverDetails = 'hetznerServer'; - static String backblazeKey = 'backblazeKey'; + + /// A [BackblazeCredential] field of [serverInstallation] box. + static String backblazeCredential = 'backblazeKey'; + + /// A [BackblazeBucket] field of [serverInstallation] box. static String backblazeBucket = 'backblazeBucket'; + + /// A boolean field of [serverInstallation] box. static String isLoading = 'isLoading'; + + /// A boolean field of [serverInstallation] box. static String isServerResetedFirstTime = 'isServerResetedFirstTime'; + + /// A boolean field of [serverInstallation] box. static String isServerResetedSecondTime = 'isServerResetedSecondTime'; - static String sshConfig = 'sshConfig'; - static String sshPrivateKey = "sshPrivateKey"; - static String sshPublicKey = "sshPublicKey"; + + /// Deprecated users box as it is unencrypted + static String usersDeprecated = 'users'; + + /// Box with users + static String users = 'usersEncrypted'; } diff --git a/lib/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart b/lib/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart index bf44cbe1..217cb15c 100644 --- a/lib/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart +++ b/lib/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart @@ -10,11 +10,12 @@ part 'authentication_dependend_state.dart'; abstract class ServerInstallationDependendCubit< T extends ServerInstallationDependendState> extends Cubit { ServerInstallationDependendCubit( - this.appConfigCubit, + this.serverInstallationCubit, T initState, ) : super(initState) { - authCubitSubscription = appConfigCubit.stream.listen(checkAuthStatus); - checkAuthStatus(appConfigCubit.state); + authCubitSubscription = + serverInstallationCubit.stream.listen(checkAuthStatus); + checkAuthStatus(serverInstallationCubit.state); } void checkAuthStatus(ServerInstallationState state) { @@ -26,7 +27,7 @@ abstract class ServerInstallationDependendCubit< } late StreamSubscription authCubitSubscription; - final ServerInstallationCubit appConfigCubit; + final ServerInstallationCubit serverInstallationCubit; void load(); void clear(); diff --git a/lib/logic/cubit/app_settings/app_settings_cubit.dart b/lib/logic/cubit/app_settings/app_settings_cubit.dart index 5dcf348e..fde5b083 100644 --- a/lib/logic/cubit/app_settings/app_settings_cubit.dart +++ b/lib/logic/cubit/app_settings/app_settings_cubit.dart @@ -9,22 +9,22 @@ part 'app_settings_state.dart'; class AppSettingsCubit extends Cubit { AppSettingsCubit({ required bool isDarkModeOn, - required bool isOnbordingShowing, + required bool isOnboardingShowing, }) : super( AppSettingsState( isDarkModeOn: isDarkModeOn, - isOnbordingShowing: isOnbordingShowing, + isOnboardingShowing: isOnboardingShowing, ), ); - Box box = Hive.box(BNames.appSettings); + Box box = Hive.box(BNames.appSettingsBox); void load() { bool? isDarkModeOn = box.get(BNames.isDarkModeOn); - bool? isOnbordingShowing = box.get(BNames.isOnbordingShowing); + bool? isOnboardingShowing = box.get(BNames.isOnboardingShowing); emit(state.copyWith( isDarkModeOn: isDarkModeOn, - isOnbordingShowing: isOnbordingShowing, + isOnboardingShowing: isOnboardingShowing, )); } @@ -34,8 +34,8 @@ class AppSettingsCubit extends Cubit { } void turnOffOnboarding() { - box.put(BNames.isOnbordingShowing, false); + box.put(BNames.isOnboardingShowing, false); - emit(state.copyWith(isOnbordingShowing: false)); + emit(state.copyWith(isOnboardingShowing: false)); } } diff --git a/lib/logic/cubit/app_settings/app_settings_state.dart b/lib/logic/cubit/app_settings/app_settings_state.dart index e1dae427..1300dcf4 100644 --- a/lib/logic/cubit/app_settings/app_settings_state.dart +++ b/lib/logic/cubit/app_settings/app_settings_state.dart @@ -3,18 +3,18 @@ part of 'app_settings_cubit.dart'; class AppSettingsState extends Equatable { const AppSettingsState({ required this.isDarkModeOn, - required this.isOnbordingShowing, + required this.isOnboardingShowing, }); final bool isDarkModeOn; - final bool isOnbordingShowing; + final bool isOnboardingShowing; - AppSettingsState copyWith({isDarkModeOn, isOnbordingShowing}) => + AppSettingsState copyWith({isDarkModeOn, isOnboardingShowing}) => AppSettingsState( isDarkModeOn: isDarkModeOn ?? this.isDarkModeOn, - isOnbordingShowing: isOnbordingShowing ?? this.isOnbordingShowing, + isOnboardingShowing: isOnboardingShowing ?? this.isOnboardingShowing, ); @override - List get props => [isDarkModeOn, isOnbordingShowing]; + List get props => [isDarkModeOn, isOnboardingShowing]; } diff --git a/lib/logic/cubit/backups/backups_cubit.dart b/lib/logic/cubit/backups/backups_cubit.dart index 4dd85046..67570c4e 100644 --- a/lib/logic/cubit/backups/backups_cubit.dart +++ b/lib/logic/cubit/backups/backups_cubit.dart @@ -11,14 +11,14 @@ import 'package:selfprivacy/logic/models/json/backup.dart'; part 'backups_state.dart'; class BackupsCubit extends ServerInstallationDependendCubit { - BackupsCubit(ServerInstallationCubit appConfigCubit) - : super(appConfigCubit, BackupsState(preventActions: true)); + BackupsCubit(ServerInstallationCubit serverInstallationCubit) + : super(serverInstallationCubit, BackupsState(preventActions: true)); final api = ServerApi(); final backblaze = BackblazeApi(); Future load() async { - if (appConfigCubit.state is ServerInstallationFinished) { + if (serverInstallationCubit.state is ServerInstallationFinished) { final bucket = getIt().backblazeBucket; if (bucket == null) { emit(BackupsState( @@ -85,9 +85,9 @@ class BackupsCubit extends ServerInstallationDependendCubit { Future createBucket() async { emit(state.copyWith(preventActions: true)); - final domain = appConfigCubit.state.serverDomain!.domainName + final domain = serverInstallationCubit.state.serverDomain!.domainName .replaceAll(RegExp(r'[^a-zA-Z0-9]'), '-'); - final serverId = appConfigCubit.state.serverDetails!.id; + final serverId = serverInstallationCubit.state.serverDetails!.id; var bucketName = 'selfprivacy-$domain-$serverId'; // If bucket name is too long, shorten it if (bucketName.length > 49) { diff --git a/lib/logic/cubit/dns_records/dns_records_cubit.dart b/lib/logic/cubit/dns_records/dns_records_cubit.dart index 52e09a2c..6b8bf715 100644 --- a/lib/logic/cubit/dns_records/dns_records_cubit.dart +++ b/lib/logic/cubit/dns_records/dns_records_cubit.dart @@ -10,8 +10,8 @@ part 'dns_records_state.dart'; class DnsRecordsCubit extends ServerInstallationDependendCubit { - DnsRecordsCubit(ServerInstallationCubit appConfigCubit) - : super(appConfigCubit, + DnsRecordsCubit(ServerInstallationCubit serverInstallationCubit) + : super(serverInstallationCubit, DnsRecordsState(dnsState: DnsRecordsStatus.refreshing)); final api = ServerApi(); @@ -21,11 +21,12 @@ class DnsRecordsCubit emit(DnsRecordsState( dnsState: DnsRecordsStatus.refreshing, dnsRecords: _getDesiredDnsRecords( - appConfigCubit.state.serverDomain?.domainName, "", ""))); + serverInstallationCubit.state.serverDomain?.domainName, "", ""))); print('Loading DNS status'); - if (appConfigCubit.state is ServerInstallationFinished) { - final ServerDomain? domain = appConfigCubit.state.serverDomain; - final String? ipAddress = appConfigCubit.state.serverDetails?.ip4; + if (serverInstallationCubit.state is ServerInstallationFinished) { + final ServerDomain? domain = serverInstallationCubit.state.serverDomain; + final String? ipAddress = + serverInstallationCubit.state.serverDetails?.ip4; if (domain != null && ipAddress != null) { final List records = await cloudflare.getDnsRecords(cloudFlareDomain: domain); @@ -96,8 +97,8 @@ class DnsRecordsCubit Future fix() async { emit(state.copyWith(dnsState: DnsRecordsStatus.refreshing)); - final ServerDomain? domain = appConfigCubit.state.serverDomain; - final String? ipAddress = appConfigCubit.state.serverDetails?.ip4; + final ServerDomain? domain = serverInstallationCubit.state.serverDomain; + final String? ipAddress = serverInstallationCubit.state.serverDetails?.ip4; final String? dkimPublicKey = await api.getDkim(); await cloudflare.removeSimilarRecords(cloudFlareDomain: domain!); await cloudflare.createMultipleDnsRecords( diff --git a/lib/logic/cubit/server_installation/server_installation_cubit.dart b/lib/logic/cubit/server_installation/server_installation_cubit.dart index 74a9c6e9..38b140db 100644 --- a/lib/logic/cubit/server_installation/server_installation_cubit.dart +++ b/lib/logic/cubit/server_installation/server_installation_cubit.dart @@ -2,8 +2,6 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; -import 'package:selfprivacy/config/get_it_config.dart'; -import 'package:selfprivacy/logic/get_it/ssh.dart'; import 'package:selfprivacy/logic/models/hive/backblaze_credential.dart'; import 'package:selfprivacy/logic/models/hive/server_domain.dart'; import 'package:selfprivacy/logic/models/hive/server_details.dart'; @@ -18,7 +16,7 @@ part '../server_installation/server_installation_state.dart'; class ServerInstallationCubit extends Cubit { ServerInstallationCubit() : super(ServerInstallationEmpty()); - final repository = AppConfigRepository(); + final repository = ServerInstallationRepository(); Timer? timer; @@ -266,7 +264,6 @@ class ServerInstallationCubit extends Cubit { if (state.serverDetails != null) { await repository.deleteServer(state.serverDomain!); - await getIt().clear(); } await repository.deleteRecords(); emit(ServerInstallationNotFinished( diff --git a/lib/logic/cubit/server_installation/server_installation_repository.dart b/lib/logic/cubit/server_installation/server_installation_repository.dart index 9a2ce1a2..6bc2613b 100644 --- a/lib/logic/cubit/server_installation/server_installation_repository.dart +++ b/lib/logic/cubit/server_installation/server_installation_repository.dart @@ -17,8 +17,8 @@ import 'package:selfprivacy/ui/components/brand_alert/brand_alert.dart'; import '../server_installation/server_installation_cubit.dart'; -class AppConfigRepository { - Box box = Hive.box(BNames.appConfig); +class ServerInstallationRepository { + Box box = Hive.box(BNames.serverInstallation); Future load() async { final hetznerToken = getIt().hetznerKey; diff --git a/lib/logic/cubit/services/services_cubit.dart b/lib/logic/cubit/services/services_cubit.dart index b05cd014..e503389d 100644 --- a/lib/logic/cubit/services/services_cubit.dart +++ b/lib/logic/cubit/services/services_cubit.dart @@ -1,5 +1,3 @@ -import 'package:hive/hive.dart'; -import 'package:selfprivacy/config/hive_config.dart'; import 'package:selfprivacy/logic/api_maps/server.dart'; import 'package:selfprivacy/logic/common_enum/common_enum.dart'; import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart'; @@ -7,13 +5,11 @@ import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_depe part 'services_state.dart'; class ServicesCubit extends ServerInstallationDependendCubit { - ServicesCubit(ServerInstallationCubit appConfigCubit) - : super(appConfigCubit, ServicesState.allOff()); - - Box box = Hive.box(BNames.servicesState); + ServicesCubit(ServerInstallationCubit serverInstallationCubit) + : super(serverInstallationCubit, ServicesState.allOff()); final api = ServerApi(); Future load() async { - if (appConfigCubit.state is ServerInstallationFinished) { + if (serverInstallationCubit.state is ServerInstallationFinished) { var statuses = await api.servicesPowerCheck(); emit( ServicesState( @@ -29,7 +25,6 @@ class ServicesCubit extends ServerInstallationDependendCubit { @override void clear() async { - box.clear(); emit(ServicesState.allOff()); } } diff --git a/lib/logic/cubit/users/users_cubit.dart b/lib/logic/cubit/users/users_cubit.dart index 9b749963..543b5c46 100644 --- a/lib/logic/cubit/users/users_cubit.dart +++ b/lib/logic/cubit/users/users_cubit.dart @@ -1,4 +1,3 @@ -import 'package:bloc/bloc.dart'; import 'package:hive/hive.dart'; import 'package:selfprivacy/config/hive_config.dart'; import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart'; @@ -11,23 +10,23 @@ export 'package:provider/provider.dart'; part 'users_state.dart'; class UsersCubit extends ServerInstallationDependendCubit { - UsersCubit(ServerInstallationCubit appConfigCubit) + UsersCubit(ServerInstallationCubit serverInstallationCubit) : super( - appConfigCubit, + serverInstallationCubit, UsersState( [], User(login: 'root'), User(login: 'loading...'))); Box box = Hive.box(BNames.users); - Box configBox = Hive.box(BNames.appConfig); + Box serverInstallationBox = Hive.box(BNames.serverInstallation); final api = ServerApi(); Future load() async { - if (appConfigCubit.state is ServerInstallationFinished) { + if (serverInstallationCubit.state is ServerInstallationFinished) { var loadedUsers = box.values.toList(); - final primaryUser = configBox.get(BNames.rootUser, + final primaryUser = serverInstallationBox.get(BNames.rootUser, defaultValue: User(login: 'loading...')); List rootKeys = [ - ...configBox.get(BNames.rootKeys, defaultValue: []) + ...serverInstallationBox.get(BNames.rootKeys, defaultValue: []) ]; if (loadedUsers.isNotEmpty) { emit(UsersState( @@ -48,10 +47,10 @@ class UsersCubit extends ServerInstallationDependendCubit { box.addAll(usersWithSshKeys); final rootUserWithSshKeys = (await loadSshKeys([state.rootUser])).first; - configBox.put(BNames.rootKeys, rootUserWithSshKeys.sshKeys); + serverInstallationBox.put(BNames.rootKeys, rootUserWithSshKeys.sshKeys); final primaryUserWithSshKeys = (await loadSshKeys([state.primaryUser])).first; - configBox.put(BNames.rootUser, primaryUserWithSshKeys); + serverInstallationBox.put(BNames.rootUser, primaryUserWithSshKeys); emit(UsersState( usersWithSshKeys, rootUserWithSshKeys, primaryUserWithSshKeys)); } @@ -142,10 +141,10 @@ class UsersCubit extends ServerInstallationDependendCubit { box.clear(); box.addAll(usersWithSshKeys); final rootUserWithSshKeys = (await loadSshKeys([state.rootUser])).first; - configBox.put(BNames.rootKeys, rootUserWithSshKeys.sshKeys); + serverInstallationBox.put(BNames.rootKeys, rootUserWithSshKeys.sshKeys); final primaryUserWithSshKeys = (await loadSshKeys([state.primaryUser])).first; - configBox.put(BNames.rootUser, primaryUserWithSshKeys); + serverInstallationBox.put(BNames.rootUser, primaryUserWithSshKeys); emit(UsersState( usersWithSshKeys, rootUserWithSshKeys, primaryUserWithSshKeys)); return; @@ -195,10 +194,10 @@ class UsersCubit extends ServerInstallationDependendCubit { final result = await api.addRootSshKey(publicKey); if (result.isSuccess) { // Add ssh key to the array of root keys - final rootKeys = - configBox.get(BNames.rootKeys, defaultValue: []) as List; + final rootKeys = serverInstallationBox + .get(BNames.rootKeys, defaultValue: []) as List; rootKeys.add(publicKey); - configBox.put(BNames.rootKeys, rootKeys); + serverInstallationBox.put(BNames.rootKeys, rootKeys); emit(state.copyWith( rootUser: User( login: state.rootUser.login, @@ -224,7 +223,7 @@ class UsersCubit extends ServerInstallationDependendCubit { sshKeys: primaryUserKeys, note: state.primaryUser.note, ); - configBox.put(BNames.rootUser, updatedUser); + serverInstallationBox.put(BNames.rootUser, updatedUser); emit(state.copyWith( primaryUser: updatedUser, )); @@ -258,10 +257,10 @@ class UsersCubit extends ServerInstallationDependendCubit { // If it is not primary user, update user if (user.login == 'root') { - final rootKeys = - configBox.get(BNames.rootKeys, defaultValue: []) as List; + final rootKeys = serverInstallationBox + .get(BNames.rootKeys, defaultValue: []) as List; rootKeys.remove(publicKey); - configBox.put(BNames.rootKeys, rootKeys); + serverInstallationBox.put(BNames.rootKeys, rootKeys); emit(state.copyWith( rootUser: User( login: state.rootUser.login, @@ -284,7 +283,7 @@ class UsersCubit extends ServerInstallationDependendCubit { sshKeys: primaryUserKeys, note: state.primaryUser.note, ); - configBox.put(BNames.rootUser, updatedUser); + serverInstallationBox.put(BNames.rootUser, updatedUser); emit(state.copyWith( primaryUser: updatedUser, )); diff --git a/lib/logic/get_it/api_config.dart b/lib/logic/get_it/api_config.dart index 1dc5ce7d..4991a621 100644 --- a/lib/logic/get_it/api_config.dart +++ b/lib/logic/get_it/api_config.dart @@ -6,7 +6,7 @@ import 'package:selfprivacy/logic/models/hive/server_domain.dart'; import 'package:selfprivacy/logic/models/hive/server_details.dart'; class ApiConfigModel { - Box _box = Hive.box(BNames.appConfig); + Box _box = Hive.box(BNames.serverInstallation); ServerHostingDetails? get serverDetails => _serverDetails; String? get hetznerKey => _hetznerKey; @@ -33,7 +33,7 @@ class ApiConfigModel { } Future storeBackblazeCredential(BackblazeCredential value) async { - await _box.put(BNames.backblazeKey, value); + await _box.put(BNames.backblazeCredential, value); _backblazeCredential = value; } @@ -66,7 +66,7 @@ class ApiConfigModel { _hetznerKey = _box.get(BNames.hetznerKey); _cloudFlareKey = _box.get(BNames.cloudFlareKey); - _backblazeCredential = _box.get(BNames.backblazeKey); + _backblazeCredential = _box.get(BNames.backblazeCredential); _serverDomain = _box.get(BNames.serverDomain); _serverDetails = _box.get(BNames.serverDetails); _backblazeBucket = _box.get(BNames.backblazeBucket); diff --git a/lib/logic/get_it/ssh.dart b/lib/logic/get_it/ssh.dart deleted file mode 100644 index 0e833c7e..00000000 --- a/lib/logic/get_it/ssh.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'package:hive/hive.dart'; -import 'package:pointycastle/pointycastle.dart'; -import 'package:rsa_encrypt/rsa_encrypt.dart'; -import 'package:selfprivacy/config/hive_config.dart'; -import 'package:pointycastle/api.dart' as crypto; -import 'package:ssh_key/ssh_key.dart' as ssh_key; - -class SSHModel { - Box _box = Hive.box(BNames.sshConfig); - String? savedPrivateKey; - String? savedPubKey; - - Future generateKeys() async { - var helper = RsaKeyHelper(); - crypto.AsymmetricKeyPair pair = - await helper.computeRSAKeyPair(helper.getSecureRandom()); - var privateKey = pair.privateKey as RSAPrivateKey; - var publicKey = pair.publicKey as RSAPublicKey; - - savedPrivateKey = helper.encodePrivateKeyToPemPKCS1(privateKey); - savedPubKey = publicKey.encode(ssh_key.PubKeyEncoding.openSsh); - - await _box.put(BNames.sshPrivateKey, savedPrivateKey); - await _box.put(BNames.sshPublicKey, savedPubKey); - } - - void init() async { - savedPrivateKey = _box.get(BNames.sshPrivateKey); - savedPubKey = _box.get(BNames.sshPublicKey); - } - - bool get isSSHKeyGenerated => savedPrivateKey != null && savedPubKey != null; - - Future clear() async { - savedPrivateKey = null; - savedPubKey = null; - await _box.clear(); - } -} diff --git a/lib/logic/models/json/api_token.dart b/lib/logic/models/json/api_token.dart index 25556399..60801889 100644 --- a/lib/logic/models/json/api_token.dart +++ b/lib/logic/models/json/api_token.dart @@ -1,4 +1,3 @@ -import 'package:easy_localization/easy_localization.dart'; import 'package:json_annotation/json_annotation.dart'; part 'api_token.g.dart'; diff --git a/lib/main.dart b/lib/main.dart index 3f085bfc..cdb817fc 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -83,7 +83,7 @@ class MyApp extends StatelessWidget { darkTheme: darkThemeData, themeMode: appSettings.isDarkModeOn ? ThemeMode.dark : ThemeMode.light, - home: appSettings.isOnbordingShowing + home: appSettings.isOnboardingShowing ? OnboardingPage(nextPage: InitializingPage()) : RootPage(), builder: (BuildContext context, Widget? widget) { diff --git a/lib/ui/components/brand_md/brand_md.dart b/lib/ui/components/brand_md/brand_md.dart index 230b5619..f2895cff 100644 --- a/lib/ui/components/brand_md/brand_md.dart +++ b/lib/ui/components/brand_md/brand_md.dart @@ -4,7 +4,7 @@ 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.dart'; +import 'package:url_launcher/url_launcher_string.dart'; class BrandMarkdown extends StatefulWidget { const BrandMarkdown({ @@ -60,9 +60,9 @@ class _BrandMarkdownState extends State { styleSheet: markdown, onTapLink: (String text, String? href, String title) { if (href != null) { - canLaunch(href).then((canLaunchURL) { + canLaunchUrlString(href).then((canLaunchURL) { if (canLaunchURL) { - launch(href); + launchUrlString(href); } }); } diff --git a/lib/ui/components/brand_span_button/brand_span_button.dart b/lib/ui/components/brand_span_button/brand_span_button.dart index d6cdcb02..6401a821 100644 --- a/lib/ui/components/brand_span_button/brand_span_button.dart +++ b/lib/ui/components/brand_span_button/brand_span_button.dart @@ -26,8 +26,8 @@ class BrandSpanButton extends TextSpan { ); static _launchURL(String link) async { - if (await canLaunch(link)) { - await launch(link); + if (await canLaunchUrl(Uri.parse(link))) { + await launchUrl(Uri.parse(link)); } else { throw 'Could not launch $link'; } diff --git a/lib/ui/pages/services/services.dart b/lib/ui/pages/services/services.dart index 7498e370..d2619420 100644 --- a/lib/ui/pages/services/services.dart +++ b/lib/ui/pages/services/services.dart @@ -19,7 +19,7 @@ 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/utils/ui_helpers.dart'; -import 'package:url_launcher/url_launcher.dart'; +import 'package:url_launcher/url_launcher_string.dart'; import '../rootRoute.dart'; @@ -39,13 +39,12 @@ class ServicesPage extends StatefulWidget { } void _launchURL(url) async { - var _possible = await canLaunch(url); + var _possible = await canLaunchUrlString(url); if (_possible) { try { - await launch( + await launchUrlString( url, - enableJavaScript: true, ); } catch (e) { print(e); @@ -151,11 +150,9 @@ class _Card extends StatelessWidget { builder: (context) { late bool isActive; if (hasSwitchJob) { - isActive = ((jobState as JobsStateWithJobs) - .jobList - .firstWhere((el) => - el is ServiceToggleJob && - el.type == serviceType) as ServiceToggleJob) + isActive = ((jobState).jobList.firstWhere((el) => + el is ServiceToggleJob && + el.type == serviceType) as ServiceToggleJob) .needToTurnOn; } else { isActive = serviceState.isEnableByType(serviceType); diff --git a/lib/ui/pages/setup/recovering/recovery_domain.dart b/lib/ui/pages/setup/recovering/recovery_domain.dart index 5ea30da8..f38a554f 100644 --- a/lib/ui/pages/setup/recovering/recovery_domain.dart +++ b/lib/ui/pages/setup/recovering/recovery_domain.dart @@ -10,11 +10,11 @@ import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.da class RecoveryDomain extends StatelessWidget { @override Widget build(BuildContext context) { - var appConfig = context.watch(); + var serverInstallation = context.watch(); return BlocProvider( - create: (context) => - RecoveryDomainFormCubit(appConfig, FieldCubitFactory(context)), + create: (context) => RecoveryDomainFormCubit( + serverInstallation, FieldCubitFactory(context)), child: Builder( builder: (context) { var formCubitState = context.watch().state; diff --git a/pubspec.lock b/pubspec.lock index 7c047542..b7e22bfd 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -715,7 +715,7 @@ packages: source: hosted version: "2.1.2" pointycastle: - dependency: "direct main" + dependency: transitive description: name: pointycastle url: "https://pub.dartlang.org" @@ -770,13 +770,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.1.0" - rsa_encrypt: - dependency: "direct main" - description: - name: rsa_encrypt - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.0" share_plus: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 3f66997c..1a360bbe 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -34,10 +34,8 @@ dependencies: modal_bottom_sheet: ^2.0.1 nanoid: ^1.0.0 package_info: ^2.0.2 - pointycastle: ^3.5.1 pretty_dio_logger: ^1.2.0-beta-1 provider: ^6.0.2 - rsa_encrypt: ^2.0.0 share_plus: ^4.0.4 ssh_key: ^0.7.1 system_theme: ^2.0.0 From 20f6e8156c8b91499b9f4f020baf71e20769ccfd Mon Sep 17 00:00:00 2001 From: NaiJi Date: Wed, 18 May 2022 02:18:26 +0300 Subject: [PATCH 18/52] Add recovery token pages --- .../recovery_device_form_cubit.dart | 25 +++++++++ lib/ui/pages/setup/initializing.dart | 5 +- .../recovering/recovery_method_device_1.dart | 6 +-- .../recovering/recovery_method_device_2.dart | 52 ++++++++++++++----- .../recovering/recovery_method_select.dart | 4 +- .../recovering/recovery_method_token.dart | 49 +++++++++++++++++ 6 files changed, 121 insertions(+), 20 deletions(-) create mode 100644 lib/logic/cubit/forms/setup/recovering/recovery_device_form_cubit.dart create mode 100644 lib/ui/pages/setup/recovering/recovery_method_token.dart diff --git a/lib/logic/cubit/forms/setup/recovering/recovery_device_form_cubit.dart b/lib/logic/cubit/forms/setup/recovering/recovery_device_form_cubit.dart new file mode 100644 index 00000000..4486bf14 --- /dev/null +++ b/lib/logic/cubit/forms/setup/recovering/recovery_device_form_cubit.dart @@ -0,0 +1,25 @@ +import 'dart:async'; + +import 'package:cubit_form/cubit_form.dart'; +import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; +import 'package:selfprivacy/logic/cubit/forms/factories/field_cubit_factory.dart'; + +class RecoveryDeviceFormCubit extends FormCubit { + RecoveryDeviceFormCubit( + this.initializingCubit, final FieldCubitFactory fieldFactory) { + tokenField = fieldFactory.createServerDomainField(); + + super.addFields([tokenField]); + } + + @override + FutureOr onSubmit() async { + // initializingCubit.setDomain(ServerDomain( + // domainName: serverDomainField.state.value, + // provider: DnsProvider.Unknown, + // zoneId: "")); + } + + final ServerInstallationCubit initializingCubit; + late final FieldCubit tokenField; +} diff --git a/lib/ui/pages/setup/initializing.dart b/lib/ui/pages/setup/initializing.dart index 8e6fd1ec..e1f6203e 100644 --- a/lib/ui/pages/setup/initializing.dart +++ b/lib/ui/pages/setup/initializing.dart @@ -18,6 +18,7 @@ import 'package:selfprivacy/ui/components/brand_timer/brand_timer.dart'; import 'package:selfprivacy/ui/components/progress_bar/progress_bar.dart'; import 'package:selfprivacy/ui/pages/rootRoute.dart'; import 'package:selfprivacy/ui/pages/setup/recovering/recovery_domain.dart'; +import 'package:selfprivacy/ui/pages/setup/recovering/recovery_method_select.dart'; import 'package:selfprivacy/utils/route_transitions/basic.dart'; class InitializingPage extends StatelessWidget { @@ -104,8 +105,8 @@ class InitializingPage extends StatelessWidget { child: BrandButton.text( title: 'basis.connect_to_existing'.tr(), onPressed: () { - Navigator.of(context) - .push(materialRoute(RecoveryDomain())); + Navigator.of(context).push( + materialRoute(RecoveryMethodSelect())); }, ), ) diff --git a/lib/ui/pages/setup/recovering/recovery_method_device_1.dart b/lib/ui/pages/setup/recovering/recovery_method_device_1.dart index 73b32354..43b071b8 100644 --- a/lib/ui/pages/setup/recovering/recovery_method_device_1.dart +++ b/lib/ui/pages/setup/recovering/recovery_method_device_1.dart @@ -2,7 +2,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:selfprivacy/ui/components/brand_button/FilledButton.dart'; import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; -import 'package:selfprivacy/ui/pages/rootRoute.dart'; +import 'package:selfprivacy/ui/pages/setup/recovering/recovery_method_device_2.dart'; import 'package:selfprivacy/utils/route_transitions/basic.dart'; class RecoveryMethodDevice1 extends StatelessWidget { @@ -16,8 +16,8 @@ class RecoveryMethodDevice1 extends StatelessWidget { children: [ FilledButton( title: "recovering.method_device_button".tr(), - onPressed: () => - Navigator.of(context).push(materialRoute(RootPage())), + onPressed: () => Navigator.of(context) + .push(materialRoute(RecoveryMethodDevice2())), ) ], ); diff --git a/lib/ui/pages/setup/recovering/recovery_method_device_2.dart b/lib/ui/pages/setup/recovering/recovery_method_device_2.dart index c5eac006..e9323803 100644 --- a/lib/ui/pages/setup/recovering/recovery_method_device_2.dart +++ b/lib/ui/pages/setup/recovering/recovery_method_device_2.dart @@ -1,25 +1,49 @@ +import 'package:cubit_form/cubit_form.dart'; import 'package:easy_localization/easy_localization.dart'; 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/FilledButton.dart'; import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; -import 'package:selfprivacy/utils/route_transitions/basic.dart'; -import 'package:selfprivacy/ui/pages/rootRoute.dart'; class RecoveryMethodDevice2 extends StatelessWidget { @override Widget build(BuildContext context) { - return BrandHeroScreen( - heroTitle: "recovering.recovery_main_header".tr(), - heroSubtitle: "recovering.method_device_input_description".tr(), - hasBackButton: true, - hasFlashButton: false, - children: [ - FilledButton( - title: "recovering.method_device_button".tr(), - onPressed: () => - Navigator.of(context).push(materialRoute(RootPage())), - ) - ], + var appConfig = context.watch(); + + return BlocProvider( + create: (context) => + RecoveryDeviceFormCubit(appConfig, FieldCubitFactory(context)), + child: Builder( + builder: (context) { + var formCubitState = context.watch().state; + + return BrandHeroScreen( + heroTitle: "recovering.recovery_main_header".tr(), + heroSubtitle: "recovering.method_device_input_description".tr(), + hasBackButton: true, + hasFlashButton: false, + children: [ + CubitFormTextField( + formFieldCubit: + context.read().tokenField, + decoration: InputDecoration( + border: OutlineInputBorder(), + labelText: "recovering.method_device_input_placeholder".tr(), + ), + ), + SizedBox(height: 16), + FilledButton( + title: "more.continue".tr(), + onPressed: formCubitState.isSubmitting + ? null + : () => context.read().trySubmit(), + ) + ], + ); + }, + ), ); } } diff --git a/lib/ui/pages/setup/recovering/recovery_method_select.dart b/lib/ui/pages/setup/recovering/recovery_method_select.dart index 136ec36c..f92545eb 100644 --- a/lib/ui/pages/setup/recovering/recovery_method_select.dart +++ b/lib/ui/pages/setup/recovering/recovery_method_select.dart @@ -5,6 +5,7 @@ import 'package:selfprivacy/ui/components/brand_cards/brand_cards.dart'; import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; import 'package:selfprivacy/ui/pages/setup/recovering/recovery_fallback_select.dart'; import 'package:selfprivacy/ui/pages/setup/recovering/recovery_method_device_1.dart'; +import 'package:selfprivacy/ui/pages/setup/recovering/recovery_method_token.dart'; import 'package:selfprivacy/utils/route_transitions/basic.dart'; import 'package:selfprivacy/ui/pages/rootRoute.dart'; @@ -36,7 +37,8 @@ class RecoveryMethodSelect extends StatelessWidget { style: Theme.of(context).textTheme.titleMedium, ), leading: Icon(Icons.password_outlined), - onTap: () => Navigator.of(context).push(materialRoute(RootPage())), + onTap: () => Navigator.of(context) + .push(materialRoute(RecoveryMethodToken())), ), ), SizedBox(height: 16), diff --git a/lib/ui/pages/setup/recovering/recovery_method_token.dart b/lib/ui/pages/setup/recovering/recovery_method_token.dart new file mode 100644 index 00000000..7b0c2564 --- /dev/null +++ b/lib/ui/pages/setup/recovering/recovery_method_token.dart @@ -0,0 +1,49 @@ +import 'package:cubit_form/cubit_form.dart'; +import 'package:easy_localization/easy_localization.dart'; +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/FilledButton.dart'; +import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; + +class RecoveryMethodToken extends StatelessWidget { + @override + Widget build(BuildContext context) { + var appConfig = context.watch(); + + return BlocProvider( + create: (context) => + RecoveryDeviceFormCubit(appConfig, FieldCubitFactory(context)), + child: Builder( + builder: (context) { + var formCubitState = context.watch().state; + + return BrandHeroScreen( + heroTitle: "recovering.recovery_main_header".tr(), + heroSubtitle: "recovering.method_recovery_input_description".tr(), + hasBackButton: true, + hasFlashButton: false, + children: [ + CubitFormTextField( + formFieldCubit: + context.read().tokenField, + decoration: InputDecoration( + border: OutlineInputBorder(), + labelText: "recovering.method_device_input_placeholder".tr(), + ), + ), + SizedBox(height: 16), + FilledButton( + title: "more.continue".tr(), + onPressed: formCubitState.isSubmitting + ? null + : () => context.read().trySubmit(), + ) + ], + ); + }, + ), + ); + } +} From dd77b99ac8c529bc4853b2ffcb658aabd1eb78c6 Mon Sep 17 00:00:00 2001 From: inexcode Date: Wed, 18 May 2022 11:27:36 +0300 Subject: [PATCH 19/52] Rename Bnames boxes names to include the Box --- lib/config/hive_config.dart | 40 +++++++++---------- .../app_settings/app_settings_cubit.dart | 1 + .../server_installation_repository.dart | 6 +-- lib/logic/cubit/users/users_cubit.dart | 4 +- lib/logic/get_it/api_config.dart | 4 +- 5 files changed, 28 insertions(+), 27 deletions(-) diff --git a/lib/config/hive_config.dart b/lib/config/hive_config.dart index 21f329b2..094c83c8 100644 --- a/lib/config/hive_config.dart +++ b/lib/config/hive_config.dart @@ -5,8 +5,8 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:selfprivacy/logic/models/hive/backblaze_bucket.dart'; import 'package:selfprivacy/logic/models/hive/backblaze_credential.dart'; -import 'package:selfprivacy/logic/models/hive/server_domain.dart'; import 'package:selfprivacy/logic/models/hive/server_details.dart'; +import 'package:selfprivacy/logic/models/hive/server_domain.dart'; import 'package:selfprivacy/logic/models/hive/user.dart'; class HiveConfig { @@ -25,16 +25,16 @@ class HiveConfig { await getEncryptedKey(BNames.serverInstallationEncryptionKey)); await Hive.openBox(BNames.usersDeprecated); - await Hive.openBox(BNames.users, encryptionCipher: cipher); + await Hive.openBox(BNames.usersBox, encryptionCipher: cipher); Box deprecatedUsers = Hive.box(BNames.usersDeprecated); if (deprecatedUsers.isNotEmpty) { - Box users = Hive.box(BNames.users); + Box users = Hive.box(BNames.usersBox); users.addAll(deprecatedUsers.values.toList()); deprecatedUsers.clear(); } - await Hive.openBox(BNames.serverInstallation, encryptionCipher: cipher); + await Hive.openBox(BNames.serverInstallationBox, encryptionCipher: cipher); } static Future getEncryptedKey(String encKey) async { @@ -61,54 +61,54 @@ class BNames { /// A boolean field of [appSettingsBox] box. static String isOnboardingShowing = 'isOnboardingShowing'; - /// Encryption key to decrypt [serverInstallation] and [users] box. + /// Encryption key to decrypt [serverInstallationBox] and [usersBox] box. static String serverInstallationEncryptionKey = 'key'; /// Server installation box. Contains server details and provider tokens. - static String serverInstallation = 'appConfig'; + static String serverInstallationBox = 'appConfig'; - /// A List field of [serverInstallation] box. + /// A List field of [serverInstallationBox] box. static String rootKeys = 'rootKeys'; - /// A boolean field of [serverInstallation] box. + /// A boolean field of [serverInstallationBox] box. static String hasFinalChecked = 'hasFinalChecked'; - /// A boolean field of [serverInstallation] box. + /// A boolean field of [serverInstallationBox] box. static String isServerStarted = 'isServerStarted'; - /// A [ServerDomain] field of [serverInstallation] box. + /// A [ServerDomain] field of [serverInstallationBox] box. static String serverDomain = 'cloudFlareDomain'; - /// A String field of [serverInstallation] box. + /// A String field of [serverInstallationBox] box. static String hetznerKey = 'hetznerKey'; - /// A String field of [serverInstallation] box. + /// A String field of [serverInstallationBox] box. static String cloudFlareKey = 'cloudFlareKey'; - /// A [User] field of [serverInstallation] box. + /// A [User] field of [serverInstallationBox] box. static String rootUser = 'rootUser'; - /// A [ServerHostingDetails] field of [serverInstallation] box. + /// A [ServerHostingDetails] field of [serverInstallationBox] box. static String serverDetails = 'hetznerServer'; - /// A [BackblazeCredential] field of [serverInstallation] box. + /// A [BackblazeCredential] field of [serverInstallationBox] box. static String backblazeCredential = 'backblazeKey'; - /// A [BackblazeBucket] field of [serverInstallation] box. + /// A [BackblazeBucket] field of [serverInstallationBox] box. static String backblazeBucket = 'backblazeBucket'; - /// A boolean field of [serverInstallation] box. + /// A boolean field of [serverInstallationBox] box. static String isLoading = 'isLoading'; - /// A boolean field of [serverInstallation] box. + /// A boolean field of [serverInstallationBox] box. static String isServerResetedFirstTime = 'isServerResetedFirstTime'; - /// A boolean field of [serverInstallation] box. + /// A boolean field of [serverInstallationBox] box. static String isServerResetedSecondTime = 'isServerResetedSecondTime'; /// Deprecated users box as it is unencrypted static String usersDeprecated = 'users'; /// Box with users - static String users = 'usersEncrypted'; + static String usersBox = 'usersEncrypted'; } diff --git a/lib/logic/cubit/app_settings/app_settings_cubit.dart b/lib/logic/cubit/app_settings/app_settings_cubit.dart index fde5b083..a4a2d0f7 100644 --- a/lib/logic/cubit/app_settings/app_settings_cubit.dart +++ b/lib/logic/cubit/app_settings/app_settings_cubit.dart @@ -2,6 +2,7 @@ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:hive/hive.dart'; import 'package:selfprivacy/config/hive_config.dart'; + export 'package:provider/provider.dart'; part 'app_settings_state.dart'; diff --git a/lib/logic/cubit/server_installation/server_installation_repository.dart b/lib/logic/cubit/server_installation/server_installation_repository.dart index 6bc2613b..f2ef84f1 100644 --- a/lib/logic/cubit/server_installation/server_installation_repository.dart +++ b/lib/logic/cubit/server_installation/server_installation_repository.dart @@ -8,17 +8,17 @@ import 'package:selfprivacy/logic/api_maps/cloudflare.dart'; import 'package:selfprivacy/logic/api_maps/hetzner.dart'; import 'package:selfprivacy/logic/api_maps/server.dart'; import 'package:selfprivacy/logic/models/hive/backblaze_credential.dart'; -import 'package:selfprivacy/logic/models/hive/server_domain.dart'; -import 'package:selfprivacy/logic/models/message.dart'; import 'package:selfprivacy/logic/models/hive/server_details.dart'; +import 'package:selfprivacy/logic/models/hive/server_domain.dart'; import 'package:selfprivacy/logic/models/hive/user.dart'; +import 'package:selfprivacy/logic/models/message.dart'; import 'package:selfprivacy/ui/components/action_button/action_button.dart'; import 'package:selfprivacy/ui/components/brand_alert/brand_alert.dart'; import '../server_installation/server_installation_cubit.dart'; class ServerInstallationRepository { - Box box = Hive.box(BNames.serverInstallation); + Box box = Hive.box(BNames.serverInstallationBox); Future load() async { final hetznerToken = getIt().hetznerKey; diff --git a/lib/logic/cubit/users/users_cubit.dart b/lib/logic/cubit/users/users_cubit.dart index 543b5c46..b65dcef9 100644 --- a/lib/logic/cubit/users/users_cubit.dart +++ b/lib/logic/cubit/users/users_cubit.dart @@ -15,8 +15,8 @@ class UsersCubit extends ServerInstallationDependendCubit { serverInstallationCubit, UsersState( [], User(login: 'root'), User(login: 'loading...'))); - Box box = Hive.box(BNames.users); - Box serverInstallationBox = Hive.box(BNames.serverInstallation); + Box box = Hive.box(BNames.usersBox); + Box serverInstallationBox = Hive.box(BNames.serverInstallationBox); final api = ServerApi(); diff --git a/lib/logic/get_it/api_config.dart b/lib/logic/get_it/api_config.dart index 4991a621..1d6f2610 100644 --- a/lib/logic/get_it/api_config.dart +++ b/lib/logic/get_it/api_config.dart @@ -2,11 +2,11 @@ import 'package:hive/hive.dart'; import 'package:selfprivacy/config/hive_config.dart'; import 'package:selfprivacy/logic/models/hive/backblaze_bucket.dart'; import 'package:selfprivacy/logic/models/hive/backblaze_credential.dart'; -import 'package:selfprivacy/logic/models/hive/server_domain.dart'; import 'package:selfprivacy/logic/models/hive/server_details.dart'; +import 'package:selfprivacy/logic/models/hive/server_domain.dart'; class ApiConfigModel { - Box _box = Hive.box(BNames.serverInstallation); + Box _box = Hive.box(BNames.serverInstallationBox); ServerHostingDetails? get serverDetails => _serverDetails; String? get hetznerKey => _hetznerKey; From 2d96b4505e8c454a5ae3a844ffdc41387a5138d2 Mon Sep 17 00:00:00 2001 From: NaiJi Date: Wed, 18 May 2022 12:07:14 +0300 Subject: [PATCH 20/52] Disable unavailable functionality when server is not created - Remove create and upgrade server jobs when server is not there - Disable root SSH panel page when server is not there --- assets/translations/en.json | 2 +- lib/logic/cubit/backups/backups_cubit.dart | 1 + .../cubit/dns_records/dns_records_cubit.dart | 1 + lib/logic/cubit/services/services_cubit.dart | 1 + lib/logic/cubit/users/users_cubit.dart | 1 + .../components/jobs_content/jobs_content.dart | 62 +++++++++++-------- lib/ui/pages/more/more.dart | 11 +++- 7 files changed, 49 insertions(+), 30 deletions(-) diff --git a/assets/translations/en.json b/assets/translations/en.json index 78ba00c5..3b7bf3af 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -36,7 +36,7 @@ "about_project": "About us", "about_app": "About application", "onboarding": "Onboarding", - "create_ssh_key": "Create ssh key", + "create_ssh_key": "Create SSH key", "generate_key": "Generate key", "generate_key_text": "You can generate ssh key", "console": "Console", diff --git a/lib/logic/cubit/backups/backups_cubit.dart b/lib/logic/cubit/backups/backups_cubit.dart index 67570c4e..47ad4796 100644 --- a/lib/logic/cubit/backups/backups_cubit.dart +++ b/lib/logic/cubit/backups/backups_cubit.dart @@ -5,6 +5,7 @@ import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/logic/api_maps/backblaze.dart'; import 'package:selfprivacy/logic/api_maps/server.dart'; import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart'; +import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; import 'package:selfprivacy/logic/models/hive/backblaze_bucket.dart'; import 'package:selfprivacy/logic/models/json/backup.dart'; diff --git a/lib/logic/cubit/dns_records/dns_records_cubit.dart b/lib/logic/cubit/dns_records/dns_records_cubit.dart index 6b8bf715..88ad9ba0 100644 --- a/lib/logic/cubit/dns_records/dns_records_cubit.dart +++ b/lib/logic/cubit/dns_records/dns_records_cubit.dart @@ -1,5 +1,6 @@ import 'package:cubit_form/cubit_form.dart'; import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart'; +import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; import 'package:selfprivacy/logic/models/hive/server_domain.dart'; import 'package:selfprivacy/logic/models/json/dns_records.dart'; diff --git a/lib/logic/cubit/services/services_cubit.dart b/lib/logic/cubit/services/services_cubit.dart index e503389d..5caa5a42 100644 --- a/lib/logic/cubit/services/services_cubit.dart +++ b/lib/logic/cubit/services/services_cubit.dart @@ -1,6 +1,7 @@ import 'package:selfprivacy/logic/api_maps/server.dart'; import 'package:selfprivacy/logic/common_enum/common_enum.dart'; import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart'; +import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; part 'services_state.dart'; diff --git a/lib/logic/cubit/users/users_cubit.dart b/lib/logic/cubit/users/users_cubit.dart index 543b5c46..41b8747a 100644 --- a/lib/logic/cubit/users/users_cubit.dart +++ b/lib/logic/cubit/users/users_cubit.dart @@ -1,6 +1,7 @@ import 'package:hive/hive.dart'; import 'package:selfprivacy/config/hive_config.dart'; import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart'; +import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; import 'package:selfprivacy/logic/models/hive/user.dart'; import '../../api_maps/server.dart'; diff --git a/lib/ui/components/jobs_content/jobs_content.dart b/lib/ui/components/jobs_content/jobs_content.dart index 92298a06..1859cd70 100644 --- a/lib/ui/components/jobs_content/jobs_content.dart +++ b/lib/ui/components/jobs_content/jobs_content.dart @@ -5,6 +5,7 @@ import 'package:selfprivacy/config/brand_colors.dart'; import 'package:selfprivacy/config/brand_theme.dart'; import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/logic/cubit/jobs/jobs_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_button/brand_button.dart'; @@ -19,38 +20,45 @@ class JobsContent extends StatelessWidget { Widget build(BuildContext context) { return BlocBuilder( builder: (context, state) { - late final List widgets; + late List widgets; + var installationState = context.read().state; if (state is JobsStateEmpty) { widgets = [ SizedBox(height: 80), Center(child: BrandText.body1('jobs.empty'.tr())), - SizedBox(height: 80), - BrandButton.rised( - onPressed: () => context.read().upgradeServer(), - text: 'jobs.upgradeServer'.tr(), - ), - SizedBox(height: 10), - BrandButton.text( - onPressed: () { - var nav = getIt(); - nav.showPopUpDialog(BrandAlert( - title: 'jobs.rebootServer'.tr(), - contentText: 'modals.3'.tr(), - actions: [ - ActionButton( - text: 'basis.cancel'.tr(), - ), - ActionButton( - onPressed: () => - {context.read().rebootServer()}, - text: 'modals.9'.tr(), - ) - ], - )); - }, - title: 'jobs.rebootServer'.tr(), - ), ]; + + if (installationState is ServerInstallationFinished) { + widgets = [ + ...widgets, + SizedBox(height: 80), + BrandButton.rised( + onPressed: () => context.read().upgradeServer(), + text: 'jobs.upgradeServer'.tr(), + ), + SizedBox(height: 10), + BrandButton.text( + onPressed: () { + var nav = getIt(); + nav.showPopUpDialog(BrandAlert( + title: 'jobs.rebootServer'.tr(), + contentText: 'modals.3'.tr(), + actions: [ + ActionButton( + text: 'basis.cancel'.tr(), + ), + ActionButton( + onPressed: () => + {context.read().rebootServer()}, + text: 'modals.9'.tr(), + ) + ], + )); + }, + title: 'jobs.rebootServer'.tr(), + ), + ]; + } } else if (state is JobsStateLoading) { widgets = [ SizedBox(height: 80), diff --git a/lib/ui/pages/more/more.dart b/lib/ui/pages/more/more.dart index c588c007..d91ee769 100644 --- a/lib/ui/pages/more/more.dart +++ b/lib/ui/pages/more/more.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:ionicons/ionicons.dart'; import 'package:selfprivacy/config/brand_colors.dart'; import 'package:selfprivacy/config/brand_theme.dart'; +import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; import 'package:selfprivacy/ui/components/brand_divider/brand_divider.dart'; import 'package:selfprivacy/ui/components/brand_header/brand_header.dart'; import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart'; @@ -70,6 +71,8 @@ class MorePage extends StatelessWidget { goTo: Console(), ), _NavItem( + isEnabled: context.read().state + is ServerInstallationFinished, title: 'more.create_ssh_key'.tr(), iconData: Ionicons.key_outline, goTo: SshKeysPage( @@ -87,6 +90,7 @@ class MorePage extends StatelessWidget { class _NavItem extends StatelessWidget { const _NavItem({ Key? key, + this.isEnabled = true, required this.iconData, required this.goTo, required this.title, @@ -95,15 +99,18 @@ class _NavItem extends StatelessWidget { final IconData iconData; final Widget goTo; final String title; + final bool isEnabled; @override Widget build(BuildContext context) { return GestureDetector( - onTap: () => Navigator.of(context).push(materialRoute(goTo)), + onTap: isEnabled + ? () => Navigator.of(context).push(materialRoute(goTo)) + : null, child: _MoreMenuItem( iconData: iconData, title: title, - isActive: true, + isActive: isEnabled, ), ); } From d2553b0d0827e2c58f412ab6f82e16c14d23f73a Mon Sep 17 00:00:00 2001 From: inexcode Date: Wed, 18 May 2022 13:39:11 +0300 Subject: [PATCH 21/52] Add auth functions to server_installation_repository.dart --- lib/logic/api_maps/server.dart | 19 +- .../server_installation_repository.dart | 175 +++++++++++++++++- .../server_installation_state.dart | 25 ++- lib/logic/models/hive/server_details.g.dart | 2 +- lib/ui/pages/providers/providers.dart | 6 +- lib/ui/pages/server_details/chart.dart | 2 +- lib/ui/pages/server_details/header.dart | 2 +- ...etails.dart => server_details_screen.dart} | 19 +- .../pages/server_details/server_settings.dart | 2 +- lib/ui/pages/server_details/text_details.dart | 2 +- .../server_details/time_zone/time_zone.dart | 2 +- pubspec.lock | 44 ++++- pubspec.yaml | 2 + 13 files changed, 269 insertions(+), 33 deletions(-) rename lib/ui/pages/server_details/{server_details.dart => server_details_screen.dart} (94%) diff --git a/lib/logic/api_maps/server.dart b/lib/logic/api_maps/server.dart index 0cafbf6b..21bceee9 100644 --- a/lib/logic/api_maps/server.dart +++ b/lib/logic/api_maps/server.dart @@ -5,14 +5,14 @@ import 'dart:io'; import 'package:dio/dio.dart'; import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/logic/common_enum/common_enum.dart'; +import 'package:selfprivacy/logic/models/hive/backblaze_bucket.dart'; +import 'package:selfprivacy/logic/models/hive/user.dart'; import 'package:selfprivacy/logic/models/json/api_token.dart'; import 'package:selfprivacy/logic/models/json/auto_upgrade_settings.dart'; -import 'package:selfprivacy/logic/models/hive/backblaze_bucket.dart'; import 'package:selfprivacy/logic/models/json/backup.dart'; -import 'package:selfprivacy/logic/models/json/recovery_token_status.dart'; import 'package:selfprivacy/logic/models/json/device_token.dart'; +import 'package:selfprivacy/logic/models/json/recovery_token_status.dart'; import 'package:selfprivacy/logic/models/timezone_settings.dart'; -import 'package:selfprivacy/logic/models/hive/user.dart'; import 'api_map.dart'; @@ -34,9 +34,13 @@ class ServerApi extends ApiMap { bool hasLogger; bool isWithToken; String? overrideDomain; + String? customToken; ServerApi( - {this.hasLogger = false, this.isWithToken = true, this.overrideDomain}); + {this.hasLogger = false, + this.isWithToken = true, + this.overrideDomain, + this.customToken}); BaseOptions get options { var options = BaseOptions(); @@ -52,7 +56,12 @@ class ServerApi extends ApiMap { } if (overrideDomain != null) { - options = BaseOptions(baseUrl: 'https://api.$overrideDomain'); + options = BaseOptions( + baseUrl: 'https://api.$overrideDomain', + headers: customToken != null + ? {'Authorization': 'Bearer $customToken'} + : null, + ); } return options; diff --git a/lib/logic/cubit/server_installation/server_installation_repository.dart b/lib/logic/cubit/server_installation/server_installation_repository.dart index f2ef84f1..6af170f6 100644 --- a/lib/logic/cubit/server_installation/server_installation_repository.dart +++ b/lib/logic/cubit/server_installation/server_installation_repository.dart @@ -1,7 +1,12 @@ +import 'dart:io'; + import 'package:basic_utils/basic_utils.dart'; +import 'package:device_info_plus/device_info_plus.dart'; import 'package:dio/dio.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; +import 'package:pub_semver/pub_semver.dart'; import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/config/hive_config.dart'; import 'package:selfprivacy/logic/api_maps/cloudflare.dart'; @@ -11,12 +16,25 @@ import 'package:selfprivacy/logic/models/hive/backblaze_credential.dart'; import 'package:selfprivacy/logic/models/hive/server_details.dart'; import 'package:selfprivacy/logic/models/hive/server_domain.dart'; import 'package:selfprivacy/logic/models/hive/user.dart'; +import 'package:selfprivacy/logic/models/json/device_token.dart'; import 'package:selfprivacy/logic/models/message.dart'; import 'package:selfprivacy/ui/components/action_button/action_button.dart'; import 'package:selfprivacy/ui/components/brand_alert/brand_alert.dart'; import '../server_installation/server_installation_cubit.dart'; +class IpNotFoundException implements Exception { + final String message; + + IpNotFoundException(this.message); +} + +class ServerAuthorizationException implements Exception { + final String message; + + ServerAuthorizationException(this.message); +} + class ServerInstallationRepository { Box box = Hive.box(BNames.serverInstallationBox); @@ -43,7 +61,7 @@ class ServerInstallationRepository { ); } - if (getIt().serverDomain?.provider == DnsProvider.Unknown) { + if (serverDomain != null && serverDomain.provider == DnsProvider.Unknown) { return ServerInstallationRecovery( hetznerKey: hetznerToken, cloudFlareKey: cloudflareToken, @@ -52,7 +70,8 @@ class ServerInstallationRepository { serverDetails: serverDetails, rootUser: box.get(BNames.rootUser), currentStep: _getCurrentRecoveryStep( - hetznerToken, cloudflareToken, serverDomain!, serverDetails), + hetznerToken, cloudflareToken, serverDomain, serverDetails), + recoveryCapabilities: await getRecoveryCapabilities(serverDomain), ); } @@ -296,6 +315,158 @@ class ServerInstallationRepository { return await hetznerApi.powerOn(); } + Future getRecoveryCapabilities( + ServerDomain serverDomain, + ) async { + var serverApi = ServerApi( + isWithToken: false, + overrideDomain: serverDomain.domainName, + ); + final serverApiVersion = await serverApi.getApiVersion(); + if (serverApiVersion == null) { + return ServerRecoveryCapabilities.none; + } + try { + final parsedVersion = Version.parse(serverApiVersion); + if (parsedVersion.major == 1 && parsedVersion.minor < 2) { + return ServerRecoveryCapabilities.legacy; + } + return ServerRecoveryCapabilities.loginTokens; + } on FormatException { + return ServerRecoveryCapabilities.none; + } + } + + Future getServerIpFromDomain(ServerDomain serverDomain) async { + final lookup = await DnsUtils.lookupRecord( + serverDomain.domainName, RRecordType.A, + provider: DnsApiProvider.CLOUDFLARE); + if (lookup == null || lookup.isEmpty) { + throw IpNotFoundException('No IP found for domain $serverDomain'); + } + return lookup[0].data; + } + + Future getDeviceName() async { + final DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); + if (kIsWeb) { + return await deviceInfo.webBrowserInfo + .then((value) => '${value.browserName} ${value.platform}'); + } else { + if (Platform.isAndroid) { + return await deviceInfo.androidInfo + .then((value) => '${value.model} ${value.version.release}'); + } else if (Platform.isIOS) { + return await deviceInfo.iosInfo.then((value) => + '${value.utsname.machine} ${value.systemName} ${value.systemVersion}'); + } else if (Platform.isLinux) { + return await deviceInfo.linuxInfo.then((value) => value.prettyName); + } else if (Platform.isMacOS) { + return await deviceInfo.macOsInfo + .then((value) => '${value.hostName} ${value.computerName}'); + } else if (Platform.isWindows) { + return await deviceInfo.windowsInfo.then((value) => value.computerName); + } + } + return 'Unidentified'; + } + + Future authorizeByLoginToken( + ServerDomain serverDomain, + String loginToken, + ) async { + var serverApi = ServerApi( + isWithToken: false, + overrideDomain: serverDomain.domainName, + ); + final serverIp = await getServerIpFromDomain(serverDomain); + final apiResponse = await serverApi.authorizeDevice( + DeviceToken(device: await getDeviceName(), token: loginToken)); + + if (apiResponse.isSuccess) { + return ServerHostingDetails( + apiToken: apiResponse.data, + volume: ServerVolume( + id: 0, + name: '', + ), + provider: ServerProvider.Unknown, + id: 0, + ip4: serverIp, + startTime: null, + createTime: null, + ); + } + + throw ServerAuthorizationException( + apiResponse.errorMessage ?? apiResponse.data, + ); + } + + Future authorizeByRecoveryToken( + ServerDomain serverDomain, + String recoveryToken, + ) async { + var serverApi = ServerApi( + isWithToken: false, + overrideDomain: serverDomain.domainName, + ); + final apiResponse = await serverApi.useRecoveryToken( + DeviceToken(device: await getDeviceName(), token: recoveryToken)); + + if (apiResponse.isSuccess) { + return ServerHostingDetails( + apiToken: apiResponse.data, + volume: ServerVolume( + id: 0, + name: '', + ), + provider: ServerProvider.Unknown, + id: 0, + ip4: '', + startTime: null, + createTime: null, + ); + } + + throw ServerAuthorizationException( + apiResponse.errorMessage ?? apiResponse.data, + ); + } + + Future authorizeByApiToken( + ServerDomain serverDomain, + String apiToken, + ) async { + var serverApi = ServerApi( + isWithToken: false, + overrideDomain: serverDomain.domainName, + customToken: apiToken, + ); + final deviceAuthKey = await serverApi.createDeviceToken(); + final apiResponse = await serverApi.authorizeDevice( + DeviceToken(device: await getDeviceName(), token: deviceAuthKey.data)); + + if (apiResponse.isSuccess) { + return ServerHostingDetails( + apiToken: apiResponse.data, + volume: ServerVolume( + id: 0, + name: '', + ), + provider: ServerProvider.Unknown, + id: 0, + ip4: '', + startTime: null, + createTime: null, + ); + } + + throw ServerAuthorizationException( + apiResponse.errorMessage ?? apiResponse.data, + ); + } + Future saveServerDetails(ServerHostingDetails serverDetails) async { await getIt().storeServerDetails(serverDetails); } diff --git a/lib/logic/cubit/server_installation/server_installation_state.dart b/lib/logic/cubit/server_installation/server_installation_state.dart index e0fbd1bf..b75b19d6 100644 --- a/lib/logic/cubit/server_installation/server_installation_state.dart +++ b/lib/logic/cubit/server_installation/server_installation_state.dart @@ -265,8 +265,15 @@ enum RecoveryStep { BackblazeToken, } +enum ServerRecoveryCapabilities { + none, + legacy, + loginTokens, +} + class ServerInstallationRecovery extends ServerInstallationState { final RecoveryStep currentStep; + final ServerRecoveryCapabilities recoveryCapabilities; const ServerInstallationRecovery({ String? hetznerKey, @@ -276,6 +283,7 @@ class ServerInstallationRecovery extends ServerInstallationState { User? rootUser, ServerHostingDetails? serverDetails, required RecoveryStep this.currentStep, + required ServerRecoveryCapabilities this.recoveryCapabilities, }) : super( hetznerKey: hetznerKey, cloudFlareKey: cloudFlareKey, @@ -309,13 +317,16 @@ class ServerInstallationRecovery extends ServerInstallationState { User? rootUser, ServerHostingDetails? serverDetails, RecoveryStep? currentStep, + ServerRecoveryCapabilities? recoveryCapabilities, }) => ServerInstallationRecovery( - hetznerKey: hetznerKey ?? this.hetznerKey, - cloudFlareKey: cloudFlareKey ?? this.cloudFlareKey, - backblazeCredential: backblazeCredential ?? this.backblazeCredential, - serverDomain: serverDomain ?? this.serverDomain, - rootUser: rootUser ?? this.rootUser, - serverDetails: serverDetails ?? this.serverDetails, - currentStep: currentStep ?? this.currentStep); + hetznerKey: hetznerKey ?? this.hetznerKey, + cloudFlareKey: cloudFlareKey ?? this.cloudFlareKey, + backblazeCredential: backblazeCredential ?? this.backblazeCredential, + serverDomain: serverDomain ?? this.serverDomain, + rootUser: rootUser ?? this.rootUser, + serverDetails: serverDetails ?? this.serverDetails, + currentStep: currentStep ?? this.currentStep, + recoveryCapabilities: recoveryCapabilities ?? this.recoveryCapabilities, + ); } diff --git a/lib/logic/models/hive/server_details.g.dart b/lib/logic/models/hive/server_details.g.dart index f52e6b37..3d5ff9c1 100644 --- a/lib/logic/models/hive/server_details.g.dart +++ b/lib/logic/models/hive/server_details.g.dart @@ -1,6 +1,6 @@ // GENERATED CODE - DO NOT MODIFY BY HAND -part of 'server_details.dart'; +part of 'server_details_screen.dart'; // ************************************************************************** // TypeAdapterGenerator diff --git a/lib/ui/pages/providers/providers.dart b/lib/ui/pages/providers/providers.dart index 108c85e2..333ee66b 100644 --- a/lib/ui/pages/providers/providers.dart +++ b/lib/ui/pages/providers/providers.dart @@ -1,10 +1,10 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:selfprivacy/config/brand_theme.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/cubit/dns_records/dns_records_cubit.dart'; import 'package:selfprivacy/logic/cubit/providers/providers_cubit.dart'; +import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; import 'package:selfprivacy/logic/models/provider.dart'; import 'package:selfprivacy/ui/components/brand_bottom_sheet/brand_bottom_sheet.dart'; import 'package:selfprivacy/ui/components/brand_cards/brand_cards.dart'; @@ -15,7 +15,7 @@ import 'package:selfprivacy/ui/components/not_ready_card/not_ready_card.dart'; import 'package:selfprivacy/ui/helpers/modals.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.dart'; +import 'package:selfprivacy/ui/pages/server_details/server_details_screen.dart'; import 'package:selfprivacy/utils/route_transitions/basic.dart'; var navigatorKey = GlobalKey(); @@ -113,7 +113,7 @@ class _Card extends StatelessWidget { context: context, builder: (context) => BrandBottomSheet( isExpended: true, - child: ServerDetails(), + child: ServerDetailsScreen(), ), ); diff --git a/lib/ui/pages/server_details/chart.dart b/lib/ui/pages/server_details/chart.dart index 6ccd86fc..242df725 100644 --- a/lib/ui/pages/server_details/chart.dart +++ b/lib/ui/pages/server_details/chart.dart @@ -1,4 +1,4 @@ -part of 'server_details.dart'; +part of 'server_details_screen.dart'; class _Chart extends StatelessWidget { const _Chart({Key? key}) : super(key: key); diff --git a/lib/ui/pages/server_details/header.dart b/lib/ui/pages/server_details/header.dart index d03d3d08..92abf3f8 100644 --- a/lib/ui/pages/server_details/header.dart +++ b/lib/ui/pages/server_details/header.dart @@ -1,4 +1,4 @@ -part of 'server_details.dart'; +part of 'server_details_screen.dart'; class _Header extends StatelessWidget { const _Header({ diff --git a/lib/ui/pages/server_details/server_details.dart b/lib/ui/pages/server_details/server_details_screen.dart similarity index 94% rename from lib/ui/pages/server_details/server_details.dart rename to lib/ui/pages/server_details/server_details_screen.dart index 90d4144e..56e7e18d 100644 --- a/lib/ui/pages/server_details/server_details.dart +++ b/lib/ui/pages/server_details/server_details_screen.dart @@ -1,11 +1,12 @@ 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/common_enum/common_enum.dart'; -import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; import 'package:selfprivacy/logic/cubit/hetzner_metrics/hetzner_metrics_cubit.dart'; import 'package:selfprivacy/logic/cubit/server_detailed_info/server_detailed_info_cubit.dart'; +import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; import 'package:selfprivacy/logic/models/state_types.dart'; import 'package:selfprivacy/ui/components/brand_divider/brand_divider.dart'; import 'package:selfprivacy/ui/components/brand_header/brand_header.dart'; @@ -14,32 +15,32 @@ import 'package:selfprivacy/ui/components/brand_loader/brand_loader.dart'; import 'package:selfprivacy/ui/components/brand_radio_tile/brand_radio_tile.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:easy_localization/easy_localization.dart'; import 'package:selfprivacy/ui/components/switch_block/switch_bloc.dart'; import 'package:selfprivacy/ui/pages/server_details/time_zone/lang.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'; + import 'cpu_chart.dart'; import 'network_charts.dart'; -import 'package:selfprivacy/utils/extensions/duration.dart'; -part 'server_settings.dart'; -part 'text_details.dart'; part 'chart.dart'; part 'header.dart'; +part 'server_settings.dart'; +part 'text_details.dart'; part 'time_zone/time_zone.dart'; var navigatorKey = GlobalKey(); -class ServerDetails extends StatefulWidget { - const ServerDetails({Key? key}) : super(key: key); +class ServerDetailsScreen extends StatefulWidget { + const ServerDetailsScreen({Key? key}) : super(key: key); @override - _ServerDetailsState createState() => _ServerDetailsState(); + _ServerDetailsScreenState createState() => _ServerDetailsScreenState(); } -class _ServerDetailsState extends State +class _ServerDetailsScreenState extends State with SingleTickerProviderStateMixin { late TabController tabController; diff --git a/lib/ui/pages/server_details/server_settings.dart b/lib/ui/pages/server_details/server_settings.dart index c4d6ed02..cca5fcea 100644 --- a/lib/ui/pages/server_details/server_settings.dart +++ b/lib/ui/pages/server_details/server_settings.dart @@ -1,4 +1,4 @@ -part of 'server_details.dart'; +part of 'server_details_screen.dart'; class _ServerSettings extends StatelessWidget { const _ServerSettings({ diff --git a/lib/ui/pages/server_details/text_details.dart b/lib/ui/pages/server_details/text_details.dart index a4620f0e..3d1c751d 100644 --- a/lib/ui/pages/server_details/text_details.dart +++ b/lib/ui/pages/server_details/text_details.dart @@ -1,4 +1,4 @@ -part of 'server_details.dart'; +part of 'server_details_screen.dart'; class _TextDetails extends StatelessWidget { const _TextDetails({Key? key}) : super(key: key); 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 9802bbb8..cdc0fa65 100644 --- a/lib/ui/pages/server_details/time_zone/time_zone.dart +++ b/lib/ui/pages/server_details/time_zone/time_zone.dart @@ -1,4 +1,4 @@ -part of '../server_details.dart'; +part of '../server_details_screen.dart'; final List locations = timeZoneDatabase.locations.values.toList() ..sort((l1, l2) => diff --git a/pubspec.lock b/pubspec.lock index b7e22bfd..965eb666 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -218,6 +218,48 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.2.3" + device_info_plus: + dependency: "direct main" + description: + name: device_info_plus + url: "https://pub.dartlang.org" + source: hosted + version: "3.2.3" + device_info_plus_linux: + dependency: transitive + description: + name: device_info_plus_linux + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" + device_info_plus_macos: + dependency: transitive + description: + name: device_info_plus_macos + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.3" + device_info_plus_platform_interface: + dependency: transitive + description: + name: device_info_plus_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.3.0+1" + device_info_plus_web: + dependency: transitive + description: + name: device_info_plus_web + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + device_info_plus_windows: + dependency: transitive + description: + name: device_info_plus_windows + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" dio: dependency: "direct main" description: @@ -750,7 +792,7 @@ packages: source: hosted version: "6.0.2" pub_semver: - dependency: transitive + dependency: "direct main" description: name: pub_semver url: "https://pub.dartlang.org" diff --git a/pubspec.yaml b/pubspec.yaml index 1a360bbe..f8b02e64 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,6 +13,7 @@ dependencies: crypt: ^4.2.1 cubit_form: ^2.0.1 cupertino_icons: ^1.0.4 + device_info_plus: ^3.2.3 dio: ^4.0.4 dynamic_color: ^1.2.2 easy_localization: ^3.0.0 @@ -36,6 +37,7 @@ dependencies: package_info: ^2.0.2 pretty_dio_logger: ^1.2.0-beta-1 provider: ^6.0.2 + pub_semver: ^2.1.1 share_plus: ^4.0.4 ssh_key: ^0.7.1 system_theme: ^2.0.0 From df40a09419d23a690c97dd6762efa53213f48c8c Mon Sep 17 00:00:00 2001 From: inexcode Date: Wed, 18 May 2022 14:21:11 +0300 Subject: [PATCH 22/52] Add cubit methods to try recover the server --- .../server_installation_cubit.dart | 55 ++++++++++++++++++- .../server_installation_repository.dart | 12 ++-- .../server_installation_state.dart | 6 ++ lib/logic/models/hive/server_details.g.dart | 2 +- 4 files changed, 67 insertions(+), 8 deletions(-) diff --git a/lib/logic/cubit/server_installation/server_installation_cubit.dart b/lib/logic/cubit/server_installation/server_installation_cubit.dart index 38b140db..3c80f2eb 100644 --- a/lib/logic/cubit/server_installation/server_installation_cubit.dart +++ b/lib/logic/cubit/server_installation/server_installation_cubit.dart @@ -3,8 +3,8 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:selfprivacy/logic/models/hive/backblaze_credential.dart'; -import 'package:selfprivacy/logic/models/hive/server_domain.dart'; import 'package:selfprivacy/logic/models/hive/server_details.dart'; +import 'package:selfprivacy/logic/models/hive/server_domain.dart'; import 'package:selfprivacy/logic/models/hive/user.dart'; import '../server_installation/server_installation_repository.dart'; @@ -252,6 +252,59 @@ class ServerInstallationCubit extends Cubit { timer = Timer(delay, work); } + void submitDomainForAccessRecovery(String domain) async { + var serverDomain = ServerDomain( + domainName: domain, + provider: DnsProvider.Unknown, + zoneId: '', + ); + final recoveryCapabilities = + await repository.getRecoveryCapabilities(serverDomain); + + emit(ServerInstallationRecovery( + serverDomain: serverDomain, + recoveryCapabilities: recoveryCapabilities, + currentStep: RecoveryStep.Selecting, + )); + } + + void tryToRecover(String token, ServerRecoveryMethods method) async { + final dataState = this.state as ServerInstallationRecovery; + final serverDomain = dataState.serverDomain; + if (serverDomain == null) { + return; + } + try { + Future Function(ServerDomain, String) + recoveryFunction; + switch (method) { + case ServerRecoveryMethods.newDeviceKey: + recoveryFunction = repository.authorizeByNewDeviceKey; + break; + case ServerRecoveryMethods.recoveryKey: + recoveryFunction = repository.authorizeByRecoveryKey; + break; + case ServerRecoveryMethods.oldToken: + recoveryFunction = repository.authorizeByApiToken; + break; + default: + throw Exception('Unknown recovery method'); + } + final serverDetails = await recoveryFunction( + serverDomain, + token, + ); + emit(dataState.copyWith( + serverDetails: serverDetails, + currentStep: RecoveryStep.HetznerToken, + )); + } on ServerAuthorizationException { + return; + } on IpNotFoundException { + return; + } + } + void clearAppConfig() { closeTimer(); diff --git a/lib/logic/cubit/server_installation/server_installation_repository.dart b/lib/logic/cubit/server_installation/server_installation_repository.dart index 6af170f6..9a0a826c 100644 --- a/lib/logic/cubit/server_installation/server_installation_repository.dart +++ b/lib/logic/cubit/server_installation/server_installation_repository.dart @@ -371,9 +371,9 @@ class ServerInstallationRepository { return 'Unidentified'; } - Future authorizeByLoginToken( + Future authorizeByNewDeviceKey( ServerDomain serverDomain, - String loginToken, + String newDeviceKey, ) async { var serverApi = ServerApi( isWithToken: false, @@ -381,7 +381,7 @@ class ServerInstallationRepository { ); final serverIp = await getServerIpFromDomain(serverDomain); final apiResponse = await serverApi.authorizeDevice( - DeviceToken(device: await getDeviceName(), token: loginToken)); + DeviceToken(device: await getDeviceName(), token: newDeviceKey)); if (apiResponse.isSuccess) { return ServerHostingDetails( @@ -403,16 +403,16 @@ class ServerInstallationRepository { ); } - Future authorizeByRecoveryToken( + Future authorizeByRecoveryKey( ServerDomain serverDomain, - String recoveryToken, + String recoveryKey, ) async { var serverApi = ServerApi( isWithToken: false, overrideDomain: serverDomain.domainName, ); final apiResponse = await serverApi.useRecoveryToken( - DeviceToken(device: await getDeviceName(), token: recoveryToken)); + DeviceToken(device: await getDeviceName(), token: recoveryKey)); if (apiResponse.isSuccess) { return ServerHostingDetails( diff --git a/lib/logic/cubit/server_installation/server_installation_state.dart b/lib/logic/cubit/server_installation/server_installation_state.dart index b75b19d6..a318dd18 100644 --- a/lib/logic/cubit/server_installation/server_installation_state.dart +++ b/lib/logic/cubit/server_installation/server_installation_state.dart @@ -271,6 +271,12 @@ enum ServerRecoveryCapabilities { loginTokens, } +enum ServerRecoveryMethods { + newDeviceKey, + recoveryKey, + oldToken, +} + class ServerInstallationRecovery extends ServerInstallationState { final RecoveryStep currentStep; final ServerRecoveryCapabilities recoveryCapabilities; diff --git a/lib/logic/models/hive/server_details.g.dart b/lib/logic/models/hive/server_details.g.dart index 3d5ff9c1..f52e6b37 100644 --- a/lib/logic/models/hive/server_details.g.dart +++ b/lib/logic/models/hive/server_details.g.dart @@ -1,6 +1,6 @@ // GENERATED CODE - DO NOT MODIFY BY HAND -part of 'server_details_screen.dart'; +part of 'server_details.dart'; // ************************************************************************** // TypeAdapterGenerator From 6fd7f9400d55551ce59b1c05662bb333be64ad1b Mon Sep 17 00:00:00 2001 From: NaiJi Date: Thu, 19 May 2022 17:26:57 +0300 Subject: [PATCH 23/52] Implement recovery by old token pages with mock .md Co-authored-by: Inex Code --- assets/markdown/how_fallback_old-en.md | 15 +++ assets/markdown/how_fallback_old-ru.md | 13 +++ assets/markdown/how_fallback_ssh-en.md | 15 +++ assets/markdown/how_fallback_ssh-ru.md | 13 +++ assets/markdown/how_fallback_terminal-en.md | 15 +++ assets/markdown/how_fallback_terminal-ru.md | 13 +++ assets/translations/en.json | 2 +- .../initializing/backblaze_form_cubit.dart | 6 +- .../setup/initializing/domain_cloudflare.dart | 6 +- .../initializing/hetzner_form_cubit.dart | 6 +- .../initializing/root_user_form_cubit.dart | 6 +- .../recovery_domain_form_cubit.dart | 4 + .../server_installation_cubit.dart | 40 +++++++ .../components/brand_header/brand_header.dart | 5 +- .../brand_hero_screen/brand_hero_screen.dart | 7 +- lib/ui/pages/setup/initializing.dart | 6 +- ..._2.dart => recover_by_new_device_key.dart} | 30 ++++- .../recovering/recover_by_old_token.dart | 79 +++++++++++++ ...oken.dart => recover_by_recovery_key.dart} | 4 +- .../setup/recovering/recovery_domain.dart | 49 -------- .../recovering/recovery_fallback_select.dart | 56 --------- .../recovering/recovery_method_device_1.dart | 25 ---- .../recovering/recovery_method_select.dart | 77 +++++++++++-- .../setup/recovering/recovery_routing.dart | 109 ++++++++++++++++++ 24 files changed, 439 insertions(+), 162 deletions(-) create mode 100644 assets/markdown/how_fallback_old-en.md create mode 100644 assets/markdown/how_fallback_old-ru.md create mode 100644 assets/markdown/how_fallback_ssh-en.md create mode 100644 assets/markdown/how_fallback_ssh-ru.md create mode 100644 assets/markdown/how_fallback_terminal-en.md create mode 100644 assets/markdown/how_fallback_terminal-ru.md rename lib/ui/pages/setup/recovering/{recovery_method_device_2.dart => recover_by_new_device_key.dart} (70%) create mode 100644 lib/ui/pages/setup/recovering/recover_by_old_token.dart rename lib/ui/pages/setup/recovering/{recovery_method_token.dart => recover_by_recovery_key.dart} (91%) delete mode 100644 lib/ui/pages/setup/recovering/recovery_domain.dart delete mode 100644 lib/ui/pages/setup/recovering/recovery_fallback_select.dart delete mode 100644 lib/ui/pages/setup/recovering/recovery_method_device_1.dart create mode 100644 lib/ui/pages/setup/recovering/recovery_routing.dart diff --git a/assets/markdown/how_fallback_old-en.md b/assets/markdown/how_fallback_old-en.md new file mode 100644 index 00000000..368ea83a --- /dev/null +++ b/assets/markdown/how_fallback_old-en.md @@ -0,0 +1,15 @@ +### How to get Cloudflare API Token +1. Visit the following link: https://dash.cloudflare.com/ +2. the right corner, click on the profile icon (a man in a circle). For the mobile version of the site, in the upper left corner, click the **Menu** button (three horizontal bars), in the dropdown menu, click on **My Profile** +3. There are four configuration categories to choose from: *Communication*, *Authentication*, **API Tokens**, *Session*. Choose **API Tokens**. +4. Click on **Create Token** button. +5. Go down to the bottom and see the **Create Custom Token** field and press **Get Started** button on the right side. +6. In the **Token Name** field, give your token a name. +7. Next we have Permissions. In the leftmost field, select **Zone**. In the longest field, center, select **DNS**. In the rightmost field, select **Edit**. +8. Next, right under this line, click Add More. Similar field will appear. +9. In the leftmost field of the new line, select, similar to the last line — **Zone**. In the center — a little different. Here choose the same as in the left — **Zone**. In the rightmost field, select **Read**. +10. Next look at **Zone Resources**. Under this inscription there is a line with two fields. The left must have **Include** and the right must have **Specific Zone**. Once you select Specific Zone, another field appears on the right. Choose your domain in it. +11. Flick to the bottom and press the blue **Continue to Summary** button. +12. Check if you got everything right. A similar string must be present: *Domain — DNS:Edit, Zone:Read*. +13. Click on **Create Token**. +14. We copy the created token, and save it in a reliable place (preferably in the password manager). diff --git a/assets/markdown/how_fallback_old-ru.md b/assets/markdown/how_fallback_old-ru.md new file mode 100644 index 00000000..2c0ad22b --- /dev/null +++ b/assets/markdown/how_fallback_old-ru.md @@ -0,0 +1,13 @@ +### Как получить Cloudflare API Token +1. Переходим по [ссылке](https://dash.cloudflare.com/) и авторизуемся в ранее созданном аккаунте. https://dash.cloudflare.com/ +В правом углу кликаем на иконку профиля (человечек в кружочке). Для мобильной версии сайта, в верхнем левом углу, нажимаем кнопку **Меню** (три горизонтальных полоски), в выпавшем меню, ищем пункт **My Profile**. +3. Нам предлагается на выбор, четыре категории настройки: **Preferences**, **Authentication**, **API Tokens**, **Sessions**. Выбираем **API Tokens**. +4. Самым первым пунктом видим кнопку **Create Token**. С полной уверенностью в себе и желанием обрести приватность, нажимаем на неё. +5. Спускаемся в самый низ и видим поле **Create Custom Token** и кнопку **Get Started** с правой стороны. Нажимаем. +6. В поле **Token Name** даём своему токену имя. Можете покреативить и отнестись к этому как к наименованию домашнего зверька :) +7. Далее, у нас **Permissions**. В первом поле выбираем Zone. Во втором поле, по центру, выбираем **DNS**. В последнем поле выбираем **Edit**. +8. Далее смотрим на **Zone Resources**. Под этой надписью есть строка с двумя полями. В первом должно быть **Include**, а во втором — **Specific Zone**. Как только Вы выберите **Specific Zone**, справа появится ещё одно поле. В нём выбираем наш домен. +9. Листаем в самый низ и нажимаем на синюю кнопку **Continue to Summary**. +10. Проверяем, всё ли мы правильно выбрали. Должна присутствовать подобная строка: ваш.домен — **DNS:Edit, Zone:Read**. +11. Нажимаем **Create Token**. +12. Копируем созданный токен, и сохраняем его в надёжном месте (желательно — в менеджере паролей). \ No newline at end of file diff --git a/assets/markdown/how_fallback_ssh-en.md b/assets/markdown/how_fallback_ssh-en.md new file mode 100644 index 00000000..368ea83a --- /dev/null +++ b/assets/markdown/how_fallback_ssh-en.md @@ -0,0 +1,15 @@ +### How to get Cloudflare API Token +1. Visit the following link: https://dash.cloudflare.com/ +2. the right corner, click on the profile icon (a man in a circle). For the mobile version of the site, in the upper left corner, click the **Menu** button (three horizontal bars), in the dropdown menu, click on **My Profile** +3. There are four configuration categories to choose from: *Communication*, *Authentication*, **API Tokens**, *Session*. Choose **API Tokens**. +4. Click on **Create Token** button. +5. Go down to the bottom and see the **Create Custom Token** field and press **Get Started** button on the right side. +6. In the **Token Name** field, give your token a name. +7. Next we have Permissions. In the leftmost field, select **Zone**. In the longest field, center, select **DNS**. In the rightmost field, select **Edit**. +8. Next, right under this line, click Add More. Similar field will appear. +9. In the leftmost field of the new line, select, similar to the last line — **Zone**. In the center — a little different. Here choose the same as in the left — **Zone**. In the rightmost field, select **Read**. +10. Next look at **Zone Resources**. Under this inscription there is a line with two fields. The left must have **Include** and the right must have **Specific Zone**. Once you select Specific Zone, another field appears on the right. Choose your domain in it. +11. Flick to the bottom and press the blue **Continue to Summary** button. +12. Check if you got everything right. A similar string must be present: *Domain — DNS:Edit, Zone:Read*. +13. Click on **Create Token**. +14. We copy the created token, and save it in a reliable place (preferably in the password manager). diff --git a/assets/markdown/how_fallback_ssh-ru.md b/assets/markdown/how_fallback_ssh-ru.md new file mode 100644 index 00000000..2c0ad22b --- /dev/null +++ b/assets/markdown/how_fallback_ssh-ru.md @@ -0,0 +1,13 @@ +### Как получить Cloudflare API Token +1. Переходим по [ссылке](https://dash.cloudflare.com/) и авторизуемся в ранее созданном аккаунте. https://dash.cloudflare.com/ +В правом углу кликаем на иконку профиля (человечек в кружочке). Для мобильной версии сайта, в верхнем левом углу, нажимаем кнопку **Меню** (три горизонтальных полоски), в выпавшем меню, ищем пункт **My Profile**. +3. Нам предлагается на выбор, четыре категории настройки: **Preferences**, **Authentication**, **API Tokens**, **Sessions**. Выбираем **API Tokens**. +4. Самым первым пунктом видим кнопку **Create Token**. С полной уверенностью в себе и желанием обрести приватность, нажимаем на неё. +5. Спускаемся в самый низ и видим поле **Create Custom Token** и кнопку **Get Started** с правой стороны. Нажимаем. +6. В поле **Token Name** даём своему токену имя. Можете покреативить и отнестись к этому как к наименованию домашнего зверька :) +7. Далее, у нас **Permissions**. В первом поле выбираем Zone. Во втором поле, по центру, выбираем **DNS**. В последнем поле выбираем **Edit**. +8. Далее смотрим на **Zone Resources**. Под этой надписью есть строка с двумя полями. В первом должно быть **Include**, а во втором — **Specific Zone**. Как только Вы выберите **Specific Zone**, справа появится ещё одно поле. В нём выбираем наш домен. +9. Листаем в самый низ и нажимаем на синюю кнопку **Continue to Summary**. +10. Проверяем, всё ли мы правильно выбрали. Должна присутствовать подобная строка: ваш.домен — **DNS:Edit, Zone:Read**. +11. Нажимаем **Create Token**. +12. Копируем созданный токен, и сохраняем его в надёжном месте (желательно — в менеджере паролей). \ No newline at end of file diff --git a/assets/markdown/how_fallback_terminal-en.md b/assets/markdown/how_fallback_terminal-en.md new file mode 100644 index 00000000..368ea83a --- /dev/null +++ b/assets/markdown/how_fallback_terminal-en.md @@ -0,0 +1,15 @@ +### How to get Cloudflare API Token +1. Visit the following link: https://dash.cloudflare.com/ +2. the right corner, click on the profile icon (a man in a circle). For the mobile version of the site, in the upper left corner, click the **Menu** button (three horizontal bars), in the dropdown menu, click on **My Profile** +3. There are four configuration categories to choose from: *Communication*, *Authentication*, **API Tokens**, *Session*. Choose **API Tokens**. +4. Click on **Create Token** button. +5. Go down to the bottom and see the **Create Custom Token** field and press **Get Started** button on the right side. +6. In the **Token Name** field, give your token a name. +7. Next we have Permissions. In the leftmost field, select **Zone**. In the longest field, center, select **DNS**. In the rightmost field, select **Edit**. +8. Next, right under this line, click Add More. Similar field will appear. +9. In the leftmost field of the new line, select, similar to the last line — **Zone**. In the center — a little different. Here choose the same as in the left — **Zone**. In the rightmost field, select **Read**. +10. Next look at **Zone Resources**. Under this inscription there is a line with two fields. The left must have **Include** and the right must have **Specific Zone**. Once you select Specific Zone, another field appears on the right. Choose your domain in it. +11. Flick to the bottom and press the blue **Continue to Summary** button. +12. Check if you got everything right. A similar string must be present: *Domain — DNS:Edit, Zone:Read*. +13. Click on **Create Token**. +14. We copy the created token, and save it in a reliable place (preferably in the password manager). diff --git a/assets/markdown/how_fallback_terminal-ru.md b/assets/markdown/how_fallback_terminal-ru.md new file mode 100644 index 00000000..2c0ad22b --- /dev/null +++ b/assets/markdown/how_fallback_terminal-ru.md @@ -0,0 +1,13 @@ +### Как получить Cloudflare API Token +1. Переходим по [ссылке](https://dash.cloudflare.com/) и авторизуемся в ранее созданном аккаунте. https://dash.cloudflare.com/ +В правом углу кликаем на иконку профиля (человечек в кружочке). Для мобильной версии сайта, в верхнем левом углу, нажимаем кнопку **Меню** (три горизонтальных полоски), в выпавшем меню, ищем пункт **My Profile**. +3. Нам предлагается на выбор, четыре категории настройки: **Preferences**, **Authentication**, **API Tokens**, **Sessions**. Выбираем **API Tokens**. +4. Самым первым пунктом видим кнопку **Create Token**. С полной уверенностью в себе и желанием обрести приватность, нажимаем на неё. +5. Спускаемся в самый низ и видим поле **Create Custom Token** и кнопку **Get Started** с правой стороны. Нажимаем. +6. В поле **Token Name** даём своему токену имя. Можете покреативить и отнестись к этому как к наименованию домашнего зверька :) +7. Далее, у нас **Permissions**. В первом поле выбираем Zone. Во втором поле, по центру, выбираем **DNS**. В последнем поле выбираем **Edit**. +8. Далее смотрим на **Zone Resources**. Под этой надписью есть строка с двумя полями. В первом должно быть **Include**, а во втором — **Specific Zone**. Как только Вы выберите **Specific Zone**, справа появится ещё одно поле. В нём выбираем наш домен. +9. Листаем в самый низ и нажимаем на синюю кнопку **Continue to Summary**. +10. Проверяем, всё ли мы правильно выбрали. Должна присутствовать подобная строка: ваш.домен — **DNS:Edit, Zone:Read**. +11. Нажимаем **Create Token**. +12. Копируем созданный токен, и сохраняем его в надёжном месте (желательно — в менеджере паролей). \ No newline at end of file diff --git a/assets/translations/en.json b/assets/translations/en.json index 3b7bf3af..88889d8d 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -296,7 +296,7 @@ "method_device_button": "I have received my token", "method_device_input_description": "Enter your authorization token", "method_device_input_placeholder": "Token", - "method_recovery_input_description": "Enter your recovery token", + "method_recovery_input_description": "Enter your recovery key", "fallback_select_description": "What exactly do you have? Pick the first available option:", "fallback_select_token_copy": "Copy of auth token from other version of the application.", "fallback_select_root_ssh": "Root SSH access to the server.", diff --git a/lib/logic/cubit/forms/setup/initializing/backblaze_form_cubit.dart b/lib/logic/cubit/forms/setup/initializing/backblaze_form_cubit.dart index 50fc8e80..f3554ef4 100644 --- a/lib/logic/cubit/forms/setup/initializing/backblaze_form_cubit.dart +++ b/lib/logic/cubit/forms/setup/initializing/backblaze_form_cubit.dart @@ -6,7 +6,7 @@ import 'package:selfprivacy/logic/models/hive/backblaze_credential.dart'; import 'package:easy_localization/easy_localization.dart'; class BackblazeFormCubit extends FormCubit { - BackblazeFormCubit(this.initializingCubit) { + BackblazeFormCubit(this.serverSetupCubit) { //var regExp = RegExp(r"\s+|[-!$%^&*()@+|~=`{}\[\]:<>?,.\/]"); keyId = FieldCubit( initalValue: '', @@ -27,13 +27,13 @@ class BackblazeFormCubit extends FormCubit { @override FutureOr onSubmit() async { - initializingCubit.setBackblazeKey( + serverSetupCubit.setBackblazeKey( keyId.state.value, applicationKey.state.value, ); } - final ServerInstallationCubit initializingCubit; + final ServerInstallationCubit serverSetupCubit; late final FieldCubit keyId; late final FieldCubit applicationKey; diff --git a/lib/logic/cubit/forms/setup/initializing/domain_cloudflare.dart b/lib/logic/cubit/forms/setup/initializing/domain_cloudflare.dart index 363cee7c..07c46c74 100644 --- a/lib/logic/cubit/forms/setup/initializing/domain_cloudflare.dart +++ b/lib/logic/cubit/forms/setup/initializing/domain_cloudflare.dart @@ -4,9 +4,9 @@ import 'package:selfprivacy/logic/cubit/server_installation/server_installation_ import 'package:selfprivacy/logic/models/hive/server_domain.dart'; class DomainSetupCubit extends Cubit { - DomainSetupCubit(this.initializingCubit) : super(Initial()); + DomainSetupCubit(this.serverSetupCubit) : super(Initial()); - final ServerInstallationCubit initializingCubit; + final ServerInstallationCubit serverSetupCubit; Future load() async { emit(Loading(LoadingTypes.loadingDomain)); @@ -42,7 +42,7 @@ class DomainSetupCubit extends Cubit { provider: DnsProvider.Cloudflare, ); - initializingCubit.setDomain(domain); + serverSetupCubit.setDomain(domain); emit(DomainSet()); } } diff --git a/lib/logic/cubit/forms/setup/initializing/hetzner_form_cubit.dart b/lib/logic/cubit/forms/setup/initializing/hetzner_form_cubit.dart index 42517c64..6871942e 100644 --- a/lib/logic/cubit/forms/setup/initializing/hetzner_form_cubit.dart +++ b/lib/logic/cubit/forms/setup/initializing/hetzner_form_cubit.dart @@ -7,7 +7,7 @@ import 'package:selfprivacy/logic/cubit/server_installation/server_installation_ import 'package:selfprivacy/logic/cubit/forms/validations/validations.dart'; class HetznerFormCubit extends FormCubit { - HetznerFormCubit(this.initializingCubit) { + HetznerFormCubit(this.serverSetupCubit) { var regExp = RegExp(r"\s+|[-!$%^&*()@+|~=`{}\[\]:<>?,.\/]"); apiKey = FieldCubit( initalValue: '', @@ -24,10 +24,10 @@ class HetznerFormCubit extends FormCubit { @override FutureOr onSubmit() async { - initializingCubit.setHetznerKey(apiKey.state.value); + serverSetupCubit.setHetznerKey(apiKey.state.value); } - final ServerInstallationCubit initializingCubit; + final ServerInstallationCubit serverSetupCubit; late final FieldCubit apiKey; diff --git a/lib/logic/cubit/forms/setup/initializing/root_user_form_cubit.dart b/lib/logic/cubit/forms/setup/initializing/root_user_form_cubit.dart index 3bff2aba..81ccd88d 100644 --- a/lib/logic/cubit/forms/setup/initializing/root_user_form_cubit.dart +++ b/lib/logic/cubit/forms/setup/initializing/root_user_form_cubit.dart @@ -7,7 +7,7 @@ import 'package:selfprivacy/logic/models/hive/user.dart'; class RootUserFormCubit extends FormCubit { RootUserFormCubit( - this.initializingCubit, final FieldCubitFactory fieldFactory) { + this.serverSetupCubit, final FieldCubitFactory fieldFactory) { userName = fieldFactory.createUserLoginField(); password = fieldFactory.createUserPasswordField(); @@ -22,10 +22,10 @@ class RootUserFormCubit extends FormCubit { login: userName.state.value, password: password.state.value, ); - initializingCubit.setRootUser(user); + serverSetupCubit.setRootUser(user); } - final ServerInstallationCubit initializingCubit; + final ServerInstallationCubit serverSetupCubit; late final FieldCubit userName; late final FieldCubit password; diff --git a/lib/logic/cubit/forms/setup/recovering/recovery_domain_form_cubit.dart b/lib/logic/cubit/forms/setup/recovering/recovery_domain_form_cubit.dart index 59e512d2..921cf996 100644 --- a/lib/logic/cubit/forms/setup/recovering/recovery_domain_form_cubit.dart +++ b/lib/logic/cubit/forms/setup/recovering/recovery_domain_form_cubit.dart @@ -43,6 +43,10 @@ class RecoveryDomainFormCubit extends FormCubit { return domainValid; } + FutureOr setCustomError(String error) { + serverDomainField.setError(error); + } + final ServerInstallationCubit initializingCubit; late final FieldCubit serverDomainField; } diff --git a/lib/logic/cubit/server_installation/server_installation_cubit.dart b/lib/logic/cubit/server_installation/server_installation_cubit.dart index 3c80f2eb..33d360fe 100644 --- a/lib/logic/cubit/server_installation/server_installation_cubit.dart +++ b/lib/logic/cubit/server_installation/server_installation_cubit.dart @@ -305,6 +305,46 @@ class ServerInstallationCubit extends Cubit { } } + void revertRecoveryStep() { + final dataState = this.state as ServerInstallationRecovery; + switch (dataState.currentStep) { + case RecoveryStep.Selecting: + emit(ServerInstallationEmpty()); + break; + case RecoveryStep.RecoveryKey: + case RecoveryStep.NewDeviceKey: + case RecoveryStep.OldToken: + emit(dataState.copyWith( + currentStep: RecoveryStep.Selecting, + )); + break; + // We won't revert steps after client is authorized + default: + break; + } + } + + void selectRecoveryMethod(ServerRecoveryMethods method) { + final dataState = this.state as ServerInstallationRecovery; + switch (method) { + case ServerRecoveryMethods.newDeviceKey: + emit(dataState.copyWith( + currentStep: RecoveryStep.NewDeviceKey, + )); + break; + case ServerRecoveryMethods.recoveryKey: + emit(dataState.copyWith( + currentStep: RecoveryStep.RecoveryKey, + )); + break; + case ServerRecoveryMethods.oldToken: + emit(dataState.copyWith( + currentStep: RecoveryStep.OldToken, + )); + break; + } + } + void clearAppConfig() { closeTimer(); diff --git a/lib/ui/components/brand_header/brand_header.dart b/lib/ui/components/brand_header/brand_header.dart index 6795fdb4..ca36305e 100644 --- a/lib/ui/components/brand_header/brand_header.dart +++ b/lib/ui/components/brand_header/brand_header.dart @@ -9,11 +9,13 @@ class BrandHeader extends StatelessWidget { this.title = "", this.hasBackButton = false, this.hasFlashButton = false, + this.onBackButtonPressed, }) : super(key: key); final String title; final bool hasBackButton; final bool hasFlashButton; + final VoidCallback? onBackButtonPressed; @override Widget build(BuildContext context) { @@ -29,7 +31,8 @@ class BrandHeader extends StatelessWidget { if (hasBackButton) ...[ IconButton( icon: Icon(BrandIcons.arrow_left), - onPressed: () => Navigator.of(context).pop(), + onPressed: + onBackButtonPressed ?? () => Navigator.of(context).pop(), ), SizedBox(width: 10), ], diff --git a/lib/ui/components/brand_hero_screen/brand_hero_screen.dart b/lib/ui/components/brand_hero_screen/brand_hero_screen.dart index efa75515..e6163cd8 100644 --- a/lib/ui/components/brand_hero_screen/brand_hero_screen.dart +++ b/lib/ui/components/brand_hero_screen/brand_hero_screen.dart @@ -11,6 +11,7 @@ class BrandHeroScreen extends StatelessWidget { this.heroIcon, this.heroTitle, this.heroSubtitle, + this.onBackButtonPressed, }) : super(key: key); final List children; @@ -20,6 +21,7 @@ class BrandHeroScreen extends StatelessWidget { final IconData? heroIcon; final String? heroTitle; final String? heroSubtitle; + final VoidCallback? onBackButtonPressed; @override Widget build(BuildContext context) { @@ -31,6 +33,7 @@ class BrandHeroScreen extends StatelessWidget { title: headerTitle, hasBackButton: hasBackButton, hasFlashButton: hasFlashButton, + onBackButtonPressed: onBackButtonPressed, ), ), body: ListView( @@ -48,9 +51,7 @@ class BrandHeroScreen extends StatelessWidget { if (heroTitle != null) Text( heroTitle!, - style: Theme.of(context).textTheme.headlineMedium?.copyWith( - color: Colors.black, - ), + style: Theme.of(context).textTheme.headlineMedium, textAlign: TextAlign.start, ), SizedBox(height: 8.0), diff --git a/lib/ui/pages/setup/initializing.dart b/lib/ui/pages/setup/initializing.dart index e1f6203e..81630b63 100644 --- a/lib/ui/pages/setup/initializing.dart +++ b/lib/ui/pages/setup/initializing.dart @@ -17,7 +17,7 @@ import 'package:selfprivacy/ui/components/brand_text/brand_text.dart'; import 'package:selfprivacy/ui/components/brand_timer/brand_timer.dart'; import 'package:selfprivacy/ui/components/progress_bar/progress_bar.dart'; import 'package:selfprivacy/ui/pages/rootRoute.dart'; -import 'package:selfprivacy/ui/pages/setup/recovering/recovery_domain.dart'; +import 'package:selfprivacy/ui/pages/setup/recovering/recovery_routing.dart'; import 'package:selfprivacy/ui/pages/setup/recovering/recovery_method_select.dart'; import 'package:selfprivacy/utils/route_transitions/basic.dart'; @@ -37,6 +37,10 @@ class InitializingPage extends StatelessWidget { () => _stepCheck(cubit), () => Container(child: Center(child: Text('initializing.finish'.tr()))) ][cubit.state.progress.index](); + + if (cubit is ServerInstallationRecovery) { + return RecoveryRouting(); + } return BlocListener( listener: (context, state) { if (cubit.state is ServerInstallationFinished) { diff --git a/lib/ui/pages/setup/recovering/recovery_method_device_2.dart b/lib/ui/pages/setup/recovering/recover_by_new_device_key.dart similarity index 70% rename from lib/ui/pages/setup/recovering/recovery_method_device_2.dart rename to lib/ui/pages/setup/recovering/recover_by_new_device_key.dart index e9323803..63f7e127 100644 --- a/lib/ui/pages/setup/recovering/recovery_method_device_2.dart +++ b/lib/ui/pages/setup/recovering/recover_by_new_device_key.dart @@ -1,13 +1,35 @@ -import 'package:cubit_form/cubit_form.dart'; import 'package:easy_localization/easy_localization.dart'; 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/FilledButton.dart'; import 'package:selfprivacy/ui/components/brand_hero_screen/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'; +import 'package:selfprivacy/logic/cubit/forms/factories/field_cubit_factory.dart'; -class RecoveryMethodDevice2 extends StatelessWidget { +class RecoverByNewDeviceKeyInstruction extends StatelessWidget { + @override + Widget build(BuildContext context) { + return BrandHeroScreen( + heroTitle: "recovering.recovery_main_header".tr(), + heroSubtitle: "recovering.method_device_description".tr(), + hasBackButton: true, + hasFlashButton: false, + onBackButtonPressed: () => + context.read().revertRecoveryStep(), + children: [ + FilledButton( + title: "recovering.method_device_button".tr(), + onPressed: () => Navigator.of(context) + .push(materialRoute(RecoverByNewDeviceKeyInput())), + ) + ], + ); + } +} + +class RecoverByNewDeviceKeyInput extends StatelessWidget { @override Widget build(BuildContext context) { var appConfig = context.watch(); diff --git a/lib/ui/pages/setup/recovering/recover_by_old_token.dart b/lib/ui/pages/setup/recovering/recover_by_old_token.dart new file mode 100644 index 00000000..522494fd --- /dev/null +++ b/lib/ui/pages/setup/recovering/recover_by_old_token.dart @@ -0,0 +1,79 @@ +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/FilledButton.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/utils/route_transitions/basic.dart'; +import 'package:cubit_form/cubit_form.dart'; +import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; +import 'package:selfprivacy/logic/cubit/forms/factories/field_cubit_factory.dart'; + +class RecoverByOldTokenInstruction extends StatelessWidget { + @override + RecoverByOldTokenInstruction({required this.instructionFilename}); + + Widget build(BuildContext context) { + return BrandHeroScreen( + heroTitle: "recovering.recovery_main_header".tr(), + hasBackButton: true, + hasFlashButton: false, + onBackButtonPressed: () => + context.read().revertRecoveryStep(), + children: [ + BrandMarkdown( + fileName: instructionFilename, + ), + SizedBox(height: 16), + FilledButton( + title: "recovering.method_device_button".tr(), + onPressed: () => + Navigator.of(context).push(materialRoute(RecoverByOldToken())), + ) + ], + ); + } + + final String instructionFilename; +} + +class RecoverByOldToken extends StatelessWidget { + @override + Widget build(BuildContext context) { + var appConfig = context.watch(); + + return BlocProvider( + create: (context) => + RecoveryDeviceFormCubit(appConfig, FieldCubitFactory(context)), + child: Builder( + builder: (context) { + var formCubitState = context.watch().state; + + return BrandHeroScreen( + heroTitle: "recovering.recovery_main_header".tr(), + heroSubtitle: "recovering.method_device_input_description".tr(), + hasBackButton: true, + hasFlashButton: false, + children: [ + CubitFormTextField( + formFieldCubit: + context.read().tokenField, + decoration: InputDecoration( + border: OutlineInputBorder(), + labelText: "recovering.method_device_input_placeholder".tr(), + ), + ), + SizedBox(height: 16), + FilledButton( + title: "more.continue".tr(), + onPressed: formCubitState.isSubmitting + ? null + : () => context.read().trySubmit(), + ) + ], + ); + }, + ), + ); + } +} diff --git a/lib/ui/pages/setup/recovering/recovery_method_token.dart b/lib/ui/pages/setup/recovering/recover_by_recovery_key.dart similarity index 91% rename from lib/ui/pages/setup/recovering/recovery_method_token.dart rename to lib/ui/pages/setup/recovering/recover_by_recovery_key.dart index 7b0c2564..780c1ca4 100644 --- a/lib/ui/pages/setup/recovering/recovery_method_token.dart +++ b/lib/ui/pages/setup/recovering/recover_by_recovery_key.dart @@ -7,7 +7,7 @@ import 'package:selfprivacy/logic/cubit/server_installation/server_installation_ import 'package:selfprivacy/ui/components/brand_button/FilledButton.dart'; import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; -class RecoveryMethodToken extends StatelessWidget { +class RecoverByRecoveryKey extends StatelessWidget { @override Widget build(BuildContext context) { var appConfig = context.watch(); @@ -24,6 +24,8 @@ class RecoveryMethodToken extends StatelessWidget { heroSubtitle: "recovering.method_recovery_input_description".tr(), hasBackButton: true, hasFlashButton: false, + onBackButtonPressed: () => + context.read().revertRecoveryStep(), children: [ CubitFormTextField( formFieldCubit: diff --git a/lib/ui/pages/setup/recovering/recovery_domain.dart b/lib/ui/pages/setup/recovering/recovery_domain.dart deleted file mode 100644 index f38a554f..00000000 --- a/lib/ui/pages/setup/recovering/recovery_domain.dart +++ /dev/null @@ -1,49 +0,0 @@ -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/FilledButton.dart'; -import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; - -class RecoveryDomain extends StatelessWidget { - @override - Widget build(BuildContext context) { - var serverInstallation = context.watch(); - - return BlocProvider( - create: (context) => RecoveryDomainFormCubit( - serverInstallation, FieldCubitFactory(context)), - child: Builder( - builder: (context) { - var formCubitState = context.watch().state; - - return BrandHeroScreen( - heroTitle: "recovering.recovery_main_header".tr(), - heroSubtitle: "recovering.domain_recovery_description".tr(), - hasBackButton: true, - hasFlashButton: false, - children: [ - CubitFormTextField( - formFieldCubit: - context.read().serverDomainField, - decoration: InputDecoration( - border: OutlineInputBorder(), - labelText: "recovering.domain_recover_placeholder".tr(), - ), - ), - SizedBox(height: 16), - FilledButton( - title: "more.continue".tr(), - onPressed: formCubitState.isSubmitting - ? null - : () => context.read().trySubmit(), - ) - ], - ); - }, - ), - ); - } -} diff --git a/lib/ui/pages/setup/recovering/recovery_fallback_select.dart b/lib/ui/pages/setup/recovering/recovery_fallback_select.dart deleted file mode 100644 index bca3d07c..00000000 --- a/lib/ui/pages/setup/recovering/recovery_fallback_select.dart +++ /dev/null @@ -1,56 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:selfprivacy/ui/components/brand_cards/brand_cards.dart'; -import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; -import 'package:selfprivacy/utils/route_transitions/basic.dart'; -import 'package:selfprivacy/ui/pages/rootRoute.dart'; - -class RecoveryFallbackSelect extends StatelessWidget { - @override - Widget build(BuildContext context) { - return BrandHeroScreen( - heroTitle: "recovering.recovery_main_header".tr(), - heroSubtitle: "recovering.fallback_select_description".tr(), - hasBackButton: true, - hasFlashButton: false, - children: [ - BrandCards.outlined( - child: ListTile( - title: Text( - "recovering.fallback_select_token_copy".tr(), - style: Theme.of(context).textTheme.titleMedium, - ), - leading: Icon(Icons.vpn_key), - onTap: () => Navigator.of(context).push(materialRoute(RootPage())), - ), - ), - SizedBox(height: 16), - BrandCards.outlined( - child: ListTile( - title: Text( - "recovering.fallback_select_root_ssh".tr(), - style: Theme.of(context).textTheme.titleMedium, - ), - leading: Icon(Icons.terminal), - onTap: () => Navigator.of(context).push(materialRoute(RootPage())), - ), - ), - SizedBox(height: 16), - BrandCards.outlined( - child: ListTile( - title: Text( - "recovering.fallback_select_provider_console".tr(), - style: Theme.of(context).textTheme.titleMedium, - ), - subtitle: Text( - "recovering.fallback_select_provider_console_hint".tr(), - style: Theme.of(context).textTheme.bodyMedium, - ), - leading: Icon(Icons.web), - onTap: () => Navigator.of(context).push(materialRoute(RootPage())), - ), - ), - ], - ); - } -} diff --git a/lib/ui/pages/setup/recovering/recovery_method_device_1.dart b/lib/ui/pages/setup/recovering/recovery_method_device_1.dart deleted file mode 100644 index 43b071b8..00000000 --- a/lib/ui/pages/setup/recovering/recovery_method_device_1.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:selfprivacy/ui/components/brand_button/FilledButton.dart'; -import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; -import 'package:selfprivacy/ui/pages/setup/recovering/recovery_method_device_2.dart'; -import 'package:selfprivacy/utils/route_transitions/basic.dart'; - -class RecoveryMethodDevice1 extends StatelessWidget { - @override - Widget build(BuildContext context) { - return BrandHeroScreen( - heroTitle: "recovering.recovery_main_header".tr(), - heroSubtitle: "recovering.method_device_description".tr(), - hasBackButton: true, - hasFlashButton: false, - children: [ - FilledButton( - title: "recovering.method_device_button".tr(), - onPressed: () => Navigator.of(context) - .push(materialRoute(RecoveryMethodDevice2())), - ) - ], - ); - } -} diff --git a/lib/ui/pages/setup/recovering/recovery_method_select.dart b/lib/ui/pages/setup/recovering/recovery_method_select.dart index f92545eb..57c336b8 100644 --- a/lib/ui/pages/setup/recovering/recovery_method_select.dart +++ b/lib/ui/pages/setup/recovering/recovery_method_select.dart @@ -1,13 +1,11 @@ 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/ui/components/brand_button/brand_button.dart'; import 'package:selfprivacy/ui/components/brand_cards/brand_cards.dart'; import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; -import 'package:selfprivacy/ui/pages/setup/recovering/recovery_fallback_select.dart'; -import 'package:selfprivacy/ui/pages/setup/recovering/recovery_method_device_1.dart'; -import 'package:selfprivacy/ui/pages/setup/recovering/recovery_method_token.dart'; +import 'package:selfprivacy/ui/pages/setup/recovering/recover_by_old_token.dart'; import 'package:selfprivacy/utils/route_transitions/basic.dart'; -import 'package:selfprivacy/ui/pages/rootRoute.dart'; class RecoveryMethodSelect extends StatelessWidget { @override @@ -25,8 +23,9 @@ class RecoveryMethodSelect extends StatelessWidget { style: Theme.of(context).textTheme.titleMedium, ), leading: Icon(Icons.offline_share_outlined), - onTap: () => Navigator.of(context) - .push(materialRoute(RecoveryMethodDevice1())), + onTap: () => context + .read() + .selectRecoveryMethod(ServerRecoveryMethods.newDeviceKey), ), ), SizedBox(height: 16), @@ -37,17 +36,77 @@ class RecoveryMethodSelect extends StatelessWidget { style: Theme.of(context).textTheme.titleMedium, ), leading: Icon(Icons.password_outlined), - onTap: () => Navigator.of(context) - .push(materialRoute(RecoveryMethodToken())), + onTap: () => context + .read() + .selectRecoveryMethod(ServerRecoveryMethods.recoveryKey), ), ), SizedBox(height: 16), BrandButton.text( title: "recovering.method_select_nothing".tr(), onPressed: () => Navigator.of(context) - .push(materialRoute(RecoveryFallbackSelect())), + .push(materialRoute(RecoveryFallbackMethodSelect())), ) ], ); } } + +class RecoveryFallbackMethodSelect extends StatelessWidget { + @override + Widget build(BuildContext context) { + return BrandHeroScreen( + heroTitle: "recovering.recovery_main_header".tr(), + heroSubtitle: "recovering.fallback_select_description".tr(), + hasBackButton: true, + hasFlashButton: false, + children: [ + BrandCards.outlined( + child: ListTile( + title: Text( + "recovering.fallback_select_token_copy".tr(), + style: Theme.of(context).textTheme.titleMedium, + ), + leading: Icon(Icons.vpn_key), + onTap: () => Navigator.of(context) + .push(materialRoute(RecoverByOldTokenInstruction( + instructionFilename: 'how_fallback_old', + ))), + ), + ), + SizedBox(height: 16), + BrandCards.outlined( + child: ListTile( + title: Text( + "recovering.fallback_select_root_ssh".tr(), + style: Theme.of(context).textTheme.titleMedium, + ), + leading: Icon(Icons.terminal), + onTap: () => Navigator.of(context) + .push(materialRoute(RecoverByOldTokenInstruction( + instructionFilename: 'how_fallback_ssh', + ))), + ), + ), + SizedBox(height: 16), + BrandCards.outlined( + child: ListTile( + title: Text( + "recovering.fallback_select_provider_console".tr(), + style: Theme.of(context).textTheme.titleMedium, + ), + subtitle: Text( + "recovering.fallback_select_provider_console_hint".tr(), + style: Theme.of(context).textTheme.bodyMedium, + ), + leading: Icon(Icons.web), + onTap: () => Navigator.of(context) + .push(materialRoute(RecoverByOldTokenInstruction( + instructionFilename: 'how_fallback_terminal', + ))), + ), + ), + ], + ); + } +} diff --git a/lib/ui/pages/setup/recovering/recovery_routing.dart b/lib/ui/pages/setup/recovering/recovery_routing.dart new file mode 100644 index 00000000..5db32eef --- /dev/null +++ b/lib/ui/pages/setup/recovering/recovery_routing.dart @@ -0,0 +1,109 @@ +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/FilledButton.dart'; +import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; +import 'package:selfprivacy/ui/pages/setup/recovering/recover_by_recovery_key.dart'; +import 'package:selfprivacy/ui/pages/setup/recovering/recover_by_new_device_key.dart'; +import 'package:selfprivacy/ui/pages/setup/recovering/recovery_method_select.dart'; + +class RecoveryRouting extends StatelessWidget { + @override + Widget build(BuildContext context) { + var serverInstallation = context.watch(); + + StatelessWidget currentPage = SelectDomainToRecover(); + + if (serverInstallation is ServerInstallationRecovery) { + final state = (serverInstallation as ServerInstallationRecovery); + switch (state.currentStep) { + case RecoveryStep.Selecting: + if (state.recoveryCapabilities != ServerRecoveryCapabilities.none) + currentPage = RecoveryMethodSelect(); + break; + case RecoveryStep.RecoveryKey: + currentPage = RecoverByRecoveryKey(); + break; + case RecoveryStep.NewDeviceKey: + currentPage = RecoverByNewDeviceKeyInstruction(); + break; + case RecoveryStep.OldToken: + break; + case RecoveryStep.HetznerToken: + break; + case RecoveryStep.CloudflareToken: + break; + case RecoveryStep.BackblazeToken: + break; + } + } + + return AnimatedSwitcher( + duration: Duration(milliseconds: 300), + child: currentPage, + ); + } +} + +class SelectDomainToRecover extends StatelessWidget { + @override + Widget build(BuildContext context) { + var serverInstallation = context.watch(); + + return BlocProvider( + create: (context) => RecoveryDomainFormCubit( + serverInstallation, FieldCubitFactory(context)), + child: Builder( + builder: (context) { + var formCubitState = context.watch().state; + + return BlocListener( + listener: (context, state) { + if (state is ServerInstallationRecovery) { + if (state.currentStep == RecoveryStep.Selecting) { + if (state.recoveryCapabilities == + ServerRecoveryCapabilities.none) { + context + .read() + .setCustomError("recovering.domain_recover_error".tr()); + } + } + } + }, + child: BrandHeroScreen( + heroTitle: "recovering.recovery_main_header".tr(), + heroSubtitle: "recovering.domain_recovery_description".tr(), + hasBackButton: true, + hasFlashButton: false, + onBackButtonPressed: + serverInstallation is ServerInstallationRecovery + ? () => serverInstallation.clearAppConfig() + : null, + children: [ + CubitFormTextField( + formFieldCubit: + context.read().serverDomainField, + decoration: InputDecoration( + border: OutlineInputBorder(), + labelText: "recovering.domain_recover_placeholder".tr(), + ), + ), + SizedBox(height: 16), + FilledButton( + title: "more.continue".tr(), + onPressed: formCubitState.isSubmitting + ? null + : () => + context.read().trySubmit(), + ) + ], + ), + ); + }, + ), + ); + } +} From eaa1ba143c799d16413557fa924d7349ec16ed28 Mon Sep 17 00:00:00 2001 From: NaiJi Date: Thu, 19 May 2022 20:43:25 +0300 Subject: [PATCH 24/52] Implement pages for server confirmation on restoring access Co-authored-by: Inex Code --- assets/translations/en.json | 11 ++- lib/logic/api_maps/hetzner.dart | 23 +++--- .../initializing/backblaze_form_cubit.dart | 6 +- .../setup/initializing/domain_cloudflare.dart | 6 +- .../initializing/hetzner_form_cubit.dart | 6 +- .../initializing/root_user_form_cubit.dart | 6 +- .../server_installation_cubit.dart | 13 ++++ .../server_installation_state.dart | 1 + .../models/json/hetzner_server_info.dart | 28 ++++++++ .../models/json/hetzner_server_info.g.dart | 14 ++++ .../components/brand_cards/brand_cards.dart | 24 +++++++ lib/ui/pages/setup/initializing.dart | 4 +- .../recovering/recover_by_old_token.dart | 5 +- .../recovering/recovery_confirm_server.dart | 62 ++++++++++++++++ .../recovery_hentzner_connected.dart | 70 +++++++++++++++++++ .../setup/recovering/recovery_routing.dart | 4 ++ 16 files changed, 252 insertions(+), 31 deletions(-) create mode 100644 lib/ui/pages/setup/recovering/recovery_confirm_server.dart create mode 100644 lib/ui/pages/setup/recovering/recovery_hentzner_connected.dart diff --git a/assets/translations/en.json b/assets/translations/en.json index 88889d8d..847fb35e 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -274,7 +274,6 @@ "15": "Server created. DNS checks and server boot in progress...", "16": "Until the next check: ", "17": "Check", - "18": "How to obtain Hetzner API Token", "19": "1 Go via this link ", "20": "\n", "21": "One more restart to apply your security certificates.", @@ -301,7 +300,15 @@ "fallback_select_token_copy": "Copy of auth token from other version of the application.", "fallback_select_root_ssh": "Root SSH access to the server.", "fallback_select_provider_console": "Access to the server console of my prodiver.", - "fallback_select_provider_console_hint": "For example: Hetzner." + "fallback_select_provider_console_hint": "For example: Hetzner.", + "hetzner_connected": "Connect to Hetzner", + "hetzner_connected_description": "Communication established. Enter Hetzner token with access to {}:", + "hetzner_connected_placeholder": "Hetzner token", + "confirm_server": "Confirm server", + "confirm_server_description": "Found your server! Confirm it is correct.", + "confirm_server_accept": "Yes! That's it", + "confirm_server_decline": "Choose a different server" + }, "modals": { "_comment": "messages in modals", diff --git a/lib/logic/api_maps/hetzner.dart b/lib/logic/api_maps/hetzner.dart index 2256f9e5..cbcacf80 100644 --- a/lib/logic/api_maps/hetzner.dart +++ b/lib/logic/api_maps/hetzner.dart @@ -55,19 +55,6 @@ class HetznerApi extends ApiMap { } } - Future isFreeToCreate() async { - var client = await getClient(); - - Response serversReponse = await client.get('/servers'); - List servers = serversReponse.data['servers']; - var server = servers.firstWhere( - (el) => el['name'] == 'selfprivacy-server', - orElse: null, - ); - client.close(); - return server == null; - } - Future createVolume() async { var client = await getClient(); Response dbCreateResponse = await client.post( @@ -237,6 +224,16 @@ class HetznerApi extends ApiMap { return HetznerServerInfo.fromJson(response.data!['server']); } + Future> getServers() async { + var client = await getClient(); + Response response = await client.get('/servers'); + close(client); + + return (response.data!['servers'] as List) + .map((e) => HetznerServerInfo.fromJson(e)) + .toList(); + } + Future createReverseDns({ required String ip4, required String domainName, diff --git a/lib/logic/cubit/forms/setup/initializing/backblaze_form_cubit.dart b/lib/logic/cubit/forms/setup/initializing/backblaze_form_cubit.dart index f3554ef4..fc9062e7 100644 --- a/lib/logic/cubit/forms/setup/initializing/backblaze_form_cubit.dart +++ b/lib/logic/cubit/forms/setup/initializing/backblaze_form_cubit.dart @@ -6,7 +6,7 @@ import 'package:selfprivacy/logic/models/hive/backblaze_credential.dart'; import 'package:easy_localization/easy_localization.dart'; class BackblazeFormCubit extends FormCubit { - BackblazeFormCubit(this.serverSetupCubit) { + BackblazeFormCubit(this.serverInstallationCubit) { //var regExp = RegExp(r"\s+|[-!$%^&*()@+|~=`{}\[\]:<>?,.\/]"); keyId = FieldCubit( initalValue: '', @@ -27,13 +27,13 @@ class BackblazeFormCubit extends FormCubit { @override FutureOr onSubmit() async { - serverSetupCubit.setBackblazeKey( + serverInstallationCubit.setBackblazeKey( keyId.state.value, applicationKey.state.value, ); } - final ServerInstallationCubit serverSetupCubit; + final ServerInstallationCubit serverInstallationCubit; late final FieldCubit keyId; late final FieldCubit applicationKey; diff --git a/lib/logic/cubit/forms/setup/initializing/domain_cloudflare.dart b/lib/logic/cubit/forms/setup/initializing/domain_cloudflare.dart index 07c46c74..fe595927 100644 --- a/lib/logic/cubit/forms/setup/initializing/domain_cloudflare.dart +++ b/lib/logic/cubit/forms/setup/initializing/domain_cloudflare.dart @@ -4,9 +4,9 @@ import 'package:selfprivacy/logic/cubit/server_installation/server_installation_ import 'package:selfprivacy/logic/models/hive/server_domain.dart'; class DomainSetupCubit extends Cubit { - DomainSetupCubit(this.serverSetupCubit) : super(Initial()); + DomainSetupCubit(this.serverInstallationCubit) : super(Initial()); - final ServerInstallationCubit serverSetupCubit; + final ServerInstallationCubit serverInstallationCubit; Future load() async { emit(Loading(LoadingTypes.loadingDomain)); @@ -42,7 +42,7 @@ class DomainSetupCubit extends Cubit { provider: DnsProvider.Cloudflare, ); - serverSetupCubit.setDomain(domain); + serverInstallationCubit.setDomain(domain); emit(DomainSet()); } } diff --git a/lib/logic/cubit/forms/setup/initializing/hetzner_form_cubit.dart b/lib/logic/cubit/forms/setup/initializing/hetzner_form_cubit.dart index 6871942e..0ac97e84 100644 --- a/lib/logic/cubit/forms/setup/initializing/hetzner_form_cubit.dart +++ b/lib/logic/cubit/forms/setup/initializing/hetzner_form_cubit.dart @@ -7,7 +7,7 @@ import 'package:selfprivacy/logic/cubit/server_installation/server_installation_ import 'package:selfprivacy/logic/cubit/forms/validations/validations.dart'; class HetznerFormCubit extends FormCubit { - HetznerFormCubit(this.serverSetupCubit) { + HetznerFormCubit(this.serverInstallationCubit) { var regExp = RegExp(r"\s+|[-!$%^&*()@+|~=`{}\[\]:<>?,.\/]"); apiKey = FieldCubit( initalValue: '', @@ -24,10 +24,10 @@ class HetznerFormCubit extends FormCubit { @override FutureOr onSubmit() async { - serverSetupCubit.setHetznerKey(apiKey.state.value); + serverInstallationCubit.setHetznerKey(apiKey.state.value); } - final ServerInstallationCubit serverSetupCubit; + final ServerInstallationCubit serverInstallationCubit; late final FieldCubit apiKey; diff --git a/lib/logic/cubit/forms/setup/initializing/root_user_form_cubit.dart b/lib/logic/cubit/forms/setup/initializing/root_user_form_cubit.dart index 81ccd88d..6e3e5c3d 100644 --- a/lib/logic/cubit/forms/setup/initializing/root_user_form_cubit.dart +++ b/lib/logic/cubit/forms/setup/initializing/root_user_form_cubit.dart @@ -7,7 +7,7 @@ import 'package:selfprivacy/logic/models/hive/user.dart'; class RootUserFormCubit extends FormCubit { RootUserFormCubit( - this.serverSetupCubit, final FieldCubitFactory fieldFactory) { + this.serverInstallationCubit, final FieldCubitFactory fieldFactory) { userName = fieldFactory.createUserLoginField(); password = fieldFactory.createUserPasswordField(); @@ -22,10 +22,10 @@ class RootUserFormCubit extends FormCubit { login: userName.state.value, password: password.state.value, ); - serverSetupCubit.setRootUser(user); + serverInstallationCubit.setRootUser(user); } - final ServerInstallationCubit serverSetupCubit; + final ServerInstallationCubit serverInstallationCubit; late final FieldCubit userName; late final FieldCubit password; diff --git a/lib/logic/cubit/server_installation/server_installation_cubit.dart b/lib/logic/cubit/server_installation/server_installation_cubit.dart index 33d360fe..7b99298d 100644 --- a/lib/logic/cubit/server_installation/server_installation_cubit.dart +++ b/lib/logic/cubit/server_installation/server_installation_cubit.dart @@ -47,6 +47,14 @@ class ServerInstallationCubit extends Cubit { void setHetznerKey(String hetznerKey) async { await repository.saveHetznerKey(hetznerKey); + + if (state is ServerInstallationRecovery) { + emit((state as ServerInstallationRecovery).copyWith( + hetznerKey: hetznerKey, + currentStep: RecoveryStep.ServerSelection, + )); + } + emit((state as ServerInstallationNotFinished) .copyWith(hetznerKey: hetznerKey)); } @@ -318,6 +326,11 @@ class ServerInstallationCubit extends Cubit { currentStep: RecoveryStep.Selecting, )); break; + case RecoveryStep.ServerSelection: + emit(dataState.copyWith( + currentStep: RecoveryStep.HetznerToken, + )); + break; // We won't revert steps after client is authorized default: break; diff --git a/lib/logic/cubit/server_installation/server_installation_state.dart b/lib/logic/cubit/server_installation/server_installation_state.dart index a318dd18..b376914d 100644 --- a/lib/logic/cubit/server_installation/server_installation_state.dart +++ b/lib/logic/cubit/server_installation/server_installation_state.dart @@ -261,6 +261,7 @@ enum RecoveryStep { NewDeviceKey, OldToken, HetznerToken, + ServerSelection, CloudflareToken, BackblazeToken, } diff --git a/lib/logic/models/json/hetzner_server_info.dart b/lib/logic/models/json/hetzner_server_info.dart index 98af1c3e..42763559 100644 --- a/lib/logic/models/json/hetzner_server_info.dart +++ b/lib/logic/models/json/hetzner_server_info.dart @@ -15,6 +15,9 @@ class HetznerServerInfo { @JsonKey(name: 'datacenter', fromJson: HetznerServerInfo.locationFromJson) final HetznerLocation location; + @JsonKey(name: 'public_net') + final HetznerPublicNetInfo publicNet; + static HetznerLocation locationFromJson(Map json) => HetznerLocation.fromJson(json['location']); @@ -28,9 +31,34 @@ class HetznerServerInfo { this.created, this.serverType, this.location, + this.publicNet, ); } +@JsonSerializable() +class HetznerPublicNetInfo { + final HetznerIp4 ip4; + + static HetznerPublicNetInfo fromJson(Map json) => + _$HetznerPublicNetInfoFromJson(json); + + HetznerPublicNetInfo(this.ip4); +} + +@JsonSerializable() +class HetznerIp4 { + final bool blocked; + @JsonKey(name: 'dns_ptr') + final String reverseDns; + final int id; + final String ip; + + static HetznerIp4 fromJson(Map json) => + _$HetznerIp4FromJson(json); + + HetznerIp4(this.id, this.ip, this.blocked, this.reverseDns); +} + enum ServerStatus { running, initializing, diff --git a/lib/logic/models/json/hetzner_server_info.g.dart b/lib/logic/models/json/hetzner_server_info.g.dart index 73e6be68..a1b3f130 100644 --- a/lib/logic/models/json/hetzner_server_info.g.dart +++ b/lib/logic/models/json/hetzner_server_info.g.dart @@ -15,6 +15,7 @@ HetznerServerInfo _$HetznerServerInfoFromJson(Map json) => HetznerServerTypeInfo.fromJson( json['server_type'] as Map), HetznerServerInfo.locationFromJson(json['datacenter'] as Map), + HetznerPublicNetInfo.fromJson(json['public_net'] as Map), ); const _$ServerStatusEnumMap = { @@ -29,6 +30,19 @@ const _$ServerStatusEnumMap = { ServerStatus.unknown: 'unknown', }; +HetznerPublicNetInfo _$HetznerPublicNetInfoFromJson( + Map json) => + HetznerPublicNetInfo( + HetznerIp4.fromJson(json['ip4'] as Map), + ); + +HetznerIp4 _$HetznerIp4FromJson(Map json) => HetznerIp4( + json['id'] as int, + json['ip'] as String, + json['blocked'] as bool, + json['dns_ptr'] as String, + ); + HetznerServerTypeInfo _$HetznerServerTypeInfoFromJson( Map json) => HetznerServerTypeInfo( diff --git a/lib/ui/components/brand_cards/brand_cards.dart b/lib/ui/components/brand_cards/brand_cards.dart index 67ad09af..777663cf 100644 --- a/lib/ui/components/brand_cards/brand_cards.dart +++ b/lib/ui/components/brand_cards/brand_cards.dart @@ -23,6 +23,9 @@ class BrandCards { static Widget outlined({required Widget child}) => _OutlinedCard( child: child, ); + static Widget filled({required Widget child}) => _FilledCard( + child: child, + ); } class _BrandCard extends StatelessWidget { @@ -78,6 +81,27 @@ class _OutlinedCard extends StatelessWidget { } } +class _FilledCard extends StatelessWidget { + const _FilledCard({ + Key? key, + required this.child, + }) : super(key: key); + + final Widget child; + @override + Widget build(BuildContext context) { + return Card( + elevation: 0.0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(12)), + ), + clipBehavior: Clip.antiAlias, + child: child, + color: Theme.of(context).colorScheme.surfaceVariant, + ); + } +} + final bigShadow = [ BoxShadow( offset: Offset(0, 4), diff --git a/lib/ui/pages/setup/initializing.dart b/lib/ui/pages/setup/initializing.dart index 81630b63..fc01e373 100644 --- a/lib/ui/pages/setup/initializing.dart +++ b/lib/ui/pages/setup/initializing.dart @@ -124,9 +124,9 @@ class InitializingPage extends StatelessWidget { ); } - Widget _stepHetzner(ServerInstallationCubit initializingCubit) { + Widget _stepHetzner(ServerInstallationCubit serverInstallationCubit) { return BlocProvider( - create: (context) => HetznerFormCubit(initializingCubit), + create: (context) => HetznerFormCubit(serverInstallationCubit), child: Builder(builder: (context) { var formCubitState = context.watch().state; return Column( 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 522494fd..e8a7e12b 100644 --- a/lib/ui/pages/setup/recovering/recover_by_old_token.dart +++ b/lib/ui/pages/setup/recovering/recover_by_old_token.dart @@ -27,8 +27,9 @@ class RecoverByOldTokenInstruction extends StatelessWidget { SizedBox(height: 16), FilledButton( title: "recovering.method_device_button".tr(), - onPressed: () => - Navigator.of(context).push(materialRoute(RecoverByOldToken())), + onPressed: () => context + .read() + .selectRecoveryMethod(ServerRecoveryMethods.oldToken), ) ], ); diff --git a/lib/ui/pages/setup/recovering/recovery_confirm_server.dart b/lib/ui/pages/setup/recovering/recovery_confirm_server.dart new file mode 100644 index 00000000..262dfa6a --- /dev/null +++ b/lib/ui/pages/setup/recovering/recovery_confirm_server.dart @@ -0,0 +1,62 @@ +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/setup/recovering/recovery_domain_form_cubit.dart'; +import 'package:selfprivacy/ui/components/brand_button/FilledButton.dart'; +import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; + +class RecoveryConfirmServer extends StatelessWidget { + @override + Widget build(BuildContext context) { + var serverInstallation = context.watch(); + + return Builder( + builder: (context) { + var formCubitState = context.watch().state; + + return BlocListener( + listener: (context, state) { + if (state is ServerInstallationRecovery) { + if (state.currentStep == RecoveryStep.Selecting) { + if (state.recoveryCapabilities == + ServerRecoveryCapabilities.none) { + context + .read() + .setCustomError("recovering.domain_recover_error".tr()); + } + } + } + }, + child: BrandHeroScreen( + heroTitle: "recovering.recovery_main_header".tr(), + heroSubtitle: "recovering.domain_recovery_description".tr(), + hasBackButton: true, + hasFlashButton: false, + onBackButtonPressed: + serverInstallation is ServerInstallationRecovery + ? () => serverInstallation.clearAppConfig() + : null, + children: [ + CubitFormTextField( + formFieldCubit: + context.read().serverDomainField, + decoration: InputDecoration( + border: OutlineInputBorder(), + labelText: "recovering.domain_recover_placeholder".tr(), + ), + ), + SizedBox(height: 16), + FilledButton( + title: "more.continue".tr(), + onPressed: formCubitState.isSubmitting + ? null + : () => context.read().trySubmit(), + ) + ], + ), + ); + }, + ); + } +} diff --git a/lib/ui/pages/setup/recovering/recovery_hentzner_connected.dart b/lib/ui/pages/setup/recovering/recovery_hentzner_connected.dart new file mode 100644 index 00000000..bfd8fd76 --- /dev/null +++ b/lib/ui/pages/setup/recovering/recovery_hentzner_connected.dart @@ -0,0 +1,70 @@ +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/hetzner_form_cubit.dart'; +import 'package:selfprivacy/ui/components/brand_bottom_sheet/brand_bottom_sheet.dart'; +import 'package:selfprivacy/ui/components/brand_button/FilledButton.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: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 RecoveryHetznerConnected extends StatelessWidget { + @override + Widget build(BuildContext context) { + var appConfig = context.watch(); + + return BlocProvider( + create: (context) => HetznerFormCubit(appConfig), + child: Builder( + builder: (context) { + var formCubitState = context.watch().state; + + return BrandHeroScreen( + heroTitle: "recovering.hetzner_connected".tr(), + heroSubtitle: "recovering.hetzner_connected_description".tr(), + hasBackButton: true, + hasFlashButton: false, + children: [ + CubitFormTextField( + formFieldCubit: context.read().apiKey, + decoration: InputDecoration( + border: OutlineInputBorder(), + labelText: "recovering.hetzner_connected_placeholder".tr(), + ), + ), + SizedBox(height: 16), + FilledButton( + title: "more.continue".tr(), + onPressed: formCubitState.isSubmitting + ? null + : () => context.read().trySubmit(), + ), + SizedBox(height: 16), + BrandButton.text( + title: 'initializing.how'.tr(), + onPressed: () => showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (BuildContext context) { + return BrandBottomSheet( + isExpended: true, + child: Padding( + padding: paddingH15V0, + child: BrandMarkdown( + fileName: 'how_hetzner', + ), + ), + ); + }, + ), + ), + ], + ); + }, + ), + ); + } +} diff --git a/lib/ui/pages/setup/recovering/recovery_routing.dart b/lib/ui/pages/setup/recovering/recovery_routing.dart index 5db32eef..7237ed97 100644 --- a/lib/ui/pages/setup/recovering/recovery_routing.dart +++ b/lib/ui/pages/setup/recovering/recovery_routing.dart @@ -6,6 +6,7 @@ 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/FilledButton.dart'; import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.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'; import 'package:selfprivacy/ui/pages/setup/recovering/recover_by_new_device_key.dart'; import 'package:selfprivacy/ui/pages/setup/recovering/recovery_method_select.dart'; @@ -31,9 +32,12 @@ class RecoveryRouting extends StatelessWidget { currentPage = RecoverByNewDeviceKeyInstruction(); break; case RecoveryStep.OldToken: + currentPage = RecoverByOldToken(); break; case RecoveryStep.HetznerToken: break; + case RecoveryStep.ServerSelection: + break; case RecoveryStep.CloudflareToken: break; case RecoveryStep.BackblazeToken: From eddeac57d676ff944de5a4ebfef9a8dd41619c45 Mon Sep 17 00:00:00 2001 From: NaiJi Date: Sat, 21 May 2022 01:56:50 +0300 Subject: [PATCH 25/52] Implement server selection pages Co-authored-by: Inex Code --- assets/translations/en.json | 6 +- assets/translations/ru.json | 13 +- lib/config/hive_config.dart | 3 + lib/logic/api_maps/cloudflare.dart | 36 ++- lib/logic/api_maps/server.dart | 5 +- .../forms/factories/field_cubit_factory.dart | 2 +- .../recovery_device_form_cubit.dart | 14 +- .../recovery_domain_form_cubit.dart | 9 +- .../server_installation_cubit.dart | 79 ++++++ .../server_installation_repository.dart | 49 +++- .../models/json/hetzner_server_info.dart | 6 +- .../models/json/hetzner_server_info.g.dart | 3 +- lib/logic/models/server_basic_info.dart | 55 +++++ lib/ui/pages/setup/initializing.dart | 186 +++++++------- .../recovering/recover_by_new_device_key.dart | 7 +- .../recovering/recover_by_old_token.dart | 8 +- .../recovering/recover_by_recovery_key.dart | 7 +- .../recovering/recovery_confirm_server.dart | 226 ++++++++++++++---- .../recovery_hentzner_connected.dart | 4 +- .../setup/recovering/recovery_routing.dart | 14 +- 20 files changed, 545 insertions(+), 187 deletions(-) create mode 100644 lib/logic/models/server_basic_info.dart diff --git a/assets/translations/en.json b/assets/translations/en.json index 847fb35e..58b844a8 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -300,6 +300,7 @@ "fallback_select_token_copy": "Copy of auth token from other version of the application.", "fallback_select_root_ssh": "Root SSH access to the server.", "fallback_select_provider_console": "Access to the server console of my prodiver.", + "authorization_failed": "Couldn't log in with this key", "fallback_select_provider_console_hint": "For example: Hetzner.", "hetzner_connected": "Connect to Hetzner", "hetzner_connected_description": "Communication established. Enter Hetzner token with access to {}:", @@ -307,7 +308,10 @@ "confirm_server": "Confirm server", "confirm_server_description": "Found your server! Confirm it is correct.", "confirm_server_accept": "Yes! That's it", - "confirm_server_decline": "Choose a different server" + "confirm_server_decline": "Choose a different server", + "choose_server": "Choose your server", + "choose_server_description": "We couldn't figure out which server your are trying to connect to.", + "no_servers": "There is no available servers on your account." }, "modals": { diff --git a/assets/translations/ru.json b/assets/translations/ru.json index 6a898516..c2fc34bc 100644 --- a/assets/translations/ru.json +++ b/assets/translations/ru.json @@ -302,7 +302,18 @@ "fallback_select_token_copy": "Копия токена авторизации из другой версии приложения.", "fallback_select_root_ssh": "Root доступ к серверу по SSH.", "fallback_select_provider_console": "Доступ к консоли хостинга.", - "fallback_select_provider_console_hint": "Например, Hetzner." + "authorization_failed": "Не удалось войти с этим ключом", + "fallback_select_provider_console_hint": "Например, Hetzner.", + "hetzner_connected": "Подключение к Hetzner", + "hetzner_connected_description": "Связь с сервером установлена. Введите токен Hetzner с доступом к {}:", + "hetzner_connected_placeholder": "Hetzner токен", + "confirm_server": "Подтвердите сервер", + "confirm_server_description": "Нашли сервер! Подтвердите, что это он:", + "confirm_server_accept": "Да, это он", + "confirm_server_decline": "Выбрать другой сервер", + "choose_server": "Выберите сервер", + "choose_server_description": "Не удалось определить, с каким сервером вы устанавливаете связь.", + "no_servers": "На вашем аккаунте нет доступных серверов." }, "modals": { "_comment": "messages in modals", diff --git a/lib/config/hive_config.dart b/lib/config/hive_config.dart index 094c83c8..360c5e55 100644 --- a/lib/config/hive_config.dart +++ b/lib/config/hive_config.dart @@ -19,6 +19,9 @@ class HiveConfig { Hive.registerAdapter(BackblazeBucketAdapter()); Hive.registerAdapter(ServerVolumeAdapter()); + Hive.registerAdapter(DnsProviderAdapter()); + Hive.registerAdapter(ServerProviderAdapter()); + await Hive.openBox(BNames.appSettingsBox); var cipher = HiveAesCipher( diff --git a/lib/logic/api_maps/cloudflare.dart b/lib/logic/api_maps/cloudflare.dart index c0d7334b..9b950546 100644 --- a/lib/logic/api_maps/cloudflare.dart +++ b/lib/logic/api_maps/cloudflare.dart @@ -6,8 +6,24 @@ import 'package:selfprivacy/logic/api_maps/api_map.dart'; import 'package:selfprivacy/logic/models/hive/server_domain.dart'; import 'package:selfprivacy/logic/models/json/dns_records.dart'; +class DomainNotFoundException implements Exception { + final String message; + DomainNotFoundException(this.message); +} + class CloudflareApi extends ApiMap { - CloudflareApi({this.hasLogger = false, this.isWithToken = true}); + @override + final bool hasLogger; + @override + final bool isWithToken; + + final String? customToken; + + CloudflareApi({ + this.hasLogger = false, + this.isWithToken = true, + this.customToken, + }); BaseOptions get options { var options = BaseOptions(baseUrl: rootAddress); @@ -17,6 +33,10 @@ class CloudflareApi extends ApiMap { options.headers = {'Authorization': 'Bearer $token'}; } + if (customToken != null) { + options.headers = {'Authorization': 'Bearer $customToken'}; + } + if (validateStatus != null) { options.validateStatus = validateStatus!; } @@ -58,7 +78,11 @@ class CloudflareApi extends ApiMap { close(client); - return response.data['result'][0]['id']; + if (response.data['result'].isEmpty) { + throw DomainNotFoundException('No domains found'); + } else { + return response.data['result'][0]['id']; + } } Future removeSimilarRecords({ @@ -209,7 +233,7 @@ class CloudflareApi extends ApiMap { } Future> domainList() async { - var url = '$rootAddress/zones?per_page=50'; + var url = '$rootAddress/zones'; var client = await getClient(); var response = await client.get( @@ -222,10 +246,4 @@ class CloudflareApi extends ApiMap { .map((el) => el['name'] as String) .toList(); } - - @override - final bool hasLogger; - - @override - final bool isWithToken; } diff --git a/lib/logic/api_maps/server.dart b/lib/logic/api_maps/server.dart index 21bceee9..92bd6932 100644 --- a/lib/logic/api_maps/server.dart +++ b/lib/logic/api_maps/server.dart @@ -31,7 +31,9 @@ class ApiResponse { } class ServerApi extends ApiMap { + @override bool hasLogger; + @override bool isWithToken; String? overrideDomain; String? customToken; @@ -734,7 +736,8 @@ class ServerApi extends ApiMap { final int code = response.statusCode ?? HttpStatus.internalServerError; return ApiResponse( - statusCode: code, data: response.data != null ? response.data : ''); + statusCode: code, + data: response.data["token"] != null ? response.data["token"] : ''); } Future> createDeviceToken() async { diff --git a/lib/logic/cubit/forms/factories/field_cubit_factory.dart b/lib/logic/cubit/forms/factories/field_cubit_factory.dart index ac75c2c9..a86f6445 100644 --- a/lib/logic/cubit/forms/factories/field_cubit_factory.dart +++ b/lib/logic/cubit/forms/factories/field_cubit_factory.dart @@ -52,7 +52,7 @@ class FieldCubitFactory { ); } - FieldCubit createServerDomainField() { + FieldCubit createRequiredStringField() { return FieldCubit( initalValue: '', validations: [ diff --git a/lib/logic/cubit/forms/setup/recovering/recovery_device_form_cubit.dart b/lib/logic/cubit/forms/setup/recovering/recovery_device_form_cubit.dart index 4486bf14..ddc35426 100644 --- a/lib/logic/cubit/forms/setup/recovering/recovery_device_form_cubit.dart +++ b/lib/logic/cubit/forms/setup/recovering/recovery_device_form_cubit.dart @@ -5,21 +5,19 @@ import 'package:selfprivacy/logic/cubit/server_installation/server_installation_ import 'package:selfprivacy/logic/cubit/forms/factories/field_cubit_factory.dart'; class RecoveryDeviceFormCubit extends FormCubit { - RecoveryDeviceFormCubit( - this.initializingCubit, final FieldCubitFactory fieldFactory) { - tokenField = fieldFactory.createServerDomainField(); + RecoveryDeviceFormCubit(this.installationCubit, + final FieldCubitFactory fieldFactory, this.recoveryMethod) { + tokenField = fieldFactory.createRequiredStringField(); super.addFields([tokenField]); } @override FutureOr onSubmit() async { - // initializingCubit.setDomain(ServerDomain( - // domainName: serverDomainField.state.value, - // provider: DnsProvider.Unknown, - // zoneId: "")); + installationCubit.tryToRecover(tokenField.state.value, recoveryMethod); } - final ServerInstallationCubit initializingCubit; + final ServerInstallationCubit installationCubit; late final FieldCubit tokenField; + final ServerRecoveryMethods recoveryMethod; } diff --git a/lib/logic/cubit/forms/setup/recovering/recovery_domain_form_cubit.dart b/lib/logic/cubit/forms/setup/recovering/recovery_domain_form_cubit.dart index 921cf996..c67f3bee 100644 --- a/lib/logic/cubit/forms/setup/recovering/recovery_domain_form_cubit.dart +++ b/lib/logic/cubit/forms/setup/recovering/recovery_domain_form_cubit.dart @@ -5,22 +5,19 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:selfprivacy/logic/api_maps/server.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/models/hive/server_domain.dart'; class RecoveryDomainFormCubit extends FormCubit { RecoveryDomainFormCubit( this.initializingCubit, final FieldCubitFactory fieldFactory) { - serverDomainField = fieldFactory.createServerDomainField(); + serverDomainField = fieldFactory.createRequiredStringField(); super.addFields([serverDomainField]); } @override FutureOr onSubmit() async { - initializingCubit.setDomain(ServerDomain( - domainName: serverDomainField.state.value, - provider: DnsProvider.Unknown, - zoneId: "")); + initializingCubit + .submitDomainForAccessRecovery(serverDomainField.state.value); } @override diff --git a/lib/logic/cubit/server_installation/server_installation_cubit.dart b/lib/logic/cubit/server_installation/server_installation_cubit.dart index 7b99298d..c2849903 100644 --- a/lib/logic/cubit/server_installation/server_installation_cubit.dart +++ b/lib/logic/cubit/server_installation/server_installation_cubit.dart @@ -1,11 +1,14 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:equatable/equatable.dart'; +import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/logic/models/hive/backblaze_credential.dart'; import 'package:selfprivacy/logic/models/hive/server_details.dart'; import 'package:selfprivacy/logic/models/hive/server_domain.dart'; import 'package:selfprivacy/logic/models/hive/user.dart'; +import 'package:selfprivacy/logic/models/server_basic_info.dart'; import '../server_installation/server_installation_repository.dart'; @@ -53,6 +56,7 @@ class ServerInstallationCubit extends Cubit { hetznerKey: hetznerKey, currentStep: RecoveryStep.ServerSelection, )); + return; } emit((state as ServerInstallationNotFinished) @@ -269,6 +273,8 @@ class ServerInstallationCubit extends Cubit { final recoveryCapabilities = await repository.getRecoveryCapabilities(serverDomain); + await repository.saveDomain(serverDomain); + emit(ServerInstallationRecovery( serverDomain: serverDomain, recoveryCapabilities: recoveryCapabilities, @@ -302,13 +308,18 @@ class ServerInstallationCubit extends Cubit { serverDomain, token, ); + await repository.saveServerDetails(serverDetails); emit(dataState.copyWith( serverDetails: serverDetails, currentStep: RecoveryStep.HetznerToken, )); } on ServerAuthorizationException { + getIt() + .showSnackBar('recovering.authorization_failed'.tr()); return; } on IpNotFoundException { + getIt() + .showSnackBar('recovering.domain_recover_error'.tr()); return; } } @@ -317,6 +328,7 @@ class ServerInstallationCubit extends Cubit { final dataState = this.state as ServerInstallationRecovery; switch (dataState.currentStep) { case RecoveryStep.Selecting: + repository.deleteDomain(); emit(ServerInstallationEmpty()); break; case RecoveryStep.RecoveryKey: @@ -327,6 +339,7 @@ class ServerInstallationCubit extends Cubit { )); break; case RecoveryStep.ServerSelection: + repository.deleteHetznerKey(); emit(dataState.copyWith( currentStep: RecoveryStep.HetznerToken, )); @@ -358,6 +371,72 @@ class ServerInstallationCubit extends Cubit { } } + Future> + getServersOnHetznerAccount() async { + final dataState = this.state as ServerInstallationRecovery; + final servers = await repository.getServersOnHetznerAccount(); + final validated = servers + .map((server) => ServerBasicInfoWithValidators.fromServerBasicInfo( + serverBasicInfo: server, + isIpValid: server.ip == dataState.serverDetails?.ip4, + isReverseDnsValid: + server.reverseDns == dataState.serverDomain?.domainName, + )); + return validated.toList(); + } + + Future setServerId(ServerBasicInfo server) async { + final dataState = this.state as ServerInstallationRecovery; + final serverDomain = dataState.serverDomain; + if (serverDomain == null) { + return; + } + final serverDetails = ServerHostingDetails( + ip4: server.ip, + id: server.id, + createTime: server.created, + volume: ServerVolume( + id: server.volumeId, + name: "recovered_volume", + ), + apiToken: dataState.serverDetails!.apiToken, + provider: ServerProvider.Hetzner, + ); + await repository.saveDomain(serverDomain); + await repository.saveServerDetails(serverDetails); + emit(dataState.copyWith( + serverDetails: serverDetails, + currentStep: RecoveryStep.CloudflareToken, + )); + } + + // Future setAndValidateCloudflareToken(String token) async { + // final dataState = this.state as ServerInstallationRecovery; + // final serverDomain = dataState.serverDomain; + // if (serverDomain == null) { + // return; + // } + // final domainId = await repository.getDomainId(serverDomain.domainName); + // } + + @override + void onChange(Change change) { + super.onChange(change); + print('================================'); + print('ServerInstallationState changed!'); + print('Current type: ${change.nextState.runtimeType}'); + print('Hetzner key: ${change.nextState.hetznerKey}'); + print('Cloudflare key: ${change.nextState.cloudFlareKey}'); + print('Domain: ${change.nextState.serverDomain}'); + print('BackblazeCredential: ${change.nextState.backblazeCredential}'); + if (change.nextState is ServerInstallationRecovery) { + print( + 'Recovery Step: ${(change.nextState as ServerInstallationRecovery).currentStep}'); + print( + 'Recovery Capabilities: ${(change.nextState as ServerInstallationRecovery).recoveryCapabilities}'); + } + } + void clearAppConfig() { closeTimer(); diff --git a/lib/logic/cubit/server_installation/server_installation_repository.dart b/lib/logic/cubit/server_installation/server_installation_repository.dart index 9a0a826c..af006c6b 100644 --- a/lib/logic/cubit/server_installation/server_installation_repository.dart +++ b/lib/logic/cubit/server_installation/server_installation_repository.dart @@ -18,6 +18,7 @@ import 'package:selfprivacy/logic/models/hive/server_domain.dart'; import 'package:selfprivacy/logic/models/hive/user.dart'; import 'package:selfprivacy/logic/models/json/device_token.dart'; import 'package:selfprivacy/logic/models/message.dart'; +import 'package:selfprivacy/logic/models/server_basic_info.dart'; import 'package:selfprivacy/ui/components/action_button/action_button.dart'; import 'package:selfprivacy/ui/components/brand_alert/brand_alert.dart'; @@ -100,10 +101,13 @@ class ServerInstallationRepository { ) { if (serverDetails != null) { if (hetznerToken != null) { - if (cloudflareToken != null) { - return RecoveryStep.BackblazeToken; + if (serverDetails.provider != ServerProvider.Unknown) { + if (serverDomain.provider != DnsProvider.Unknown) { + return RecoveryStep.BackblazeToken; + } + return RecoveryStep.CloudflareToken; } - return RecoveryStep.CloudflareToken; + return RecoveryStep.ServerSelection; } return RecoveryStep.HetznerToken; } @@ -123,6 +127,20 @@ class ServerInstallationRepository { return serverDetails; } + Future getDomainId(String token, String domain) async { + var cloudflareApi = CloudflareApi( + isWithToken: false, + customToken: token, + ); + + try { + final domainId = await cloudflareApi.getZoneId(domain); + return domainId; + } on DomainNotFoundException { + return null; + } + } + Future> isDnsAddressesMatch(String? domainName, String? ip4, Map? skippedMatches) async { var addresses = [ @@ -467,6 +485,21 @@ class ServerInstallationRepository { ); } + Future> getServersOnHetznerAccount() async { + var hetznerApi = HetznerApi(); + final servers = await hetznerApi.getServers(); + return servers + .map((server) => ServerBasicInfo( + id: server.id, + name: server.name, + ip: server.publicNet.ipv4.ip, + reverseDns: server.publicNet.ipv4.reverseDns, + created: server.created, + volumeId: server.volumes.isNotEmpty ? server.volumes[0] : 0, + )) + .toList(); + } + Future saveServerDetails(ServerHostingDetails serverDetails) async { await getIt().storeServerDetails(serverDetails); } @@ -476,6 +509,11 @@ class ServerInstallationRepository { await getIt().storeHetznerKey(key); } + Future deleteHetznerKey() async { + await box.delete(BNames.hetznerKey); + getIt().init(); + } + Future saveBackblazeKey(BackblazeCredential backblazeCredential) async { await getIt().storeBackblazeCredential(backblazeCredential); } @@ -488,6 +526,11 @@ class ServerInstallationRepository { await getIt().storeServerDomain(serverDomain); } + Future deleteDomain() async { + await box.delete(BNames.serverDomain); + getIt().init(); + } + Future saveIsServerStarted(bool value) async { await box.put(BNames.isServerStarted, value); } diff --git a/lib/logic/models/json/hetzner_server_info.dart b/lib/logic/models/json/hetzner_server_info.dart index 42763559..6e173181 100644 --- a/lib/logic/models/json/hetzner_server_info.dart +++ b/lib/logic/models/json/hetzner_server_info.dart @@ -8,6 +8,7 @@ class HetznerServerInfo { final String name; final ServerStatus status; final DateTime created; + final List volumes; @JsonKey(name: 'server_type') final HetznerServerTypeInfo serverType; @@ -32,17 +33,18 @@ class HetznerServerInfo { this.serverType, this.location, this.publicNet, + this.volumes, ); } @JsonSerializable() class HetznerPublicNetInfo { - final HetznerIp4 ip4; + final HetznerIp4 ipv4; static HetznerPublicNetInfo fromJson(Map json) => _$HetznerPublicNetInfoFromJson(json); - HetznerPublicNetInfo(this.ip4); + HetznerPublicNetInfo(this.ipv4); } @JsonSerializable() diff --git a/lib/logic/models/json/hetzner_server_info.g.dart b/lib/logic/models/json/hetzner_server_info.g.dart index a1b3f130..e8c21917 100644 --- a/lib/logic/models/json/hetzner_server_info.g.dart +++ b/lib/logic/models/json/hetzner_server_info.g.dart @@ -16,6 +16,7 @@ HetznerServerInfo _$HetznerServerInfoFromJson(Map json) => json['server_type'] as Map), HetznerServerInfo.locationFromJson(json['datacenter'] as Map), HetznerPublicNetInfo.fromJson(json['public_net'] as Map), + (json['volumes'] as List).map((e) => e as int).toList(), ); const _$ServerStatusEnumMap = { @@ -33,7 +34,7 @@ const _$ServerStatusEnumMap = { HetznerPublicNetInfo _$HetznerPublicNetInfoFromJson( Map json) => HetznerPublicNetInfo( - HetznerIp4.fromJson(json['ip4'] as Map), + HetznerIp4.fromJson(json['ipv4'] as Map), ); HetznerIp4 _$HetznerIp4FromJson(Map json) => HetznerIp4( diff --git a/lib/logic/models/server_basic_info.dart b/lib/logic/models/server_basic_info.dart new file mode 100644 index 00000000..fcd37ed4 --- /dev/null +++ b/lib/logic/models/server_basic_info.dart @@ -0,0 +1,55 @@ +class ServerBasicInfo { + final int id; + final String name; + final String reverseDns; + final String ip; + final DateTime created; + final int volumeId; + + ServerBasicInfo({ + required this.id, + required this.name, + required this.reverseDns, + required this.ip, + required this.created, + required this.volumeId, + }); +} + +class ServerBasicInfoWithValidators extends ServerBasicInfo { + final bool isIpValid; + final bool isReverseDnsValid; + + ServerBasicInfoWithValidators({ + required int id, + required String name, + required String reverseDns, + required String ip, + required DateTime created, + required int volumeId, + required this.isIpValid, + required this.isReverseDnsValid, + }) : super( + id: id, + name: name, + reverseDns: reverseDns, + ip: ip, + created: created, + volumeId: volumeId, + ); + + ServerBasicInfoWithValidators.fromServerBasicInfo({ + required ServerBasicInfo serverBasicInfo, + required isIpValid, + required isReverseDnsValid, + }) : this( + id: serverBasicInfo.id, + name: serverBasicInfo.name, + reverseDns: serverBasicInfo.reverseDns, + ip: serverBasicInfo.ip, + created: serverBasicInfo.created, + volumeId: serverBasicInfo.volumeId, + isIpValid: isIpValid, + isReverseDnsValid: isReverseDnsValid, + ); +} diff --git a/lib/ui/pages/setup/initializing.dart b/lib/ui/pages/setup/initializing.dart index fc01e373..e0971ae1 100644 --- a/lib/ui/pages/setup/initializing.dart +++ b/lib/ui/pages/setup/initializing.dart @@ -17,6 +17,8 @@ import 'package:selfprivacy/ui/components/brand_text/brand_text.dart'; import 'package:selfprivacy/ui/components/brand_timer/brand_timer.dart'; import 'package:selfprivacy/ui/components/progress_bar/progress_bar.dart'; import 'package:selfprivacy/ui/pages/rootRoute.dart'; +import 'package:selfprivacy/ui/pages/setup/recovering/recovery_confirm_server.dart'; +import 'package:selfprivacy/ui/pages/setup/recovering/recovery_hentzner_connected.dart'; import 'package:selfprivacy/ui/pages/setup/recovering/recovery_routing.dart'; import 'package:selfprivacy/ui/pages/setup/recovering/recovery_method_select.dart'; import 'package:selfprivacy/utils/route_transitions/basic.dart'; @@ -25,103 +27,105 @@ class InitializingPage extends StatelessWidget { @override Widget build(BuildContext context) { var cubit = context.watch(); - var actualInitializingPage = [ - () => _stepHetzner(cubit), - () => _stepCloudflare(cubit), - () => _stepBackblaze(cubit), - () => _stepDomain(cubit), - () => _stepUser(cubit), - () => _stepServer(cubit), - () => _stepCheck(cubit), - () => _stepCheck(cubit), - () => _stepCheck(cubit), - () => Container(child: Center(child: Text('initializing.finish'.tr()))) - ][cubit.state.progress.index](); - if (cubit is ServerInstallationRecovery) { + if (cubit.state is ServerInstallationRecovery) { return RecoveryRouting(); - } - return BlocListener( - listener: (context, state) { - if (cubit.state is ServerInstallationFinished) { - Navigator.of(context).pushReplacement(materialRoute(RootPage())); - } - }, - child: SafeArea( - child: Scaffold( - body: SingleChildScrollView( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Padding( - padding: paddingH15V0.copyWith(top: 10, bottom: 10), - child: cubit.state.isFullyInitilized - ? SizedBox( - height: 80, - ) - : ProgressBar( - steps: [ - 'Hetzner', - 'CloudFlare', - 'Backblaze', - 'Domain', - 'User', - 'Server', - '✅ Check', - ], - activeIndex: cubit.state.porgressBar, - ), - ), - _addCard( - AnimatedSwitcher( - duration: 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(RootPage()), - (predicate) => false, - ); - }, + } else { + var actualInitializingPage = [ + () => _stepHetzner(cubit), + () => _stepCloudflare(cubit), + () => _stepBackblaze(cubit), + () => _stepDomain(cubit), + () => _stepUser(cubit), + () => _stepServer(cubit), + () => _stepCheck(cubit), + () => _stepCheck(cubit), + () => _stepCheck(cubit), + () => Container(child: Center(child: Text('initializing.finish'.tr()))) + ][cubit.state.progress.index](); + + return BlocListener( + listener: (context, state) { + if (cubit.state is ServerInstallationFinished) { + Navigator.of(context).pushReplacement(materialRoute(RootPage())); + } + }, + child: SafeArea( + child: Scaffold( + body: SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: paddingH15V0.copyWith(top: 10, bottom: 10), + child: cubit.state.isFullyInitilized + ? SizedBox( + height: 80, + ) + : ProgressBar( + steps: [ + 'Hetzner', + 'CloudFlare', + 'Backblaze', + 'Domain', + 'User', + 'Server', + '✅ Check', + ], + activeIndex: cubit.state.porgressBar, ), - ), - (cubit.state is ServerInstallationFinished) - ? Container() - : Container( - alignment: Alignment.center, - child: BrandButton.text( - title: 'basis.connect_to_existing'.tr(), - onPressed: () { - Navigator.of(context).push( - materialRoute(RecoveryMethodSelect())); - }, - ), - ) - ], - )), - ], + ), + _addCard( + AnimatedSwitcher( + duration: 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(RootPage()), + (predicate) => false, + ); + }, + ), + ), + (cubit.state is ServerInstallationFinished) + ? Container() + : Container( + alignment: Alignment.center, + child: BrandButton.text( + title: 'basis.connect_to_existing'.tr(), + onPressed: () { + Navigator.of(context).push( + materialRoute(RecoveryRouting())); + }, + ), + ) + ], + )), + ], + ), ), ), ), - ), - ); + ); + } } Widget _stepHetzner(ServerInstallationCubit serverInstallationCubit) { 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 63f7e127..e93b4c4f 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 @@ -35,8 +35,11 @@ class RecoverByNewDeviceKeyInput extends StatelessWidget { var appConfig = context.watch(); return BlocProvider( - create: (context) => - RecoveryDeviceFormCubit(appConfig, FieldCubitFactory(context)), + create: (context) => RecoveryDeviceFormCubit( + appConfig, + FieldCubitFactory(context), + ServerRecoveryMethods.newDeviceKey, + ), child: Builder( builder: (context) { var formCubitState = context.watch().state; 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 e8a7e12b..4d36262b 100644 --- a/lib/ui/pages/setup/recovering/recover_by_old_token.dart +++ b/lib/ui/pages/setup/recovering/recover_by_old_token.dart @@ -4,7 +4,6 @@ import 'package:selfprivacy/logic/cubit/forms/setup/recovering/recovery_device_f import 'package:selfprivacy/ui/components/brand_button/FilledButton.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/utils/route_transitions/basic.dart'; import 'package:cubit_form/cubit_form.dart'; import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; import 'package:selfprivacy/logic/cubit/forms/factories/field_cubit_factory.dart'; @@ -44,8 +43,11 @@ class RecoverByOldToken extends StatelessWidget { var appConfig = context.watch(); return BlocProvider( - create: (context) => - RecoveryDeviceFormCubit(appConfig, FieldCubitFactory(context)), + create: (context) => RecoveryDeviceFormCubit( + appConfig, + FieldCubitFactory(context), + ServerRecoveryMethods.oldToken, + ), child: Builder( builder: (context) { var formCubitState = context.watch().state; 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 780c1ca4..34969b25 100644 --- a/lib/ui/pages/setup/recovering/recover_by_recovery_key.dart +++ b/lib/ui/pages/setup/recovering/recover_by_recovery_key.dart @@ -13,8 +13,11 @@ class RecoverByRecoveryKey extends StatelessWidget { var appConfig = context.watch(); return BlocProvider( - create: (context) => - RecoveryDeviceFormCubit(appConfig, FieldCubitFactory(context)), + create: (context) => RecoveryDeviceFormCubit( + appConfig, + FieldCubitFactory(context), + ServerRecoveryMethods.recoveryKey, + ), child: Builder( builder: (context) { var formCubitState = context.watch().state; diff --git a/lib/ui/pages/setup/recovering/recovery_confirm_server.dart b/lib/ui/pages/setup/recovering/recovery_confirm_server.dart index 262dfa6a..b83f00f7 100644 --- a/lib/ui/pages/setup/recovering/recovery_confirm_server.dart +++ b/lib/ui/pages/setup/recovering/recovery_confirm_server.dart @@ -1,62 +1,188 @@ -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/setup/recovering/recovery_domain_form_cubit.dart'; +import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart'; +import 'package:selfprivacy/logic/models/json/hetzner_server_info.dart'; +import 'package:selfprivacy/logic/models/server_basic_info.dart'; import 'package:selfprivacy/ui/components/brand_button/FilledButton.dart'; +import 'package:selfprivacy/ui/components/brand_button/brand_button.dart'; +import 'package:selfprivacy/ui/components/brand_cards/brand_cards.dart'; import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; -class RecoveryConfirmServer extends StatelessWidget { +class RecoveryConfirmServer extends StatefulWidget { + const RecoveryConfirmServer({Key? key}) : super(key: key); + + @override + _RecoveryConfirmServerState createState() => _RecoveryConfirmServerState(); +} + +class _RecoveryConfirmServerState extends State { + bool _isExtended = false; + + bool _isServerFound(List servers) { + return servers + .where((server) => server.isIpValid && server.isReverseDnsValid) + .length == + 1; + } + + ServerBasicInfoWithValidators _firstValidServer( + List servers) { + return servers + .where((server) => server.isIpValid && server.isReverseDnsValid) + .first; + } + @override Widget build(BuildContext context) { - var serverInstallation = context.watch(); - - return Builder( - builder: (context) { - var formCubitState = context.watch().state; - - return BlocListener( - listener: (context, state) { - if (state is ServerInstallationRecovery) { - if (state.currentStep == RecoveryStep.Selecting) { - if (state.recoveryCapabilities == - ServerRecoveryCapabilities.none) { - context - .read() - .setCustomError("recovering.domain_recover_error".tr()); - } - } + return BrandHeroScreen( + heroTitle: _isExtended + ? "recovering.choose_server".tr() + : "recovering.confirm_server".tr(), + heroSubtitle: _isExtended + ? "recovering.choose_server_description".tr() + : "recovering.confirm_server_description".tr(), + hasBackButton: true, + hasFlashButton: false, + children: [ + FutureBuilder>( + future: context + .read() + .getServersOnHetznerAccount(), + builder: (context, snapshot) { + if (snapshot.hasData) { + final servers = snapshot.data; + return Column( + children: [ + if (servers != null && servers.isNotEmpty) + Column( + children: [ + if (servers.length == 1 || + (!_isExtended && _isServerFound(servers))) + _ConfirmServer(context, _firstValidServer(servers), + servers.length > 1), + if (servers.length > 1 && + (_isExtended || !_isServerFound(servers))) + _ChooseServer(context, servers), + ], + ), + if (servers?.isEmpty ?? true) + Center( + child: Text( + "recovering.no_servers".tr(), + style: Theme.of(context).textTheme.headline6, + ), + ), + ], + ); + } else { + return Center( + child: CircularProgressIndicator(), + ); } }, - child: BrandHeroScreen( - heroTitle: "recovering.recovery_main_header".tr(), - heroSubtitle: "recovering.domain_recovery_description".tr(), - hasBackButton: true, - hasFlashButton: false, - onBackButtonPressed: - serverInstallation is ServerInstallationRecovery - ? () => serverInstallation.clearAppConfig() - : null, - children: [ - CubitFormTextField( - formFieldCubit: - context.read().serverDomainField, - decoration: InputDecoration( - border: OutlineInputBorder(), - labelText: "recovering.domain_recover_placeholder".tr(), - ), - ), - SizedBox(height: 16), - FilledButton( - title: "more.continue".tr(), - onPressed: formCubitState.isSubmitting - ? null - : () => context.read().trySubmit(), - ) - ], - ), - ); - }, + ) + ], ); } + + Widget _ConfirmServer( + BuildContext context, + ServerBasicInfoWithValidators server, + bool showMoreServersButton, + ) { + return Container( + child: Column( + children: [ + _ServerCard( + context: context, + server: server, + ), + SizedBox(height: 16), + FilledButton( + title: "recovering.confirm_server_accept".tr(), + onPressed: () => _showConfirmationDialog(context, server), + ), + SizedBox(height: 16), + if (showMoreServersButton) + BrandButton.text( + title: 'recovering.confirm_server_decline'.tr(), + onPressed: () => setState(() => _isExtended = true), + ), + ], + ), + ); + } + + Widget _ChooseServer( + BuildContext context, List servers) { + return Column( + children: [ + for (final server in servers) + Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: _ServerCard( + context: context, + server: server, + onTap: () => _showConfirmationDialog(context, server), + ), + ), + ], + ); + } + + Widget _ServerCard( + {required BuildContext context, + required ServerBasicInfoWithValidators server, + VoidCallback? onTap}) { + return BrandCards.filled( + child: ListTile( + onTap: onTap, + title: Text(server.name), + leading: Icon(Icons.dns), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(server.isReverseDnsValid ? Icons.check : Icons.close), + Text('rDNS: ${server.reverseDns}'), + ], + ), + Row( + children: [ + Icon(server.isIpValid ? Icons.check : Icons.close), + Text('IP: ${server.ip}'), + ], + ), + ], + ), + ), + ); + } + + _showConfirmationDialog( + BuildContext context, ServerBasicInfoWithValidators server) => + showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: Text('ssh.delete'.tr()), + content: SingleChildScrollView( + child: ListBody( + children: [ + Text("WOW DIALOGUE TEXT WOW :)"), + ], + ), + ), + actions: [ + TextButton( + child: Text('basis.cancel'.tr()), + onPressed: () { + Navigator.of(context)..pop(); + }, + ), + ], + ); + }, + ); } diff --git a/lib/ui/pages/setup/recovering/recovery_hentzner_connected.dart b/lib/ui/pages/setup/recovering/recovery_hentzner_connected.dart index bfd8fd76..f49eb982 100644 --- a/lib/ui/pages/setup/recovering/recovery_hentzner_connected.dart +++ b/lib/ui/pages/setup/recovering/recovery_hentzner_connected.dart @@ -23,7 +23,9 @@ class RecoveryHetznerConnected extends StatelessWidget { return BrandHeroScreen( heroTitle: "recovering.hetzner_connected".tr(), - heroSubtitle: "recovering.hetzner_connected_description".tr(), + heroSubtitle: "recovering.hetzner_connected_description".tr(args: [ + appConfig.state.serverDomain?.domainName ?? "your domain" + ]), hasBackButton: true, hasFlashButton: false, children: [ diff --git a/lib/ui/pages/setup/recovering/recovery_routing.dart b/lib/ui/pages/setup/recovering/recovery_routing.dart index 7237ed97..d3a8a0b9 100644 --- a/lib/ui/pages/setup/recovering/recovery_routing.dart +++ b/lib/ui/pages/setup/recovering/recovery_routing.dart @@ -9,20 +9,22 @@ import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.da import 'package:selfprivacy/ui/pages/setup/recovering/recover_by_old_token.dart'; import 'package:selfprivacy/ui/pages/setup/recovering/recover_by_recovery_key.dart'; import 'package:selfprivacy/ui/pages/setup/recovering/recover_by_new_device_key.dart'; +import 'package:selfprivacy/ui/pages/setup/recovering/recovery_confirm_server.dart'; +import 'package:selfprivacy/ui/pages/setup/recovering/recovery_hentzner_connected.dart'; import 'package:selfprivacy/ui/pages/setup/recovering/recovery_method_select.dart'; class RecoveryRouting extends StatelessWidget { @override Widget build(BuildContext context) { - var serverInstallation = context.watch(); + var serverInstallation = context.watch().state; - StatelessWidget currentPage = SelectDomainToRecover(); + Widget currentPage = SelectDomainToRecover(); if (serverInstallation is ServerInstallationRecovery) { - final state = (serverInstallation as ServerInstallationRecovery); - switch (state.currentStep) { + switch (serverInstallation.currentStep) { case RecoveryStep.Selecting: - if (state.recoveryCapabilities != ServerRecoveryCapabilities.none) + if (serverInstallation.recoveryCapabilities != + ServerRecoveryCapabilities.none) currentPage = RecoveryMethodSelect(); break; case RecoveryStep.RecoveryKey: @@ -35,8 +37,10 @@ class RecoveryRouting extends StatelessWidget { currentPage = RecoverByOldToken(); break; case RecoveryStep.HetznerToken: + currentPage = RecoveryHetznerConnected(); break; case RecoveryStep.ServerSelection: + currentPage = RecoveryConfirmServer(); break; case RecoveryStep.CloudflareToken: break; From fa6f74e884b7393447040063447ec6954d707388 Mon Sep 17 00:00:00 2001 From: Inex Code Date: Mon, 23 May 2022 17:21:34 +0300 Subject: [PATCH 26/52] Finish recovery flow cubit --- assets/translations/en.json | 3 +- assets/translations/ru.json | 4 +- lib/logic/api_maps/server.dart | 6 ++- .../server_installation_cubit.dart | 38 +++++++++++++++---- .../server_installation_repository.dart | 30 ++++++++++++++- .../server_installation_state.dart | 14 +++++++ 6 files changed, 82 insertions(+), 13 deletions(-) diff --git a/assets/translations/en.json b/assets/translations/en.json index 58b844a8..7564e19d 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -311,7 +311,8 @@ "confirm_server_decline": "Choose a different server", "choose_server": "Choose your server", "choose_server_description": "We couldn't figure out which server your are trying to connect to.", - "no_servers": "There is no available servers on your account." + "no_servers": "There is no available servers on your account.", + "domain_not_available_on_token": "Selected domain is not available on this token." }, "modals": { diff --git a/assets/translations/ru.json b/assets/translations/ru.json index c2fc34bc..d3393394 100644 --- a/assets/translations/ru.json +++ b/assets/translations/ru.json @@ -313,7 +313,9 @@ "confirm_server_decline": "Выбрать другой сервер", "choose_server": "Выберите сервер", "choose_server_description": "Не удалось определить, с каким сервером вы устанавливаете связь.", - "no_servers": "На вашем аккаунте нет доступных серверов." + "no_servers": "На вашем аккаунте нет доступных серверов.", + "domain_not_available_on_token": "Введённый токен не имеет доступа к нужному домену." + }, "modals": { "_comment": "messages in modals", diff --git a/lib/logic/api_maps/server.dart b/lib/logic/api_maps/server.dart index 92bd6932..7a6fefb5 100644 --- a/lib/logic/api_maps/server.dart +++ b/lib/logic/api_maps/server.dart @@ -153,13 +153,15 @@ class ServerApi extends ApiMap { ); } - Future>> getUsersList() async { + Future>> getUsersList( + {withMainUser = false} + ) async { List res = []; Response response; var client = await getClient(); try { - response = await client.get('/users'); + response = await client.get('/users', queryParameters: withMainUser ? {'withMainUser': 'true'} : null); for (var user in response.data) { res.add(user.toString()); } diff --git a/lib/logic/cubit/server_installation/server_installation_cubit.dart b/lib/logic/cubit/server_installation/server_installation_cubit.dart index c2849903..13b84610 100644 --- a/lib/logic/cubit/server_installation/server_installation_cubit.dart +++ b/lib/logic/cubit/server_installation/server_installation_cubit.dart @@ -75,6 +75,15 @@ class ServerInstallationCubit extends Cubit { applicationKey: applicationKey, ); await repository.saveBackblazeKey(backblazeCredential); + if (state is ServerInstallationRecovery) { + final mainUser = await repository.getMainUser(); + final updatedState = (state as ServerInstallationRecovery).copyWith( + backblazeCredential: backblazeCredential, + rootUser: mainUser, + ); + emit(updatedState.finish()); + return; + } emit((state as ServerInstallationNotFinished) .copyWith(backblazeCredential: backblazeCredential)); } @@ -410,14 +419,27 @@ class ServerInstallationCubit extends Cubit { )); } - // Future setAndValidateCloudflareToken(String token) async { - // final dataState = this.state as ServerInstallationRecovery; - // final serverDomain = dataState.serverDomain; - // if (serverDomain == null) { - // return; - // } - // final domainId = await repository.getDomainId(serverDomain.domainName); - // } + Future setAndValidateCloudflareToken(String token) async { + final dataState = this.state as ServerInstallationRecovery; + final serverDomain = dataState.serverDomain; + if (serverDomain == null) { + return; + } + final zoneId = await repository.getDomainId(token, serverDomain.domainName); + if (zoneId == null) { + getIt() + .showSnackBar('recovering.domain_not_available_on_token'.tr()); + return; + } + emit(dataState.copyWith( + serverDomain: ServerDomain( + domainName: serverDomain.domainName, + zoneId: zoneId, + provider: DnsProvider.Cloudflare, + ), + currentStep: RecoveryStep.BackblazeToken, + )); + } @override void onChange(Change change) { diff --git a/lib/logic/cubit/server_installation/server_installation_repository.dart b/lib/logic/cubit/server_installation/server_installation_repository.dart index af006c6b..bbeb5700 100644 --- a/lib/logic/cubit/server_installation/server_installation_repository.dart +++ b/lib/logic/cubit/server_installation/server_installation_repository.dart @@ -346,7 +346,7 @@ class ServerInstallationRepository { } try { final parsedVersion = Version.parse(serverApiVersion); - if (parsedVersion.major == 1 && parsedVersion.minor < 2) { + if (!VersionConstraint.parse('>=1.2.0').allows(parsedVersion)) { return ServerRecoveryCapabilities.legacy; } return ServerRecoveryCapabilities.loginTokens; @@ -485,6 +485,34 @@ class ServerInstallationRepository { ); } + Future getMainUser() async { + var serverApi = ServerApi(); + final fallbackUser = User( + isFoundOnServer: false, + note: 'Couldn\'t find main user on server, API is outdated', + login: 'UNKNOWN', + sshKeys: [], + ); + + final serverApiVersion = await serverApi.getApiVersion(); + final users = await serverApi.getUsersList(withMainUser: true); + if (serverApiVersion == null || !users.isSuccess) { + return fallbackUser; + } + try { + final parsedVersion = Version.parse(serverApiVersion); + if (!VersionConstraint.parse('>=1.2.5').allows(parsedVersion)) { + return fallbackUser; + } + return User( + isFoundOnServer: true, + login: users.data[0], + ); + } on FormatException { + return fallbackUser; + } + } + Future> getServersOnHetznerAccount() async { var hetznerApi = HetznerApi(); final servers = await hetznerApi.getServers(); diff --git a/lib/logic/cubit/server_installation/server_installation_state.dart b/lib/logic/cubit/server_installation/server_installation_state.dart index b376914d..605a26ff 100644 --- a/lib/logic/cubit/server_installation/server_installation_state.dart +++ b/lib/logic/cubit/server_installation/server_installation_state.dart @@ -336,4 +336,18 @@ class ServerInstallationRecovery extends ServerInstallationState { currentStep: currentStep ?? this.currentStep, recoveryCapabilities: recoveryCapabilities ?? this.recoveryCapabilities, ); + + ServerInstallationFinished finish() { + return ServerInstallationFinished( + hetznerKey: hetznerKey!, + cloudFlareKey: cloudFlareKey!, + backblazeCredential: backblazeCredential!, + serverDomain: serverDomain!, + rootUser: rootUser!, + serverDetails: serverDetails!, + isServerStarted: true, + isServerResetedFirstTime: true, + isServerResetedSecondTime: true, + ); + } } From ac93a384e985fde3fd09c3ec4b4c17b2bc4470bc Mon Sep 17 00:00:00 2001 From: NaiJi Date: Tue, 24 May 2022 10:55:51 +0300 Subject: [PATCH 27/52] Implement recovery cloudflare page --- assets/translations/en.json | 6 +- assets/translations/ru.json | 7 +- lib/ui/pages/setup/initializing.dart | 3 - .../recovery_confirm_cloudflare.dart | 70 +++++++++++++++++++ 4 files changed, 80 insertions(+), 6 deletions(-) create mode 100644 lib/ui/pages/setup/recovering/recovery_confirm_cloudflare.dart diff --git a/assets/translations/en.json b/assets/translations/en.json index 7564e19d..a33f8e93 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -312,7 +312,11 @@ "choose_server": "Choose your server", "choose_server_description": "We couldn't figure out which server your are trying to connect to.", "no_servers": "There is no available servers on your account.", - "domain_not_available_on_token": "Selected domain is not available on this token." + "domain_not_available_on_token": "Selected domain is not available on this token.", + "confirm_cloudflare": "Connect to CloudFlare", + "confirm_cloudflare_description": "Enter a Cloudflare token with access to {}:", + "confirm_backblze": "Connect to Backblaze", + "confirm_backblaze_description": "Enter a Backblaze token with access to backup storage:" }, "modals": { diff --git a/assets/translations/ru.json b/assets/translations/ru.json index d3393394..d240c6e3 100644 --- a/assets/translations/ru.json +++ b/assets/translations/ru.json @@ -314,8 +314,11 @@ "choose_server": "Выберите сервер", "choose_server_description": "Не удалось определить, с каким сервером вы устанавливаете связь.", "no_servers": "На вашем аккаунте нет доступных серверов.", - "domain_not_available_on_token": "Введённый токен не имеет доступа к нужному домену." - + "domain_not_available_on_token": "Введённый токен не имеет доступа к нужному домену.", + "confirm_cloudflare": "Подключение к Cloudflare", + "confirm_cloudflare_description": "Введите токен Cloudflare, который имеет права на {}:", + "confirm_backblze": "Подключение к Backblaze", + "confirm_backblaze_description": "Введите токен Backblaze, который имеет права на хранилище резервных копий:" }, "modals": { "_comment": "messages in modals", diff --git a/lib/ui/pages/setup/initializing.dart b/lib/ui/pages/setup/initializing.dart index e0971ae1..0b99c761 100644 --- a/lib/ui/pages/setup/initializing.dart +++ b/lib/ui/pages/setup/initializing.dart @@ -17,10 +17,7 @@ import 'package:selfprivacy/ui/components/brand_text/brand_text.dart'; import 'package:selfprivacy/ui/components/brand_timer/brand_timer.dart'; import 'package:selfprivacy/ui/components/progress_bar/progress_bar.dart'; import 'package:selfprivacy/ui/pages/rootRoute.dart'; -import 'package:selfprivacy/ui/pages/setup/recovering/recovery_confirm_server.dart'; -import 'package:selfprivacy/ui/pages/setup/recovering/recovery_hentzner_connected.dart'; import 'package:selfprivacy/ui/pages/setup/recovering/recovery_routing.dart'; -import 'package:selfprivacy/ui/pages/setup/recovering/recovery_method_select.dart'; import 'package:selfprivacy/utils/route_transitions/basic.dart'; class InitializingPage extends StatelessWidget { diff --git a/lib/ui/pages/setup/recovering/recovery_confirm_cloudflare.dart b/lib/ui/pages/setup/recovering/recovery_confirm_cloudflare.dart new file mode 100644 index 00000000..c77948c7 --- /dev/null +++ b/lib/ui/pages/setup/recovering/recovery_confirm_cloudflare.dart @@ -0,0 +1,70 @@ +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/cloudflare_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'; + +class RecoveryConfirmCloudflare extends StatelessWidget { + @override + Widget build(BuildContext context) { + var appConfig = context.watch(); + + return BlocProvider( + create: (context) => CloudFlareFormCubit(appConfig), + child: Builder(builder: (context) { + var formCubitState = context.watch().state; + + return BrandHeroScreen( + heroTitle: "recovering.confirm_cloudflare".tr(), + heroSubtitle: "recovering.confirm_cloudflare_description".tr(args: [ + appConfig.state.serverDomain?.domainName ?? "your domain" + ]), + hasBackButton: true, + hasFlashButton: false, + children: [ + CubitFormTextField( + formFieldCubit: context.read().apiKey, + textAlign: TextAlign.center, + scrollPadding: EdgeInsets.only(bottom: 70), + decoration: InputDecoration( + hintText: 'initializing.5'.tr(), + ), + ), + Spacer(), + BrandButton.rised( + onPressed: formCubitState.isSubmitting + ? null + : () => context.read().trySubmit(), + text: 'basis.connect'.tr(), + ), + SizedBox(height: 10), + BrandButton.text( + onPressed: () => showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (BuildContext context) { + return BrandBottomSheet( + isExpended: true, + child: Padding( + padding: paddingH15V0, + child: BrandMarkdown( + fileName: 'how_cloudflare', + ), + ), + ); + }, + ), + title: 'initializing.how'.tr(), + ), + ], + ); + }), + ); + } +} From 7344858e8660cded113373a1b548e10cc70b93b7 Mon Sep 17 00:00:00 2001 From: NaiJi Date: Tue, 24 May 2022 11:06:58 +0300 Subject: [PATCH 28/52] Implement recovery backblaze page --- lib/logic/api_maps/server.dart | 7 +- .../recovery_confirm_backblaze.dart | 77 +++++++++++++++++++ 2 files changed, 80 insertions(+), 4 deletions(-) create mode 100644 lib/ui/pages/setup/recovering/recovery_confirm_backblaze.dart diff --git a/lib/logic/api_maps/server.dart b/lib/logic/api_maps/server.dart index 7a6fefb5..9320af9d 100644 --- a/lib/logic/api_maps/server.dart +++ b/lib/logic/api_maps/server.dart @@ -153,15 +153,14 @@ class ServerApi extends ApiMap { ); } - Future>> getUsersList( - {withMainUser = false} - ) async { + Future>> getUsersList({withMainUser = false}) async { List res = []; Response response; var client = await getClient(); try { - response = await client.get('/users', queryParameters: withMainUser ? {'withMainUser': 'true'} : null); + response = await client.get('/users', + queryParameters: withMainUser ? {'withMainUser': 'true'} : null); for (var user in response.data) { res.add(user.toString()); } diff --git a/lib/ui/pages/setup/recovering/recovery_confirm_backblaze.dart b/lib/ui/pages/setup/recovering/recovery_confirm_backblaze.dart new file mode 100644 index 00000000..521a1860 --- /dev/null +++ b/lib/ui/pages/setup/recovering/recovery_confirm_backblaze.dart @@ -0,0 +1,77 @@ +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'; + +class RecoveryConfirmBackblaze extends StatelessWidget { + @override + Widget build(BuildContext context) { + var appConfig = context.watch(); + + return BlocProvider( + create: (context) => BackblazeFormCubit(appConfig), + child: Builder(builder: (context) { + var formCubitState = context.watch().state; + + return BrandHeroScreen( + heroTitle: "recovering.confirm_backblaze".tr(), + heroSubtitle: "recovering.confirm_backblaze_description".tr(), + hasBackButton: true, + hasFlashButton: false, + children: [ + CubitFormTextField( + formFieldCubit: context.read().keyId, + textAlign: TextAlign.center, + scrollPadding: EdgeInsets.only(bottom: 70), + decoration: InputDecoration( + hintText: 'KeyID', + ), + ), + Spacer(), + CubitFormTextField( + formFieldCubit: context.read().applicationKey, + textAlign: TextAlign.center, + scrollPadding: EdgeInsets.only(bottom: 70), + decoration: InputDecoration( + hintText: 'Master Application Key', + ), + ), + Spacer(), + BrandButton.rised( + onPressed: formCubitState.isSubmitting + ? null + : () => context.read().trySubmit(), + text: 'basis.connect'.tr(), + ), + SizedBox(height: 10), + BrandButton.text( + onPressed: () => showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (BuildContext context) { + return BrandBottomSheet( + isExpended: true, + child: Padding( + padding: paddingH15V0, + child: BrandMarkdown( + fileName: 'how_backblaze', + ), + ), + ); + }, + ), + title: 'initializing.how'.tr(), + ), + ], + ); + }), + ); + } +} From 804e2750dad27e9796e8ff5a30847e1151ffc974 Mon Sep 17 00:00:00 2001 From: NaiJi Date: Tue, 24 May 2022 12:53:28 +0300 Subject: [PATCH 29/52] Fix metadata screenshots for f-droid --- assets/translations/en.json | 2 +- .../en-US/images/phoneScreenshots/1.png | Bin 184647 -> 61120 bytes .../en-US/images/phoneScreenshots/2.png | Bin 199739 -> 79651 bytes .../en-US/images/phoneScreenshots/3.png | Bin 174012 -> 52977 bytes .../en-US/images/phoneScreenshots/4.png | Bin 105046 -> 44320 bytes 5 files changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/translations/en.json b/assets/translations/en.json index a33f8e93..ca3c1984 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -256,7 +256,7 @@ "initializing": { "_comment": "initializing page", "1": "Connect a server", - "2": "Here, your data and SelfPrivacy services wiil reside", + "2": "A place where your data and SelfPrivacy services will reside:", "how": "How to obtain API token", "3": "Connect CloudFlare", "4": "To manage your domain's DNS", diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png index 7b06195e19c857adb9b42e784273bcb94e0e03b9..3ffd726f9a8048521889f905557e2c16194c31c0 100644 GIT binary patch literal 61120 zcmeFYXH=6-*C-r&K~$tkvrr`n+#mq~u|Yt^I)Mz z{|N{F4LklP{Q7Sghp^P=mHEJfFFpD<*zw=s0Kdn6yfO`cmv?%M`y1}(!Ph)J@PGI9 zclaCQ6E0pzI4{1>dnG}>AWM)D=r#}jKN|iup57!7=;|5>bl}au(j1dPppq9L&{^)k z(qun?K&So!fr{H7Kg2!!4`dGVe)qe&f(n9U(kAi zS4WaZr#J6;1o8m6fG&WHL0%vykO~h{2mJw318MH{f%HL#4<0&v=-}bQhYlY(a`@=+ z)5nh=J9b>?nYlrQ}ucS-Tm z-;L}$a^%SIqsIl09~ZnNE+T&E|99B?3=%kcD1ZO=1N)>w`vvwL5ZJf(4J5^zzWw_T z{LR__3`dS0I=t_|!Tra0*^Z|{`wkq~cktk0{zE5@96oUL0BGO-13YpBj-0uuc2w|| zg^=_^pJyy#(X;AWaHL;s+}n@&BDXE$3uLZnS~=qI&kMhF{*={6xRAbf$Z0^X`W894 zE*cmb{q-WF<{Q+1iPwktK3?zt1p{86hYlay&&yL5;H|g)Jal;Y9QaQJ_Z_g@e^5Y3 zIQHYAix$#qx6TOi5Xfi!T-@V=P8{G-BXB?fbOTiMWB)(?Nzwm}|2YDOQ(#52DrEUz z@Bftk_+%(|I!*}5a-gTY+SNjG7g?g{zB%^iSpc12Z*B-CCyXz{^XUv`!>@)?m)8GYE9* z(E5_sC!2{fd+bHh$7hf@_p}V#dV^J9(O^7V^<-;)OWU#WI+d=C!=H2F7(_-3Cy|De zCaFY`rs{8M{_!`lj=0otPGk?@tu>u@u(;D}tq|`O9}v{gy-9;2mr^6X=-w3(ESlBV zDs8_NHSkM^ewMi<_M;P;0WqAR_C&Nc93KC>nSX|TdE;f#FOM;5eHv#f4il{7(&F&y zrn!+EZ9zi_Djr=AySnew0jn>CvsfKM{wA%trq3|fn3I#JTC+!@c0hWe(Cu>=H(z_V5U8xmZ5sd#$__k3p|e9~d;PLyzCHCe9~VGwrm zIyk3wJt5-3+S{%&Sl{PIIngc=Rs0&|tTV!^`cEaa z61-?j3rco02jR)6R?$z*{{Ve+36i1;=C=nnfGf@n>}SZPP(xFI2^n~;BX|Wddq9Q% z#!;@7LMpPWW0>Q8GCf@*&T3PG<;6f5pzZ5OOY>SFQ0*vZ#!Jl!sBi#>iVWP1wsxKF z-yN`vHW$tTD(oV@@P{1d5~NblF2y6FF(+w}!a1Q&`7~?{30NdPL_oW&0>XJEk@H`M zgC|hzgR#G)ti!TwcVB0PE49PI{-mm|Qp57!9&azKOm!|>MpcWFBV9h+-=5p*J431t z34ozuD&Wo~t`c{822GljQW^O&8XWy)$%zE%?7JD{>3zejC?X@8%glFTxu2zN&Z=9ZRzI){YgT*ADS9tho^?Yg5_KAX? zvF@XpWQt>e_T+l`ywmmP2kWcE=H7QMh0Wi~SoNzhPH?q`OQWYkUcYR)(9_eG)N^?&C zr7RnF=NaM2xprX<+!BH%XfFSPIrU;u*W-8mpF3hVSN*(dW&`C4rRgzh1%p{3PEVZQ zLhkwr#j2gl$@8y(EOAh7M6HHQ-Qp+tE)q?-XPNl<3qUN0N4855)#J=Lj@*G|A5` zo~c*+bk#GglI^#_^_{&?y>q=uIq2MtTR*e_zJyS}$5phVwaW=hmG_AY8Av3RUa^C} zl4MF_b{>b}^D;>hLD#cg>xQzL{?h%ln`~T|>NWDT*s3v*#A)NWeH7`h#W=B}NyKc85nMlaK@nId>{_S&(Jt22w{}>)?e<_JW#XK7 zsZ1cy(N%!H4p4IE>VLdc`AtY#RCMjk=S#2m-Nu+c88-&o9#C?pKEQlk#gw%U6X{V@ z$yB!RwQZ5~jo8*CKf6)~a4ZFma*cHzfC)Hz(QqVx=`w)159D`a^TDGQg+0*QiWd@- zVT+&J-^^p>h57 zyF=tNc@@YGR}qirKQ)vizH_G~lrF}EpeNh_CP2oylUBOg{0cWk%0kEA?-)oE{<`s1 zl05$Dci%q>1t-p?iXQoH9^XTvJExga-an2g(1iK*TTvNC*c!7(#dEQ}mzN8aD#!b7 zDecaHf#fsgEd<03y>-Urw>uA-#-BAcW>g^b2xj%NKu-1+LoeLr>bjEkI|o$+2VCSU zm*<-2_bLykg$Ul66K>2?Irl(iRuF2zC->aYRIoEB&ndPQ7eFFkp)-pUhwe^CZogA> z?_0C!^Hz^MsaxU-SShNKarVt=|u> z^*!U~nYCS^g}ICiW$FzL8Gdn)-!;9^xSe(BnV)1QV7LbHyp+@ z7>e!2Pe^vlE%SxqCFWpjTIZ&W;xKN?LKkJE-MBTVNi!I$L5!2z+*y4WYaa(Ki$^8! z>rXiv0~8q}nGA3K0J{pQ?1~^98Zt*(7GD^vQ<#m(=P}K?>ZzIFn8WM0RZ4O>0lont z=(#}mc1!-rKZ{IXEc1mcg;1U#ivGxgNfwj`?ONq<)Q~|Wr4(`+Jno{cFM@)mI>bGj zcX1pfPfON&Q5`ttc#9BG^wnqEQmFWzOY_HWp+Xp6?5HGoGfKjF8(=2;ArMd~TQu$? zv^ee4-IsjdoEx$9w@Li<46N9h8^MrZlDD^N6>Q7^`Lo^9e9}A3s{ccoBTDCG+gl4c z3fqLmF^N6O37L;9mJ3JHjB-(3RdzUF;#Q(Y$l zX@1iWa-$bGTRF_P8D3=rLvDSBiH%-Fl+6d~qPV!Gu3@wM`o#VDT9j^Fo@{4x-qQ}v z15Z9LPB{AxxB(;C_kYP>vKeP8Psmu1tTqT0N_#hVS#@9 ztU}c*F(k76QId`X&2)^=t|Mz6i?_Nx?k$6#hhmH7p`D+OwN2qmxQg35v?p>W#HYFu zEj=l#Wfn(|WDD0tP8HZfVFd-mf@ID+mK-^uKrQTX3IoVNc>o)s-1SbI*CAQyRn#w& zii2}v2QhQv=>`VFV=I*R-1P?4KJ|Ju%@s!`q2y6uAp z$=?HhkGu<;w)}l25eH-+zj|KJZs``EAj(*YMKjmQ+n>LB4&hm?h5}2=z6F5FR5@wA zELYLiPoDC+_=9;*PB7d}v6+L2Xd22ZlVG-HE`|(}o@xYD6!&MGXCi2N1v{AB95X4T zo*hni5(6Z=XAzD%TyA&vF=YD=uCZta!vIk6h7@AiyP2%4 zov0c}cM;BzE#P$P3ELi0_OomlHDLQ;?Et2NAoFg4jQO#<4sY$IZSUS_}UdrMaqz;+tx)`i75Fa+r$;8bSYS5oR+k-#m z&P>k$Y`fP`w0Ji|mxC53N=vIMpY)aVmGAKiGAJrB%(DEJAhd>P%IUQRNQ^lLFjxtN z=)8I3*0a1HVnRV)qK~S+eT%7~^eMfI0@Qw`4(Z3jzk7f{7YT8Q)!fNWOI z5dLeRg0neK80SwIJS^1v!Ys@1T5OLgtwc+>1hZ?zS{PN8@;Qg=Cbd% z#s{%+2CM8mM-qt~3>GS2v&a`xj+NT03b!x@6nkZ3`y9>~S9Qx*X+%Gb=9q~~Z&wTm zUBRYhxV^nU-PI_UK2=o@FXI+#3q+1rQhJyr&`zT2xjTo-a@s#l2PZ2|tG7G_zZ%T1 zQt1DPeK(301|sJyECSsg&0wZ-+~ukCoN!{I2z8A3DC2A$8RDDtTp__kt|LX_*L&{R z*TCd*7~Tmx!n*3MYgOG@#Zg^*Lo6;&*>r%WxCEO-AVZ!LW1FJrd1mV5Jy6l=>N5zp z@S6P+T1uGA<8Uiupx`)WGb@%WPtC9yL$U-AP5mlD{#<o;?d>D7M#zF>oVBP`G_3t zhHOtp4Ai3D>)k9VejQgI6zfztNK05&(ClCvfyo~M@}R{n7`~3)y*<^P9!HP6FP|(K zJ>4^|WH*OOd<8Jy>U*SUy(pVa4}Aw-pW=Y=g?Iq6UB! z@y17G-#K-IURgQLIq8W&?0b{yXM7XCXN+u=w<(CI&Sqf z9$96ii*Gdwq%hOB@MyUGv21c(ScHSWej2<}?u&%2pcu;7)&v+B2LS1%Q6{5-Qr{7I z%ZGV4^vLKQsO8T+kdK)Cw(u{-fv}HRzX$i*am`>`PORGnzjC^pt&R7$M32T5lQb&@ z8!I>h=WkcP(9K)bsP6l0;b%Xd4@{JacfpWIKLh<^=NzJqt$)OhY6h^eWJ_*O!uB3W zE5tH;HIEDZI+fHl>_44+p|G#WMPJ50T|k`#UT|-vu>}MZbEpH^>PxJ48HUGikGPN* zXWGs0FbcnWl|PBi2uMxoPiP=cra;Xl4akDj)0RGny{e&=b_nv2<))w5t_aXHR^%xty1kfO#nR%|G&nT*7QUg^6*9^M1>f83UHFR|b%zb&5e z)K$!(WZd}}5kXSRAxaB%wB}2FgnMd+&uQK$_l{hWf{!h7@g1_slJw48PLh;HSB7dQ zj%YO&H~^0gPQH{GP3^7dg*cSW$}3V=Zsq;n3ENWL z1AQ{6lz%FT3!C_|2P%2}DNShoN&JuZ3Fi~6+Chs$ho*0INGFB)(g!yP{zUJR^wbvf zJJBx&)n~J|ROE{~5eUONq@}5};e>XyVFq{X2a_Ql8A)j_u@^F}etWsDSnbeOu5m|X zvy{GA-Z9~ePz<)8`q4aJ)FtIHSHd$XG%j2IE~gz#P0iPr2$6{pmT0xm_&Eq<@)Xc` z7yl-^$bt7zSu1RGS(jSpCf3EuTU8hXX^Q^YH)s@K79#zpzkjA*{#L1y&=9*aF zXa5|*j8jo&A;1TB30&`*W^2JV79v+?`lk!Y1)drEsur8NQ4(N)oOM*q{IuEr`>yNn zNa}Cy?MqEeJx_UqO#0Kg?FlOa-ewak{2g$MHoA_2*aK<(MGBHmCmTOZwpvG^xZM)l zmWa;UyM^E83!fTkr|W3jq)L&6&2HY3I`#Xk*vhdz(3t8)q>k2X2BfC=O&{IZWl`r~ zW}-^T*AMY^{__zT=cqlEFR1$^ryeaTdsAC$b(JW=ISgUjNyaVF%CWEa${R9%u~#+IF`rm_WlvEc5|hBAhbTphZyLx$n{^y zZ%0B|&QLSHM!04BTrtl&>u>wC$XfB2{0LIgGhBJSFv!wMJFv_`@VP2vf4-l}CX&~H ze)DdTtfxkFfoJq`e}$oiL&*fnUj_{Euf83>2QpMn9KUMzUCGN@@BQbT%;H9~-fLhn zNN}TItcDXi2-5G@`D~_d{WD$65UOeXQYQ^FU1Y;le9vB}6ojp72V!y}0F~6hnQwmv zPZ?jk&I%Ztaq4&kmwl=pIP+7W{AJidp}SL$3bl&5&N$K7+$5M^rJg6z+_Z1O6`sKh}c2>!e< z>1}S=<=0UjCDwAj))K1AWo@|0R}uG^0L744O@Fa{54UmeG7!QyS9;&A0D7)P|xcfjsBpjlYuIElV*0YQ2aH z*y?PePuF1a%%@Y$N=0RdmEecu2A$21k>6X@>X7+YankeUW}zAm3c~k;`Tz2Cy|Wt` ziWM$Nj43P0nbS2Qn?(5sOBB{B;KTPquzAbsvu#-M#}o`<2lL1tm|}^-6;F2^ooiW)b5Df z7k70-7pox<-f61);LLE+6Y)6jka}I1R{Z;*mwnzwk48(FDZj-mU@%+6SR4?Ex2l)n zr1XqJStueYJF)_QJKEAKl5x+nD%+!JNa~7Mw+_k@P?N}J0t-{*w}b2Yh4Md2&20U$ z8WdF#T2VGkF?W(xiD3bhgGM<#nf(5evd+(l?CHewLUUj99b8dDmtI=_Lps&`j zbdSM|!@VxeQiKsmTS2ziKt__kX2Uwz%P?xeHZOPyiTs%(ER%t%dCHTiRIT+hf-$h6 zAH|TB#v2bh*U#rytcY>b!k0qx1C^Nx%a=O?olgxwBj(J7^N{x7)Heyi@CUsjz7O*| z9lIA!5{X&MCWBR1OKfskqNuW*ijaSN*U`3U^_ko#ve7}H5C}Ph1>=Txv&pN7DH*dU zE8+egv4w^ctmc5&N~$?mL&n&Od7-YD{`PTFLk;!|^dT z9BdW6lhMSpNKGH&>lHa5!csq$I~77^AT!Y(iW(-rtD#H8LB(1@LK=7DOj=)t!lzj> zo}`2FP_-R=j*M`aS#wvc>^gUCk$(d@&~~<{%O+&U_jN z4tldHn>cpf=YH}1|7d_Y^!7Ask!pH%QipNpKfvtL+J*9 zY~g`;AG>h|EkJV#n}EE!ygcLF;FNj2K8p+06RvBu;MZG@h!V8DPLW&!W|+V$fDJ_= z?CntYt4C1>d){tUwY^Vv7Sl5oxj#<4lu(J8ucJdzm2GoYd5;ci*b}Px@8`8i?WVL& zNc0%8!Dowgrni*O?hNe|ii~vM9<|53$!{V!NRxuvS`$3H-2$^aJ+y*nLu|xNie|E0 zJawkTr`f>@*1%*GoroR15Ub(xBE z|665lG?Iga--vCLpDyW$EZB8g6?F?-QLi|+;S?;t=BYFijf!{fv{L=pz+M%qYh;bM z!3W)>nfF7*2e*2-5fz$g19q6vL4V%P>IXv^KrN$`K1mn^8rHZ`!5N4|OppruQUnRm zE^l33^K=Y8UMXUfum{r+pziTELE}CM?htjaeC{wwjJ_g9G{o zZcnECAVg8_muqQs2DFm*r;epbUNJp+n{nTum7eKQzFJU^;0C`6Tx8qPVDL(6eVO(? z%G`oWQMnqmP#@+`9Nyj>i=C0ZQ|mg-(3Z~A8{0lMl=1$zEqxF4J7oPRReq9Z=HX5s z$7Y2Nwhp=m*Izz^f-FPD^;1p01aDkyLRXa~vbo(W( zc*d&njI<9nL9mW}71(DT{J*e*gnH;Z1&v=}=<*M5I4;`aFURS)SEwJECw*HdM{smY<*X9vxwAlBuWN=0 zfkN1z%1qnUVMhmX*wKs1>9#n}(g~!HRz-ULYv+^HbfYr+ZiZy{eEqqcuHn%!lEQgd zVlSZ$lk~vzYoR#GqFwiqbBo98Sk{8cM1BJei2(xXW_o||sT$3ZowUZ{MbiqT!3DZ6 z_=DlvXDcV^*!ZT1yiEQh9-DgC+kM?HBNCQTYHy6xo7QEJ!dyC0Ixl|e^0|OLP`r|N zOHLT$;n}e#QtPj|K=3P)%QgV06w_V1)kL%d610#VWx(&F?7?NgA5)0l108SjN;|8l zX2lgQleD;XoM)!C%SaYQ@9wrKdsXc;wmiQwX(}U;?5tKcXMFx98;cACWFzPiQ~Km= zhO^+KM3ejd9P>lf!3gu#!C0>J?(wR4(buc~+%qgrcNwh!O|gPxII;^j{Lpi|=LOSB zDH%bNQ56AB1Cm$D$F>Swyd$3{cSv_3ni1TH^uYn%PKfMWO}J6Y-$W$Zc>N`qIcJso z+FCqnTgfgmYRc?-yu~DHk&OQxp0pFlC(20GA(cBONy5*Nj9omh39Y_ul(D8d#X&lQ znj#i#sI`Hy=7#)9u2;>TIN%y*e;s@|V@-6>!FVZ*!dx9CN1Pq3Ql(?W{@<>Z7Dt9T#H`-5B~Fc=(mgNQ>c$n93l zn!R^xmg-?#EcvlZR#b zZ_1l!gHQ+H7T80^No{zmz*$4vvucI^c&pJhL_XN7fyT0-RxrVssE}pXC^^gyAvjE|;EdyM_vOgqhUrre>#o_iW5|8-NFe z^7}16oi|H)@KvlxQlz+^Q=I;>&7*5Y8-kU?EOHtsfNClrvipxYlU}^ly<1Yc!U1Tc zxsTJ?@tINF4fE+|xvP;`#CT6JeT?|1HCRFk2~V-B1Hi$e^Qw}fJKl_PRq8eS+MTlr zO7c}I6Da!o0dm-Z+kNA87r6$H?Pz!b3uhtLnrCQEC0hR~pf;UqVS)6+Nv|MSf$k1Z zKCA<=J3?=?dIskF){WB)nJfK+oaTj_>}@^ER_kI9>$j6*zF;QDy{8+}7SJF0^yTTc z&&A3jZ4!{SISXO0TNdXL^axQZ`4I#w90&j;%$#7Hw1S@NgMat_(BLl}2|8?D*feup z33W)%Zn`U>V0y4hwJlm|3#ho7cgO&NhICPvcAoT1o~b<{rt3}|q)>zuug?Uz^~Or( zq;Yj$J`DIQe9AHHylR~D+kpOQAyP(G0$9v&BV#-#ScOta0wByKGzvLJmNNa@CGvj` z`~P%!e3yx)+bPM^dbeldmyJ_&Jv)_zqwWC&Bd@9KRQ%Y-r#JuSTL1Z90A+k4F;pQxguIta zR6Rg;UG^&TW9O5|LS`m*pLji=SJ%t0n`jutibqa>mkn-Se`a!JFjo7;&c(Qt6Um;% zgC#(=^BZN6%Znp|8k|;le zhavlKCjK{M{ck7n!`-#n8IOAU<~K_8Z0OTJpDT+Pk0OwUB|NPSST@isQ0beKKUlgZ zSMPqAXk%kkS@fyqN}p_;wxAV*hVxd5HdFd?bqRL!Pr9BGP8*y$7^7DO5XB(Sz1l7;^f^)1rJv&gZOo&D40 ztEP{&g(u+q{$Aq@vMt^!Bh>p0w9{aWp1RWz?^t-9pEi6dqKTv2b{I_dO^-%w3*Ss; zdhdZo-sQEDceBr&Hw^n#rQ2@3Njc{?v7J6RQC@f` ze8P0+xI5w53~;$F2xW*2X%s8>0F$-H2PL3jUV|r_8wJsTySoq~j(* zcF{Im1#R0idDskpDmj=mGYSu#k}5K-5_`=ZtDBm{Ix*Ms?`&m+zKWffCphLeZrgDX z7ggSsvYE|9E_TlEd&iE9$r$nK{nyy^diQFQKcFoT`mOg9&n|YI@OB=+Rjzh15H#HH zKZPf|R<#UP_uc)2y6dHyV4G)EdLf6F5uY2W*fWve%wuQ_DNipDEZX@nIgO+H#d76o zR#q0xCi&w1sf-XY=70uTazdpdq{)-T`AHSts;Mpv+7g9oPcj&t^+slmPBvFF9RX?)Iv{=iux(b@=QQ4Y+rQtj ziWR-B`qL1$a<$s?=74`;hhd6nHKJE7!~>UWj2I`CD ztKISG%57gWx2hMiA=y2e-bIu-o{yU<+7trTj_vB`(w=qHg$0yo*-?hc{8Hp|Yx`M0 zx0Fj%>fILnjT3Fa+0$$Us}+G3rkmA5q0lRaS%rM(t~h@C8FLhOy%DhnM9igiLYhVm zkWd^zt{m%Ou1|Y@_}^aa|5ovMUZ?-i4e{2(Te8$@L-hkZT;GluAhS@bggYQRgWb+tBJMtGdjQjB7 z>Z3hS^GLc6yvgHv4CXC8OmAn`rB+Uyulm(C1?M`ovHYIN z3K7o8@1Nb3zxO~xfo-^QLgkNp^Z&N;1qSuy<(}NMt_SqyWQC0RtrX_m?gSO7O7yNdQPC>i9tkMnQwMPzRG|CdKvd zk#oM9q_{rM^h+7hMQ;&qmCi9p8~{wFZz18&{iQ_@?5~ymTC5~Y$beGAhptR(py9M& z?Rf+HE~tO{pI0Jg7X-eMXO?F8g{2HrDua3c#|jvkeqhwxxUlPs6jI}p*}!C$WBwc) zZUwkeKA&n&|9=mM?cWQo)oZH#V!8wjn`k7P%KFUd=7@q7!FK`I$8qCR1sW47e#+PsXrJ)NnEt?;in7jSq)x3&{+`x+q69 zG>U|4U=tv$01ou~r`0}%FO>Z48RhG`#==!);@rgic4IlivU6S5S|zJS5YZw3bEz_} zJ?Z>NxJ8sr#bHdMEUx^kOHxn{ljOFcYo_L`ZO=tBzyoXrmwV_Sl$MUVC1M44C{>c%tK{JGfZ z5J`elJOVMA1v?d&x*o zO5_G+v5A@GU)Wl~4fZn_-%tp1RG``+sdAO9F+YDvAa1aini2Yzp>ZNhYs#O2wpFCF&TN$%`!7jo7e#}dtt3Wln7)T#(3odX+Gs?irMyz%ooewXJTd+*G# zXlj*h&Vq&&KqSzs^67p8RH+PB4p60;!dx5$oTCqTAaDis(5D$*j6{=cq?Q?`)#6;b zmr0eufv5JzfOltcJCfbUQ~Hs8-OPYBoOxFxNhK#$FaS9`I{YffN%KXldd#B)Epbum zdo-V8memTbi6~7@5I{aTd`h{X-5jFu4$|6!dmvh;Pw6imn?2CSfgSJt`qwz%=@6GgP*0CEq{azr*{9RxcuK1u4Ef|p zX7_shuYJ|O+RQ(n$ckLL^|ZL&!9B+$(pcCc&yX5Onxv3O;NW}1J3${l^*K2B=a>W= zf+6-aln>8)NPsspf62fIO*2xpmvLpG;uE(Hb;lroO&E41RGUBYHIL43nwe`dy=>-i zh+#WX#s;1e4Jm@5+kk5FXOr$}Zb-r*Jx`%D7l{6uJ3~xL1sle5_fVYDlaFz(>KF-; zFB(uPNxbBCkrlnsj$;Ujr{m$Z=(jHGSYOous$RabIHMEZ*|Y{cBOqYtyhwnUo%256 z!JQ9XxLY7wsw64cf3#LRo`bCX<}XWx4^W79rZt%J*v|fm;Tm zjoy5D#8B^V^2>=6a|v^h5nz_h87$clF*%U{Q>?Gf6L#w_%Tcw?DrBy+I}Dchdh1-O zSYAA#%H@F9!AEaKx9W6b5NNMNs?vyo*Yqvf$WLHE8vlwqq9An*Ii6h~H)j8}SGiL? z0W%*U?2IVNMGqDjNvmG3%dYtK+4I8pw75}!r36N@r!Y-NWD4qY@|rpmsT|6@2QJr zSC`9Oue8zK(pmaGN2d@XBT0Z~aE^>$9t*SH5aw#tAT}v*M_uQC>lO(Kc1Om3>JtEkDePy=qP`SiE8BCt{T`s@C3yu zEnlmvF-XGhX0zoe06QJ5XwNteLBUWVwf@AV&iQfIg4|+VSoVNk@hN8|l^AEg(~Ld} zxJ6L37Es8G+UGYi3B0ZEDAT=# zzexN^_smcfkjFi<_Z5WvDBj4{C!>t=e4>Nhz$i&JXGgBl_x#**J#dZwPaGKhE zd~8;wWF*ZO892#QJ+!R3 zzo064<;K?%QFDoxk*RB)kstG&t?|kkqFLqjClXX>cRC1Z6;fA5VKzewCm(p!+r>jA zMx5i+Vq*QA>_(xuE#21Rg$j*J6CGAARy&q97ybveZ-IG}-TC8b+rf+)&x`8k zn_Fxtc(M;BbCG|%5m1@I&jcd6Fo#qCR*LDtrolPwSp;9rj^r)#AD8j?}%dt z!_;9QyTWh3;o+3@u>?&%&$t2(Ov)BJBmKh37iqVy2QaThx{a|kIEY~AEd=fQ$xvtTUMv&J^86rzvRXruWl>}?VsEfK@MTp5s9XQ9`ot(bp z?3)p$Y@1O^BA>cmbyIHf=vuFtDf*qmtiK7LN5t*?%Xneckh9->@#tL$3Y@1bpsn)Z z&dq4gtwamBe{Q{$>&IUo$*lPlI?hto1h>j2HbU}5%qvtzF^*DjqZ)tEDNp|svC z!NSyV;<9BueR`5G90mNo{$^kw)luvadVDF)5@~cmrno!snvw)kxOJqK3rtt&YMlMe z^K@xhwl!G#%*8&k=qigwgp_cB@+K;2TL8p0ml_;1b}KP=@JXU%KGVwlTiQZ8zquW-_-hrgzUU<;@?*?y$jwm~Y;)E8DDV+L{JsB8>%#kFxMmcrXou)J;M+ zim%U*)<2vcz* z0X|YNmG>ElTeLq@D{=`5{qc~#6%LQ;wp1~4FF~-nD<8h>={=X{0i-L5hNV+$o(F8b zmHl88a5)2954d3pbUi7`!tjD4fs%QCpOng!68c5WrNvsB6%t!5yod-uxYt`?69Zl zgZw@U77d4TN@z&0k&XVy30<_7fJ8G48FF*z7TF#b5*^-%B~%g8sj{WUT~K3_F%Pq+ z6>Dgu_$x}@`^n`iat$6OQ7$TIBM%eP*Hx$ZVU-JrNaO}B_(pXlXy~8H|Irof`;A=P zZLd?`-n2tp_~kHP8E-!!E~^?lT00r7d7GJeRU|e2!=T&*pw-}r%`X6jvSfboj>+~` zidQuD*ABI&60eJ!QBv!7IKU``O{#S5I+7>uFsVsXNI47>Y*YIJtN0=9VXRyfEn2E{ zb##2D5ng#x>5{ur@O%EO=NXh>)AP3CH+-fVd3FVI@V@h&=YE#IyH86B-kR%~GqAkw z?ilN6>8<@sCu7aN4|@ZqB$-NFXyrf^Nhz#BIF&*9MJ-j6JYERIap8`FRc9a z(DN$d@pnl4{qg8?>{ZnRfCQ3xxU*RN_?k%<@|&nDhzqn-zquw5MEcap}|~RY`oz zZ}tVmny0+Zcf!VCA&8S60l^Y;#Y!)Lr&V!KgmVhPiwPzz@-8oU_sy%ZsPA2th?3xA zBYx)IA$CO7;XpNA*{kDvP)s(i^6f0ko|3(jfV5jM0g@kIXn`gqg+8y1K;<`!T(P*e zb`tkE-iLd;I_&!$3CB&%$~}ZYyv`EzN5#$Wv%2cvIT?!d1#s_iQXbUi@%qEhWy&m5D zhShoRp(w`Yk_?$lc<$%y!w~N#Tg~4I$6f6e@+8PJV)F%PSh#_K*_~HD8W!z*!^zQc z3JjZdJ&2L?A_6gYQ$td-(KOcMX`ct(E8qNX4#8l!-dlxW4ptSs_XXqS&8_9H+Ga2A zpsP$2jOmqFn=xO9DQtXl;>`#FKA6MR+r`O!{8BS8TgCP?7w#qQ#5xl*~{4mb^W zh0421t#s5{4JhK6Uy#xejXb^+{WU!9X#w!8)zefqH~CKFo3*y3k)&QrFq^in)C?qyA zoanx@8wPP4Kq8mjOjp0_n;v`&hf4lo*oBpe&go|SeV=tuzh0QoDeN5%_4w#w&s>`$ zJKnJ0or4@OYq_D|c1f-8`Yoe-Gq&$y;r-6IeD~BkX|13}5~GBhE(6?bbwDdE;BmPHL+Y{@vX?6PV=!m-=g>Ebz3{0OyHzET}+aRO$GWv<6$dl$>Z z_1!WyBy2RA^Eq+aNf*RVLRD0KT9g#MEfM!sR#}CV(qihD#6Y$-V|$W`8yTO4FB~XC zp)mzmT`xj8cZ%3ErDATtM$Vya=w>H7s~gAkLrb}nDjrxBga*UITcnsI(hxDu7Mjn@ zB4<4NWO+;Ibje^nk6FysFPskN({~GRn&um{4gS}-pETo zTnTRhUXm~-?o(2xde_t~%gPBaW}%KHWi}NW%ljyMpiQkk(B%0&kX@(Bo4YB}HFO_? zDH5X#-fdQwO=Xv$Q7j-BBDV)RNM2Muuoze&NS$Uzf9wAzZe-7OPV&3{Tk7&O?K-87F&As z2avCnnaRySH_;aDN1RaHV!ApArO~;W^$&0j-`%%8HD3}-$by-~pbMpimokI0QMOUE zoTiKgl4OmzgtmYivghcoX#Ew4gko#@txl>s(U02`U%r4iF`x5nSLjz5q8@td5?{NA3Rea`!*FtF< z5!3Icw!#InjlGdEHvP;Mp(7;&(PstEq#;0E%%Vm6tGQX;q-(&B<*?tI=1ENp~_S8xbJj(2M-;e5fU9=Nj=IHiI{_$NFu-Vs8yGAWj|-H z+ljynN(E}nm`#ugd+b*T%TeA^w$EFizpiLo|*OR2=2 z+-P@&Ozti_G}HuHJqId&Pw&geQwX8>)IHBeMear42of@)&OhTCKi+c{{`g1KPmVaQ zTZ-wi9=Hj>e2Jlc*0U3_sdN$?C(A*LWF5@n{O!(Orz?sIc>Fg%zj5+x6l%cenPn(u0c%($A4H%&u>te4e* z-e5XxlfR8^Ia-`$zSCM#61RD2{56q>idOVei30AhQKeCx8GPz9H+7(2;aj_P{eljgYEmrY|yB`(XpiLYp%6n>MietH}rQmU8;_qCdF8SLo%57e178SA>X#hwAixX=X zFcX|!F*(`XKCQ0Iw;E!)5nk&7-n%w*hOJ3}VoH=y);JMKudDPsx`v(vqrg*^QulRX z1{0AN`(U$FNsg=63Qn%eu%YxHb7KWksny~c&O~}{4r$3YV!V=&`~H)AyheQYfR{ztRV5u1u-Ok&Q6ZuM#2_mWh!LXbv0AF$Q9fx8_R;wxUW4HuZ ze1zZ_*s$yy-rRHaX{xGFwEJ!+!VC>Hia5{~EnWE3_w9NX1+C3zeF%4L3io!t#+2^Cl90Mkn(&u zDlKyJY$~J?3!YO2v=33JA)3vvm0D!s!Q-b1CGb0z^Qf|EpR@V}RAz?syHIk*>iYRt zFeShvtyAR=X?&|bUIl^!0W^PM1NZnufg|@z?==lx?$>!ka1ir6keE;5YkS5vu$=m4 z-t~R6c8GXY*!7y*6;c}hl%*&!rdz1aVBo=gWw@1lPux}c>j7+=pRVnTA#MtTMp<5yF-!N|dXcT36 zM-w#lk-J9JzsyqcZcprv4d>A!3uV0g$w0kW1v5Al_O+^)d2?t616tAOBDso><5Q)E zTe$%s9mix+z|BWu>aQ{wUNO<-jCt1hGPy zUoqcD%b8&Vp-oQaUlC1>A74uRaeV=>U5Xp^Dh11U>qA8JD7t|af}38c;2`^AE{YO1*vlfS2yk0yRh{Kr$6C*sBovZ3%6cr@tE??{GMHujpmo+p1R_dxwVzOSS$y zD^g&EPp+&g?hSoSSK@};5#*)mpHV2`Z<#`Z@^q%d!s~wFI_Gxz^CHjQo$;3@f2uN&$195T;qM`QtCqpQMV0gb+y^Rrxm4*H_BcY4uC zRaLo&VP7z>cf&j(k_|4Vbp)B2LE5lKU5X=0j@@Hu-MUYHF=1n7BiM~m?9r8oJh4O>m#6*|a?V;Sv=~ zIyp^v3Re_~5iF>zXh}(E`51U7dK*oj7~@<_Jr|0q?DN-F>kKNx^lic|dThQfEU-Ri zzlM5RDTOt$s@}oA9{(r=DDmmz%_sf=$xg+2wky)!pvf6Xa@|eR)itw6nfTSGC&+|| z&w_7WKhZ18^IF$qRl!@H1wNliFE<+z5X}KHS;+!r!k=0OG7%$xSZXiBFvZ2FQ?S0e z!`&jf=Z!^Fas$^h!D&60RQ=v%_BWrC{e1oh1Oog1#MJ1G%445$gWjHcOU`;>xJz--r;$#OfFJ{h;IM&Kf@YtcG~q3) ze3Xsaam3q&av zkx3xE0AeG}FMbdKt-Ez&_^`}rEjd1;(Oa(CkP`3 zcx2uQJBooU^nKptzAO<=S}wyer;qfU(*%2n%Y4y=K#tz%Q}?=HC<1cw^EL~}k2za` z_W7k-hR33(S;=gxP>*RH<;eEgg&@o{J_a3{E*r@~o2eON9)fH=5P&a6L)?6oP)I~% z&x_%HwJC(LK{SHDsAF&^p_R0DWRUAdcSvsTFk)edA%0n#z>7xIB= zITF~66SIVv*m_a3#h564P$!fQ0H|gVT!o&^=jGzlpS$Gcgr@P%iFNCNG^Kyv{g|Iw zUO!;NVzSZ)z~98!=sUG*!f*X4DmIuR?;+rx5%?!Z&;a`}R1K4mf$8z8lkT4kT15kq z0|o4NL^Xh8|CqwgzLHNXM0|K`7!%kOOU(|WdwHX&Op>3_mWtXbYu-F!UUq1tPfvh2 z$`KLZ-$s4RexCX+6>(Z{G`v+@5H`QFU@X`&m&uQb9Vrya^Q|g) zLxEr&-&J>(4j9YO!Me}|bLGq!Z1u>G_se41)}4>zTCQfKMG;*orMrQ9MyaFIs7mYU zCFDGAx0(YPWL{uXFcK*9qD~*|QDNf}xmJTyiq1t=@aK6+B)3qvcLJbuu2ASs?*6gt zJjZxkdWp0sciZUn2y&T3aA0Qa;PJnbFSg#WVqR@u@Nh%o~AmgD|;#HjzGd@`?wjEPnyF1Jq(d$TOuV%*&gfS-I~Y-7^H)5NC{h z#a!8h6|Olce7FO#4CIinCOn|p5%a$Ac1vCfN~2XY4$wJKM-^djCHN@671@92V`<_T zPwVq9=IPCwO{$84aR)XL;^47bGvm0S}L(=4xR4lJdPht_J}qze@9rm~{RUa592{9NXGthyqc>65gXDN2S^3I-V=NZO zcvNc0`>E5U!7-s3<>N)DnPZFuFA2w<-qvTsP7_y-lY`sYhH!LFZX}GlK?{(7+kz+v zCWu=Ho{67A8q0W<8rtXzM#d8`{-R~&Dqrv}63s|TfR_B@>=1RSB`>30OE#1@6W0gs z(}6B7L66uBZ&5;NMX!6UnGdoFt1Sq6!)PBJ(Y>raUhKQi9n!QO>YtmHvuafGljEy# zJ!~EHWa|dQpqaED{IH*O@=6p2r>cVY%Jf09s$$^FHf7p%*rrYKyK;`URk&pj zzjQOcFURsdT#Az6Rz6y8r25<0(_Vi}0*#SkObRty}J1TyDr zu$K4&Y*XH0SHCPYJg)3g52;WFl1{pVO3W>!Z3nWo?KY=BBomJAsdIc$K&1lOC~Lm8 zlVuSH2ry)@)iK>e=X%2!S7c>-9VDtsJ+j&9E*WpUB$_wCe;NWbN1h;OSdR4Hf_d{G z{6#XnU~@C-$#0x)9<?+D0&^f6i4aRZD96lwNd zi1{^IOasoXykV9&b5BjN{UK(KyMnO<5d*zzigb+g(OrvyQMZv{M5d@U)*Y$-kgN7e zGx@RgAcFtK952_XA&-faI%WWIOTBwWT!z%SxP`@!4Oiq$$8bBQ@c~t%*z9dD@Wipy zrVp5=BAL*kMbO@ivqv7QiyG)^KXE?OHlj5c3WJ*5_Akx1Ep)Q%#Nlz&njyIZFc`$6 zGEE#Y1t`%$U^5uR&Nk?mW~gdJoFjd(MkuJLlgrqF9WLTKF%x@0mwtxCx-a2P)ww4DhG$iM8uK^JZk)0SZq-(| z#DR*}{fYC)^^GK|`xA<6CVl#BEN5o?*!xHa)xd^8O$r?XR-&nQ;32M22T>!kUi0CQ z(}@G}5|zn_d8qE@Au_{iahAReYM(M6WG3sM+Q#(wcSF}Rv~Z}Ar9LAW;m|2J&xK|P!o6&^}v#z-0@@|f^)@&*YU00un}s?yYAo{ zQRzAr(xs=mVE&Wi5|dOx71@?p;reCJ)kB3aNcl8|encOzXxc6)Xr|X6%eBa~*=GTAi@Suw;8}gIRqUJN3W;V5XA3q_CWc># zQqI`*+$uXrr~)@P>+J%8;T$%6RMp4ibcRG4L=zlMZ!8Y1dfs!p*o)Bnkk0MyvV(i~#mFZzbYR&G|}p%<15Y-52U%Y>sl#GRc~|2R^FRL43aZV$)L z_BfH{)Bv5)FT6HHLPpvN`pfr@xo=r+*;X24wg`xw)aR;pvu*U+!ftsdK-q@<=zysF zCi&8iSnA5_ew!)dC>Fg3Ua-JleM^(nrM6}#4BI>=eYS))v|SxzZj){x6&tEjDzV>B zjw9uywow;ID&>sw%+Eh5GVw1r6&pbO#z< z*%<Kds7=El&2Oz=~e&ffCMPdJxX`i;wnd~Z)Z;<##7bP0 zi?HVJsBiT^=BKLlnEr)GP9l4U`Y4PhgGOBzpm%^VU$Nn>@DK01-4pnWX_NgyS&x<^kf(24 z3@Jp($-9@*)l|B{M;wDPVHPBUZ%)L{p~Whj3F_HdO=mX#Y4P}w=PlK6*e_+X~#2HOnz%KKEiOF^Hv7}L>D zj@biN+pt~TQi9$NUb{YQY+=D@OzQafd^H!ud#@o}uzi<818|elbiPC9TOX6(B%Qf* zOwr2J7~>>uDua=MYnusEGt~XCKzU?%8UmtC*=`BO=lu zGcQyKV<5Psf=g|oE)ZhQnLA$Md@=Xv!`a-@Z-8L3nYd70^e9$$r^}%zT6pG0B3e-v z7xb!$X%SbJ96_V=fGTs5lE(_dN75)?l}2MGw^6Csn!-8H7o~pvo#$nZ&2=2j0ziB( z{YeyJLyLQCRGN*Jwi_{24SPzxYEAphk|BRT9w!koGc^Zs7#h0;iyWNGq{N;S4}lhC ze4c@hdf~$xdBU4cM#Ba#?3KjcW*cP7+pR7`XAl>cl=P9|&99wn9)S*C(aejut?`iC z!`Va}C`nW&uq#BZYl|raX}EaB>Mk5Cd2&H>9`AN7B)%fJ)*}a}>Vyxnn|# zT_EWsFh{aLCf4}af?eE+IUrE9Rs{@NL71Yu*7kDI&6+|!Zw;kc*{K)!fcS+tC6j9@ zxBIV0ATGe3wyvn`xuRx6t2dR_pao)13W~9!@KAET)~4N-`Wn^OG1;Pga4DQVOulx$ zS_)Sqc0-h{KdW>5udWlkg!ghCc3o?2<@`e)R=+8)5F=)mD=b{L8r~+~%%D_3z87y# zS~kLJ7VLv+V9i#6c}TrEoksUZO(}^Dc507Iq({W^0RfhT8VcVn?q2?Hzy@HJU^w~(G2mmxTzx)kHdeB^8NmPY19E|2NW1V;W zS52MnFI1iP?W>=vsEk#4R}MtY+8xvR*Zch?-}E+X&5Qx4zn}i+pNHo1Gha+-o9SC! zwgRhPU^8yxH(*Lo`pq;Ot|-R+^i^X`F`wdnMq59NG_wLjz~EhYgMTLfAL)Nu;D1`+ ze_G&wTHybE3w&8W$a|2OwT!2ZrCT@sSZvQM}m*aFNZhsjoq`i9cwh= zbIly(#(qzIx_`R*KA7VbJ5oSd>j0aru(DtCpW@LkC+z=mnd3jCzY^sC|4r~OIs85C z-*Siu{<9$eQ4as71b-#{-|O&SOYrxr*p+6BE9~n3uXX;9GWhHIwC=Kz^AqO7fw;X0 z_bvo~Tyb|KUoSqKX>OK5CQ$HEc&z{23mtl)-n6%En(l4&-~TVW%AvaFUGjEy4t(Sk z)PACsRwO9~I?QI&TgbZerTD#Ny~T@&UdZ0~pYT zNQhVliQIX6Z#7UEAAVHLPxO~vOJb1J)Lz&l4vf!FjzwAs^d{@$PY#y|R(i{kD9X8Q z*BX-Gme2Sf|8Shk!X}ia-O730bH|{T<**lqd8YpTeDb8oUXI3}r!b!kk{6_)NL6+1 z$U>b|jfi~C%fFpoGsI9;5i{x>^Q%UlVG*n=e>+_r`+xOAy( zb7-JZIYU-s(Q=xNZuDkrT1W8Bn5^nyDLG%JjjT8n9IukGfy5Kaf7?@{w|Yj%)Z>Fv z7r9!OUfEJfI$MaoJ?mOS<^P)=^eu>%vb%F=8>8i9!Zk@So`!GCj07LroU0h=qiqk= zKUCH4aQeS+zkk0n|c;Mpd)^y|BIZ-xujUM zGFei~lFtpgySV=?&vP$g;fPQ1Xx;$!VNUUY`M~X{5i6-|_ekAkI;qO^acJXOvae)FhA@jorD-#34kbj;m;q7rpk_4RhP zYrAhgJg?+88UAtV zMs#B9w?p&)Y-W(q+wXjA67K5}tIA*hZBClMwB}=%T^>(P2WC9k56(G0_;3C{p~z0g z#jxD%xK$#h`qsJehd^n;M&eVXx%a=h?mz$XXhsiDZc#NOlGZf}HSKC{tH$aE?w;X9 z+eCfo`=_PWv0_gZDzhm(=E-Pea#omt$KYqE@iGTl5@k zPO4*2N;okjGsr2D%|Tz#g2krhd<@~>_-CUD6#Sztp9x3)v(cvh{XY9gtwWlYnvuaA zn#6`_*7M;qlRovoZ94N5o+RCma@ijo+0>U$I%mMab=iH_xPU7aL1EJ==ZzS+*$gx7 za{78ZDK*bPDc$bo_oguyWaNcOtrTANqOrHq_omYK(P>@!2;cKE_g>A?xl%Xayl6Z- zqQ?OYA;!#ld#fTiwau~gYN#Sto$EwW<$zItHq3{-k}CZUrn@dd<;Rf3oA%6YrHBJw zu|GN7S6}N7^{bd@Jq3&#@Vq_F;l(cg;WLA^^}hXD-shH#Oq?ziJPlpS4W7L`{66LP zKWg_MAHOIwN)L=*4H!B#U%^iJJGd!D`z=Ek_JH9lsnc{NNgxx!DY(vN7+1hf>nuUF z*MBU)K_Oif3UN7jqm3y%=M4w4K7x4>5*vxA^VXpZv9g~S51zAP=#4Tr$_i}CDc`Ms<7?r# z_eZ&J!e}7KyH2I#_6)Bh;r2B(+PU2Q1Ik3Vp{-i7Z(IT~`A4))<5a%mhmp|`=|N6s z<^yz2)^MZl-A+FU+>|#)H2^5;2jl6;(H74xa(^v0jH6Fi>X&!39R@BeaVcI5E0CNe zmBp&Gb_Njfwp7L0;w>^ju%Al8*k3?bcTCLadY;qE`Xw76Loa!2-mBo0=91-(lwkK= z+Ajy!3n4X8o!VgOvMlT|h4!(WKHaN`NcU3-zmzuyv;sxS5e{n~v0`?3z9$L7a(2u7 zJQcS&IQ}`Kjz&s)JmtLf$3)Aq>Hhj_CExCE_Rq`sqgtMvE_j-#`%U1{|N8KR-d5@p z5}uDs_uNd|n(l*X5HLnE@4ZT@MjB1aPYo31eK1?{V!OO1IBmr-ALr!9O9y}H$Xj(C z$^coXh4=?>C7 z7e;+@@bIy&0uaJ6C*OLAF_`Bdha;rVRX(VA_!&t3eq1(i|CZC5LmWG7Kmfrsa0Qql z9XyX$yy_Y1Q`9sh!WvfBo<{lsvxo;@O(JzrX530T)4xfkF^?PGJ!`RQmE#f_T!mPW z=s7nWyA^W9*Y$^Wp>KZAGXsHpU9OKbkjAgfF?6qdn7#phjU{deHkA~D{#J-m?s2`| zR-HatNNO`K(ORTNfqh_;DN1|bm*>MPEr$=#U}&l@C`!}KBiBAaX`~;l z?lS=WL9FrE)6*>&A&2;kWqL!pNY6YHOS*@0(F5KzZX`1^$xI#4sS5!jD`g^S?o<4Z z2kZB=^Xb~i|K{MFXo*5*tlM#(U^KO%{q+oCaAUB{Ha}5F3~&Zb6ed%qil>QVRqd;o z?lUlMHK{45m~CbDU4F>4t-qGQsV=@yBvVBs|A)b!!DmqO@j8#(1Hc8G^wQ%{SP zZ|pto0&X4{-DsxdN$g^JZrad`IxeFk3#}<2t8?Xtz*AKXQq41&)mv(EmA2h-5H-W~ zp!b24%1=fGAWg0>TZ5&&AFSrw*}zqdtS;S=>}O5iSb2P8lizPXnb=*t@z$bM?T(EX zamb_X5+{CsnY*(#i$^{+WO5e(hX9IYNp{}GBZ=rXhv1%D4LMXjQdr+~CFOhnPY&t% zBM*j`2YWiLc0s>2Fes*KTZZ(E>i5g`Dm8xZVtsdnf#?%J%P$t~XStso8wpq{R_bpL(FB~44UJC?7c?t-XPxqP z#VgaA;*}vHK_U+xT1N3xf@1R6I2pK(&Q);M=5Ngf;m#Qa1@bBv2QZAN(N_^Oesh6Z zu|VdzQf%^Qh@HwCsEo3|Z2=V8uyldgT4x5hrT8Hw|CzCsq`{f>4e7mzHx?~?@1bVP z^B=kph#eeE&+mO-wY}z#$sKti3qSrZR~SnS!w!!g(fM`hH^>j)A42zA1-O~g@z&Fh z@&)E&QYC9~i!64y9MvkV_g6k1pGSB4CP%w$CimmgC>4?Y)~e+O-6~AUIIfZJ9iQw_ zi+o5_93F3kZ1c+;66FJHM2w|rZ(e?wU7c4BX(r~3_iXrExH+=Vulz;zg z`aIHmctJR|IdKJVAN=fB8^w3)`sjctDIkWuFCq6n@Q2rYa?6LObje=*w|s1 zD;vvVoLTbFHoMt+cZ=3iHG1?fvmGuY(tHY-#N9%zXttjY1I^R z0>|TgWxVvJ>4J1HfHx=TN(p^3MBHs{m?^w%rf&u|ne%PP4PtzI{`gc`&)9^GXXn~U zSp%{y%WX2kFKuHaa$;uE`qz`gk-U6zF*X|Pkd20dK0V#ksuFfknXzYv>M{)vK3@;= zh!@k1m+&;%{Jsm1NMKIP>L^9M--=anxjyXnE|9c>;1YBqNyPTS=Nx`Xak=CroIccI>l^DSh&wg!Zg(Qtu7jhniZoxIUA>z;jV=f;MAk z`>}>IxK=ck3KTBbXXH(hM0Q-mKAPO;xY8N%UB$z3?)s0utl2xseBaQ+&0P-;h=v`1 zJ=*`|bm(=P0$C4&iXDCWF|;&>xnjp=j-WxX@^QQ!g7QZXhc4l1a_xou9gh4h()LKo z^TIxJ>FP<~Dynij%QweW_qpUD_A&MZu@wie7KhknkWRh) z)zJ|dEm~v<*OlH+IO8xDV{s3D2QqP9?mI;X1_q3dK;~t{Ytl=6l(u9hhcey6nL73> z*H-@C9O`ZZ&6`5{PM2%02qIjimgCz=PPZu86dYY?+ncZ?>a?0xrWV&hyd}# zc8D?BGYC3JmNlennHv!$#!AXA7WmJn)gB16XXl^Lkb&f{mT^0XeBqh&r(T} zaUhtfp<&v90&AeenjLivpai|L%fTU8^SaaxrTo&$d({4TdZw3j@kGs*j?SJ+!k0V! zZKufD?04?b^H7+{X4K6=965!Yan(WD{yp4XxyJqJ%>p$Xma3CHbOI89p>f|__lC4K za+Ka)8fS;$SNolQG?&91G>!yni1Br&N1NqMQd&al`X0SHW@Rm0i9! zgz6i4CWNj$xRU7zXe)QVkhPyvl5x=RQyO4AuYfd${_h>@Sp9Hr#Y&2#;=)Ow&E0LJIzy3lM9WI_Vhhj zRJl2V9cs3JH%U{RO*=G=WJMw0K6wbKli5CQ&!iO7#RGOyd(MQxS^+HloK$5sY?7C7&T8cPksx7_16B<%AdTzv3{QIrm1 z*vAVs7NzYtG)-1M9&NCM!5mdv_}-umXY#1uAKcchs;$u(?Cqjxqw20$y94V_tye4R?XGbkox_}LobKDDwG3a(WvULJYGK>63<2p5DS8B{J-F}z;;Ks|NA49gJZ%7Q`}+w+O0B}Sm3~p!6#+X93+Ym0HLzMur4o2PK1wV z97e`@SW)8sAZOEjl<67oK&%C6pNXi`ijtmJqP!`0k3S>j=_dw9cv6gr!aw8|QHxbkY4s9!h z%`>(paX8rI+Z9j4EeK4*lG68SF(X?lkB{otSNZZmf@Rvu4RvtH@wv{X8p@27EaEdB ziA&;xw@b63AGf@rB5IcM9FvUwVpaz(ijtD@`-B zrLxyIz=HM}8UVC@o_YI^;YHC@nv8s$f`;iV#1^uxcFSaL_==9qW5Yd&%e@)Z6pMDJ z&Ul6!vbly%zkGwVLX!78Mo7Q?TWipR$_JF+$&O{Vsp1yu(pCO(FoX#^s(N=Lq&V!( zapnxQ6gQ*I`S46fTD-4VOj{$nZ0k_XqgQDcW?+b-lOIVzX>CRWji)LC6fqQ<29ESD z;5Nrm^_~~7oVuk_1H_LQ--^1s{R9vg2Hy`4#7Wed^2&e(1i0DS$Cp>~^53aUF88wE zN&C)Hr~3L-`7H8&CAp^gd`){LF;{&t<@WT1ZFr~I+1DN9_7_4%)Quxwia@GJLR(jL z*lU9$1Sl<#>fN8`nV0czc%T zo%QyEV$F|3>-_Qe7|yA3edxedZ+psybt<`C$7Is~N>}X7_Dh-(Hy`q1A`82^&k}-z zn6g@Xa-=kqTLzbs`3z(NsjF>4*)t+5z1ai%B5o6x!oDRM2OpL$nY9^+i5z_VI%eox`V1` zdLfeWlNXmAb>3HqlTNiTR&0sX?WdsD40ex*UOu?0z_KaIC}mJojCIw^fXnY`r213& zvN!!(nWhqWPjQCbRkxxZ*cXfJNm2>5hj#DVdP{X!PTEdo^cT+yDS@5HqJ2J((w;l$ z(NW7w{Ag7TB7=E-aJI8R5WQmNU1Z6WBD?r*Wa+@s`M0>)p(M9_4y#uzO*Y#+AJ4L! zEtyv8>+&op39ppcuY!)!d4jOX@pT0QQ|fYsv@W^_l=cnECi2UiUW?=Fz)60L+)hB1 zEs1*c2cutLQ>|Q0PgnLADvkAqE*ecwjn)NyfA!kYEK;vT2`Ld4K6v~K2Nz1XUe@AF z?0(Qk!`RaGZ7o8`UYW%Lp5<0QE>-yKkKy!RI1b-pgrYn64pGYWMa?H}_-hKlOW~57 zy%jvL!QUoK&X*%RSb z(bJIn)^%<%yD@6pVSVLfQn#tl#hNWr?b=hz1R&%k^z1Wsc@E1mrS21J7xB|=%H)|$ zZxn!y0P6IsnSqI4$%*>Iq$kg*yL4oI&0ng{OZgRz+WEA?*ghH_bWVmuiRFxVBJQFnzZYUC$E{uO;qx!9?DT?G3s zch_myo^9=&`z8?~LLe&kl-fnd7xBl$9yjv4_OruyTpeTEe$`kmSX`PRON{MxMGqRA zq49@z!c0CF`&6@myXlz?c|BAmV)SQe!2wqanhHvLNP~rN-1lXcRiJBl9(=|JEAi)g z50qKdpf_DmHZ!wSKR_cKMh{3id}!#XX_~X{Y1q1E3p+bfzL;9OC=KGQ)dDM0sMgLC zJZtf#QPkgH^J`%Mj+zk={JW_-ph4)j#qCj4j`IvE#)SmN^uWE@K=(}n8fnR-kcl2z z+AggF)_W1~J_VZmQP5EuoJ}<;WxW>lZimJ1+xWD^d}O4rmlAnkoX~{>^4d&k5B~@{ z#uJrqo*J@TxpO*1+P`95{-_^2NK|lDSV*e493Ro~W1;0WSCr(}wuo0jRy^=(m>JKC zN@wa<9@&NU+8dSHPg}A~N9@mMe$CQdj4mpBLAatAbg4nSzQ9GcwO#hdjO42kaW7W2 z11o1V7d_(jzV4hJBl*|;E2EQ6mASL46~86oQUPk$+G}vRKlPrKu|fd=9zeV>Bytg z4^c$tT3wk)gy1IyC!~DpO>VA9sw~x`X56!^rR3tPq;L%i)S}eRX546g{Du^)Gr^_e zRr~Q)CVD#wMu}{RZO)HS1Wh(eBBhQqq3-VP>;n8jx**~j_PV1r7d1T@umo5gmGz0+ zY|jNLC|d-i;L=JaalHP|H>XFv(%1h{p^F;L(nSxpU)y?ay~!q+aOvuLXZg*S5F4Rb zp_nBewDC6hw? z5x)famh?6^Sw5h&##(9G$pz7og*Z0&7?;n*Tl6ih<*~cfI)dW5U)`Bg*lzFTWspY$ z!A0bcZcwsanI}tbwYS&H_Gh8om3M{u*mTdSBW2&&BI34-mO{;Z3{bK&c|%V?gA)?TN4(~#rx_2Fs0&+g3D)e zEi}okolx4n?|Tf1lkn%WFzAf}ix)0bYgIE1coVr!gGs@@o;N(=6H3H)pyS;S)6RhW zl1S&>W=F?XFIL);2T7@Era)dlclgOqW7`T&(N~MiEwrF9^GwGz8d(kW)teD4z=vF!7qm%HmC$vV>&JFWZMs6{&SU@-3;n z-OTVVEgBz{wYyQGKGJpa+j-xsaYKa*4)8&g738?2$fF>>iq2WAGo8C7CkGo*XJ+8( zdXdvk#qN8%bLXmY%E?+dI{G(Q450=*_G4WC?3KG82b7=2-kSeLwj7-#S3HCh!Lzu*U89;@h?Bo8)h{q}{B`h1w1O=pKIxhyQg{NXeCHo4!;V z|4_Saee@g0wSRl*|Nf1)94}Xb?Cj!{ zE70mXKdBPgIk5vDl|7PQAi*9>6#qQ3q{~`=e6t$0$93ZU*WfW>Sz@V z1uuC6HPNz@pcf(1Xb+%8M$GZY)pW|%!zQP-89z`jj2-)Vh**1fseeADZ{z(RZSt%C zrc_a9g@x5nwNUi6NOlP4s*7!*BrxPRK}OS&#kx`eUWRzzyI0HfNl>4f9po@UQl)N@87w<=<;KV(3KYW zhUj0`==D zvt?A*twicQ6pnxY)^UoNdS25Li+|jFZ%5I^ z{%^&Z@Ll+h4Sbe4HtW9dS8@Il8p*HKhfiZFQ<?J}8r~&%?d}Zp8H*XyJcbvaj5W>)=`7l--TL0@dX5cgKOM=b z3vlv?$7QJKiVFQXH>NY3hF}96)gAL+aT)c}BP{D;@U-4SU4HMockTGm}p z6F!ekDB#9}Su<+zwoPq|sJs2WPE`kn;J%r_Viue^Jf)Z&5_-*chHjp^9I5pi;@ras zr<=0z7VU=7X06ixnJk_>=-zNTr}^NI#uck-ACTV(@&Av#_Y7!i+t!9b1uMFdrc~(? znjit`DowzENJ1zElul?;0@77BC_zv_K&es$l0ZTWNPr+puR;jDNbkK@zqQ@>?6dbd z_nv#d?|$$5>#QG(HCN`EeGcZFV~pqdkPmJsgx*QS$Es?|n2QpE1SB>S`m}9`_XqOw zOU@|RcizZw6(U|RWAB#AwKB*>0uDzVjEZk|-w)(kNFIBw6glimc=Y@Kwo8L?RF-w_ zGSg{@Rc^Nb*@-3Z@)g6M6_V`0?3@VRzm&Uy*@yS4n%W4?p|h_uI*W$YD#7amDWz zEDiK$Sz8w6!QI=gL^2%OH@)Kf)*sn5Jl^PM`23K*;Z`ya`IoKi!>c3xZ~iBCAAiq< z+aY5JVHh=g!(621!Y_(~uTdyHEn}cuV_wVE{A}w9m~Ea%H(x4T16@B}*6Gzb49cLC zheP8V>@vzb&qjW8t}NbeWpr5z?BVV&gINnnb-uPNnrVvo5~;7 zOqQUfz<;T_5n(fJ7`rah26Py?_B}h7b~?ukPx-XLt%WDRj{kH=IL16v`=)DXp^Q%O z3)VCN?HD}VqqFIr!I|`F3yD6_troyL7TqBs{NB*9+f+gf?W`hlu(?sHuPx19M-hWG zUQ?vznN{p!k_vv%C^H!y&d@h=42U^LgSDg@A~p273}?Q{vIHG`=nF})s|v)vPdRM> z9An>bJHiqJgpO!vPEt53Ce*sa((PY75F2vAwd#HTdy4@-59($d99#%axv8f7r*?0+ zp0WLfyXyzdM~?mbOz-~u#ee_I2|_06D-|rQ>S<-tj-K=^aHy>)k9b%gv@f9$ zL4x1=ghJCW)4)!C?zR(e;)TQH&LF-QF)p(2KB_N6DP5T@G+kRnHbjwFTI$kn{EQ{0 zgng@nfBrFNq-tkrTk28&vzNWUCc89B!5s;EOlBLP>}`%}PEW6Rd~ch~atGQd;9})z z&hy+PDR=|m^Esc11dd*S%Zo)TJ-I`qB6 z#^A%^Xg|PdMq>eLloj@a<`UagpPdghw+EOt$Ng@#tunCW7v_o18azSP^9f(jT>b_T zkWalcXy?-FyldzlJ*h8<|2TwRzHE$}^$2`!MdN+574GB1!+1Tw{{@Cgz2l!+Fw|GY zpVwJ`&!CFPtH5r4GWfn_d~WotE?jBNe6B)B^Ulk#i>nAGy}(__?}F{0Quwc5&{D)q zS6#1vq8)>Fcp3)x2-t6|ZKv237+2g3@es-*WTsp;CyRNrUv^*#(rz6F5UVaS-D2LWhdK~+G%E@m_<)zP(|!1)$}7ZM;CqxML|!o3PYnVP^a=hp$l$P zL*3k=K0k?OK4&{6ro|VeJm%AW4SB$d_9oUe~sGZ41LvNZ-ayupXPH*0jq@QFx zmPlRrXnnzAQZo^^!|{pnXg~^7QFLLbxGb;SY|7bL0<)ad1{%=`*yCP|naZD*Af@2*ZZQXlAKC4#crevr&{lmX>n8KV8I)$xO34odt z!uwGf*^}q68hx@uQXCK+g8{*uDmZB7&45X_P~|{VPO9byoev=t`Tmf@#$DBCg?)NU z;wkgerAORnx49OHi-mb|nGJ4et{_C^A%`bW-4L}D|caWZT3)hcT@zih>a zOJh?ih>%8mSP`p=;4gCgiH2I|60&^@!26prW=Gw!ez8{Zj!HgU z99$+4fm22E7m;~TJqdL^Urx_Q<<%a25ONrmu@QDlplb=K%rb!C!@>0yS!muq>)lBj zesFL8xO#gD{M8h?sGFh?J(Xl)p;`H$)%?&$GP9%Nx`UY{M0D(+u+9qNyR!rPG9u@a zk}9u2Ba?gGerMPbfPJy<0TUxAxf#g{W$!96q>4lK#9Nuu148xEr6gj09)68FV)G^B zqP%xGOTXW8eUp8A#UhifeFV9)T(bV%Vi!KU=SzmG-#~r5@8K25cWQp_HdhOVG$8mz zt|>eN^AT#jU;$tkZmh`yBKCw-!AuNyyN5r>D?38J0JNj@5xH$k7u0tWAmTWm(!GM&d6-!r`cILJKI> z#wez$P~1lPGm#|uoGsjgms42#91Ir3Xw)v7GdGVb$ejxllULHobF@M9Vxv^$BhA?f z+^6xCXdQxwajnN-1kw!#SSOykc&G(0P$J(A?Kl@R%BIao+;E~cYw-aG97r#roGQKK znrSD(N0Qlhd}&+btgxku{3TLG6vR-bE#J?%BrB1>qJJ_w7Tf6HTh}t3zOE4ghV7j? zY4@Hwz+5?)SFTcM?(T1zc4)(CcuTeOw;x%#66{xEt@12DkYE zov|9SwQyo4aj9tRt?MFkbDhZ6II_WWxAy^DK2g>oZlv%um}(->%o!LuN4mnHsYP56 z`aG=#^MCF$aD>aI!=(a=#HxW2zU!RVXOPyEsWcW+!XGZ%My4vsUPF{#)hDglJ@9twa` zr$w43}!hRzvWc=mAO<<95!OzMnl|P{y3AlzXgeT-h`$| z6<~@9Y#F%cB|`<{g~Q>b8!_(eZCjO0p}3KO?Pu4H-RP6;%xE4@q55Q*nZs_48Qs$z z!(t!93)s-v7jE%C1&ilQG4H5cw(v*)%3zvd6a=Js{2_NhUGkk5`R@6HRFu)z%@ z`zLG@?2B9Vs~K3g^Tm+b6_PAD1c1jKrEPELON2M|NRFmmdwL~q(5s-RgiyHgZ7R-4 z^73qqJ63krfKd*Rb@HDcaUg20U*>>7%)Z0Ly^YXB1&qN^V=r#%zNSpexa||XeSO|i z4-*z13)LP+s>JjlY%+Y0oHdlf zw$niz8s(}r5=-4fZ_I?W-1W-Xt8UNGV-lU>c^aly_GHPee#m8MatJK4XBVQ;5yT^c zlN0EtZ%r(FXCMK2`1%LUny15l#a(@hOBmribx*rL#U8{_sV{K9v@%jlc+l%v&ZEo8 z;oyw<)xDiL7AE)?vKX+{kgqsYiujlx7=1|RJ>YfOA<@AYMXz|K-(}k*g~%eKOD*G9 z-|iP?@L83$?yKt%ztF?oFH@wRlveho&~e(`afkLgF~U`Ve~4_XdR5lK+gfZmHv?&8 z2=dUXBjHo2!)o{Qno9CQz@pOO;w9TQVL_cjB`dLchXRnGPca2z*xDrHZhWIr6V`z} zH#RAi9qZelwiyBUVnVM&ZZB)Q|PgB)%-hyZ8;{$@#3!t8F;~Ruz)gGv; z-salerp2eB@1a-u?|hGl(o9OI%P>$r74VdX3w8aguD-ygbm3Ji$aLjWM;;(;6&c_T zeIc?JUUUzGWc>5=@F@~~HWt1k%j>*(-HSbTUBqy``MEoJ2rdEWnsUTZq1+1iEb6YA zP5)QXhd~yhR(|+!Xa^B|HhnIq!gF+}QR#z=;+Yzd*o}pr@To#;@6sKFHDS9|W31aJ z>c(zQgZ5Kk3ST+Q7+x|{Yfdd1)J)shxM#mltxbB@jz;dH_6+q*6H^AI6DqXq;2aSo zVTHUsLtry>3U;7SyB?^(V|J+~XH|(WQo+Fri_JsAU@#(Te!`mzwWN;tg(04`KJOhd zE`@6S(FV*Zk`BIsizl|3BXPr7#x(S*eC39{xd4EqRjI?wcmMXanS|MCSckIE%`nn^ zm^P%KJi`*EN3!R4&-OMBKm2ScHZDwPUPBJFwrMGtzQ7XE75uhjQKp8l&!3;(8qhsK zgh?4v3M^y@no?n{ZNQ46&?^w#b0+aDNP?}`0q<%lf5k~>3Jfo3o8I6xFTf@AxpUZb zt$}jaZDulXhqj9@h~A#YwKPxK}EE}$amk$o%M7a?{YjU$XkXmSx6?G1-g@# z*(TBQAf>_4*&?cs%QaZlbUd&La1x1-Gyv2BBz`jQ!=gDEi zWa$8yFdy&0u*l;SQ5U=L^enZ9-Ut$vLU?wgGJK@@MKWaE&L^V$M(r$twRFYTrS;Y{e zD`YWWtTjJ9p~rn&f$TK48!5lkcumwuAFwWnHPRFpzb))rC^JTWt?k?%U+RN>3VjgR zS9G>(_<~cTH0SMk{Bm$Nj3nOULp5NVy?ryIkL>3XCQvV%0f(JgwKOWQMP?^ISUM3O zJNl_xTW`6E1Hz?iaHE;un{#A73!SQsSif43oJwt4IBO5<-s^%5m1tDLFA}W$JU8a( zy{azux3c=UHal7tO~8zHxK0?<^WDP1{VdqjA^tXFPKD0cX`9h^-{s~XIB&1l)W*r^ zQG3+&D|#O1jyt{Qisk)SpR?EV*txABS;t|dtT8ZHsrAjod)bLZjaEld_6mfC>tPei z8fYf0>co;r+oPr+QwRJ2!{6>d84r;fTEd2m(Wus;AyozrZ#^0nuE zBDCDoHPC6{RG6XSh#H@VzMT8B;JT71gI%^K<+fhPaxU0=Igr_PSZ?x6_w))|UX*wbn*hUwGN+&P(r4h@n~e`K=6oV`o7MpHM%(@Y|65b*s4l)rdo!a;kKe zvH0m@gDwceSRB6rj4?@>NB_-R*Inmeo{08}p&1OLJaH{+`&*LEe5|t>XXXGTTP$B+ zIpFx!?$C*XFB6(XM0EqM9b!0htY$!p)2k0SWMBc#j`=yts5{r$phrfST7RrspGFE- z%%W_ye?6mvlU!5ku}}9jN$Qd2`i?%rK`|Q3LS{z#R;2zuw-2O_X3eeT&157WpATws zmbZemN>2qm2F7>BD?shm7@heDD^))#i;NYV=%;)||0z%%C3rJJaH-wI=~GP~1~ld= zE!7AB=w?#}Gvx{gsOIhN?`1t=?=I89OvGY4pX4Qg5x$l1QkaE~!=Ro}YVpFwV5NL< zmixIZ`2-VfWTBg|T?hl*n1cE+P?X$7WO3AkyTp#h~dP@9{?cZEluZt4Y35o}1J zsPK$}akn=2Cgr)Xz%pzitY6Jcln8fbl1+3l;gpOVVOQYbVs1TLl0hhp=wwvPkpbV| z{2|%ZXblsi#u+cvE!mB0RkZ=ti{{AGjIULpl?S;Df=V)PI}9nB4IjdC>ZHQb!2lTz z?H~=!V|SWyHDF`@Ke~Qlb$CQ_N)%0%{fbF7d<`o%tHrCf^c*@e$_IIK*N&>BN=p@L zPTlG9;7kw&V7fnC^4Ah1PR1$t5u-oGGRJ^fSlGhZhyeMw-nh)oUD*o(eZ)N{f~^55 z9U$0Tl0DJmrM~E?U9SPq87%Zf2{?`0%ii7U8I^rs3d(?h>_3i*o$rBgCiiDwll!f} z#}o#J;T~UQQ7}|NE+kJoBLp8?-q7VfgUj~eOGu%tG+ZCl@#BI)-$q0&L=|ZcO*$r! zvd@154ehn2XnSgx6u30H$|f7M+LWZ>!vJi9>2$OxT{R;k!a`?C zn-Hf#RoRe+)XLUI$mz<$U8?RE+|YRZ0yP)bM_~Cu^YY8Ev}jX7OFwJF{ZV$I*SHT6 zMOn$XtR>9GGX*o3lKo6=aS|yG51yE@ailC6_Cj}*vzK6csH{cbFH1TV{@yj+gjhhu zJmVq@Vn{R8>i#~UUNq_H{Da2OXsUA0+YuMPd{cOhx4*J&=&9Mx19J~HbH*`R<*Sf=Sk%b z5|{4%rf^ucF={TZSsZuy*+{aPuo*}=QGe-1xV60a?7(i;Hy0wro1B%_&FVw!Il3nh ze&qJbYQY1)JrE$BJ*8j`4*=Bt<2UmLmQ-cJ&r3N~j7(32L3E~w0icA#I``@Vf7X@> zKd~qy{Z<>id)uVN{Nw$IwGr2hQuQdaS&>;QJxHjGWx!`OfW9Enypf&!)v{!sL39`5 zp6IKtKQ&Jnv?3^q^vh~!Zd5F?8kHUGC7c>;@LU26F85B)&>GHovBX*e=$mTDdg+q+ ziUK3igyq}v1}yMq%mjJTdm*mm9Q~?O$#*a8`%5wW>7D^r2B;w|hw|D0PWiZn&$dq@ z5~Yf@UCABw223$Efe9*t%Z_E~&oXrLxl;83P}M$B)0Zh?s524X=N+d7lpMK=$7Q>} z8QWh;%hc#`7n;RxW7lD87-px=eb4Ik3yu}!w9RV`iCd1Z&{gHvBct$@nCu`GuA1K8 zCnPr#%<~#jK zjDmXpOq3yvVloW+4Mu6yhn@4N=5rwfTu_cNWd=UaM(YV%x#;1hdBFf>=j#-xzrF^g z0n^IXkahQ^G*|dnI?s4S%FvvfUt+vZ$LC2?AGR{%@UP4Iyb~|3_uKN}SP|&Jd-scy zQOW++dYhKQJfBYIM6L}*Jd#&hgBqD$hQRsec4yhJ9`TiaECKYgl+p^mfeV;6Ma0d_+Nl?35&EHhDP$RD zT-E)&CAd&hz*YLiVr87wr~86R>*4`J^W`k0*Y&Z@la%2-2rK2e^?*K!Vj-J?)36C^ z6ckcrUooH7og8mYeK(TN6+3>zs)7$4^JNJ?F8C$Sw=rpns$tYxabG{plqI_HzVgs zFRgN(-?6i@Mr-1@>xFVA%+H|duV1X{u(`CzFxJPXtqW&Y#tl~HkfDfs&2J=Hr(a0QDeA!?zw8SE^wgjOpTc*O?ihE^&MIk_MJh5SvuvNNUcEDwoAh>MRU!fArk6s*C%pcDyHh`+ZIQ{t& zvIOJq3}n-%&o?TOo&`iQ0@SJoo~rVNh^(pY9>qSh&3d;WYQa2{3eHoF)}=|Zq(7_c z+tB>?WE@sGri^Ws{WawcJTG{0EPC56CHMv*E8|m#7CF{KncrEB>Bw_Q&$b#h(3<5N zlJO;X`))!hf>Gq5ht)bRR^G?+m<{+Kk*{z(pX4{x7L67M!xGdr_T(`vdGx*O0FQ`X zZray8!VelaWIs!wZS`cOuSx}O!`hd}2(pAU1LiY@6>T(EhD*dlHIf(UM{IFX z9UbMqCWGz%xkzMAO5o{P$>eal+6Aan7O^*PrLIPu`p_|n>jrN-DBf>-OPHh-cb1mwazx^9%;$h&Vry%U6LH<`W7iZ z*7c*=epLzyTJ<{;zxpiY=KDW$J9IorDy&|ktKZB$rqVy$lzT(M=Y^qZcRErx*33)> ze160wjGsyGkcCAz*sj)cu54+EqKk9PSgC@dGfc@VtkdK& ze?N9(#IYEnnxGRn*An!zMd)7BJ>E7lR=N4)Qvf!e z?)*7#9VZ`nNa*BVH|?Wm=E_~Yzz=eg-T8hP;CS$ji$&f;R!)}jA;{~4VE5ljpUAUI z|1)y+|JTShHi)YZ*#*$Ak)|MrC3m3(52!NU$#>l=tkv)7fLE$f`K^9P{PB``anf_! z9&>BtPEG{kK6q_q52hGKzB<6{6e%-g#mL^JS93ef3Fmx?%wQY)x@d6Tm4z^m!y)+P zvV4EgG$nbiV=DfuTEzcRi|JCTY{~NkOoUO%lSB~MCtaS=bHDxO??aa7uFHDVg+~;< z;WOi(7_2y3olWQ=K?U~&ZyWWQ_s$t<@=3w^XGx^hZQP*K#Q#eqlfTXimgcOg6^XCw zJ%QC$I+GNO9kxfi^HmSec+yc-%}te?M59g>E(;Y*^opGcKx0rfKH(Y%mXdQDiVB`YW3M& zbwL0-I%a1L9FXm7DoSVNbm^mTQJM8Tsp$rr5AP8aom9LuI-}&L+;=yxJPTf|kXdPS zF>Bp~&sFVCa)hNM9R)VtTwcL2P1hzSUs)h32F^{?A9>^*E}%O&MPNQ)c=xg>;&rWZ zM<4g`2MZp8E`;G{GNEZcIP=Zdu+bOPI*9^^Lvn^mp125SYP;e^y&d2_=C5I*te ziPrEJw5Z>P{J*+ttan)mY{d`{vXO?i_C+_39yGyS7;M2u`!+#SjcFow(b`GFBWbdO zk*PS?9g%KveZN&xyzuQ%D0pt3&XfB;=W-OeNjUOmHb7}5_H!F6lKn3Ju~TPbqUq(w`9{)S_Q}?|U+yIDNxE7d*40gD@udUA;w;QB zuP49GSZZtZ3NT#=5tKQ@CZ3#?{IKX~fT^aLk7U-97|VYAGumJ9 z6M|(WSB8kMlYYZBd-wC_hifhP#xS$EQr=J18YhL839mMn91YQXr-oxPUE1eFE4Zjs zsq2V~4kAgf?g`y|vM8q@T6qFiLi(E@vle(b=07%wKNI}>?RX=~SvM~h^1)Zjs6d|2 zH>;dLc|JRR{9eCTr}HJQvm6F_&SM$1nWZ;L&DYCs2s1-G$J5fs_7y7a;-9!D$V|ByxR>D08=mf-_L&R6Kjaq5M(Rb@sljTW-EHQ=KYTD+-n6E^n97G(YOoz+KC) z_AY>;Haw7FSo^6md17O4jvd4CN30hbTVs3ehgITt$``jyX<-o{O#u;GKahSN&Fyps3oS)*U>TN$uPPafXf7NJ^X2foDY z@R*f*M!@!WigS{DD>+jGpF*Anf9!Yp<`&Oiq{LvG?plQ{zEfzQypP2l)*U``7QNiY zXYt69@?~IoRtVDg{tz}jbNqMYx9*ZCKTO?<+0bxEE(*paIvF7VF{=ps3O#N^U-i@+ zBz+Fl?3L@tVQ8Y$n5iigF$^6~%l~_){`N%sg~*n`mwJ|otw=4OwliPY5>sz*kWoQP z=@Ry7x;;DuU8M2nXHSZ{@b?e@Jo5e#QSW38;m)|nAPJm<#Z9QRkzPjI3xo}CE*cR& z${b(Mb~4FxaZP&QIA`PAsdarUGX#ON3S%)K3Hzoa$%b08%4y@oCwM!rVz`v)p6`nc z7Q^5^>{1!PEh>WlMIXWBfgMnBJ1?iy9)O*}t4FkrGUrgC?!xg9wvIu>^A{T?b)e-$ z!w-arNF7mgB2#1b2?SNMv<+KzTg3m&Uu*ui&=s66$1~4JLdKBsqK~tJKKqgDuTroF zVUw%z^SlagmbLvLI%C~~>Ak62p>oFkQy`B)svM=Y`m`DcsoFo^Z0RjtOAUTp2$>xV zrhKcQY5ZHlzXaXs50hl`7d?}$5mea23b_{sxCFUi$$AnNw{H9rsMKSF5Onoc4bXno zDI1d9DE6|vDaG|NP2!`7{4Wg|kV|zLrpXO2S|RGt;x&Cke!fsy-0(L3WtScj>-EN~ z-z9c8^pR6v#B&o~8?*aa%Z5CAB+v1(i5~U!xv>5ZR`bg~=lcmPvbp!u9aWehe6?^7 zm%Qkrf|l0}IjKS1WAN&0oPH-iI4g%fznV$W264VDY(ALw*tq=4t6A-}2}axt>t)zM zk>a5m~x z_GpshjQQ8v{FZ+IDi(h^uUzkWXt*zK6ekV6SM{7b;(T zMgTR(RtrB4$K?iVHgqk)pkpZ-psAV?savT%BM5{$Hao2ZhGMvrFf?oU8o8o(DmnRM zM}wV?BV7{;aZ$m6hwvVodHGwXoZ{Ikqe|6fIWWR+!z$q6U1LpN+b_Af#9op6XFL(p zWlzXj4q5B*M98oId3ga{2+otuPs5p1khrmVk9?V9xQ8$ONI<(IYgCyFyB#_R^S{oW z>krYGDv)o=vyfBGOgJSe;r_6aS1f+fx7x>^rpmwgBg#)?aH7$#!n?4QN0j$}>En=As85#D&(pKLBPxyps0%M^74$ zI>`l#r+?72Usmmhcb^!jNS=|4sXV3I&-8;PMLMXMkn49lXuVEEQ@;!pW9N^Qm+U%f_R$_m4>j=<*1gBa&%PnNJxQs$cbF1-cP{#-`i>UB4S?Y@Ud> znudcL2Xk;WWAPaiGH+KYol`!X*EADzBju@iNN+yAm1C|c_TNL+8YTd`3Xk}*UqXCk zzb^Do^ieHA-R8KIhNR`zX9+GuWz}ajiL_2pWzn{~qVFa;T2Z&LAa4(u*35#xqT_R4Y}DMWG4(rjbUF5B@xz)i4{d|+exdhE(Iz5%z-oy409!+aHvjCV zD?N2B*E1{5p3hBdnn~`KTleQpz?0v)4(qA&cQTMq$1U`a^PGPF#K|;!rW|rJ@#+7) zA*>)ODw6ZTlN$RA<;$>Z>`Yzu$D2JR8Ta2N-e}B*QQU{_<&e$u`Wb)Fs3%C$_il3T zIgG-tH&1uWweMb#9ol*yreBo%oa)jbzUh7WyRiw2QlrooE`8Mnz_^j@C|tCMM30yB z`z4=%=2TgEyW_!{3+Zh~fA_V{c|Y5OO^xK}8uwX+vi{3p4l1OYAaV{**UI<;oI$)zwu@&BsSt z!9&qu_ymwJla#D>Nut*V{H`>`E6ZKi4WZUvb4H1ux4hB_XY7A zSz8{S?|_J=qcciS9!6M@_!P##u;sJR)1QrS?Aol<3Qx=r8tDs*;{t1c4juRUs`{K& z|GnD&DcifUmk)j^^l#$nRg?bt-LJB}IIsS!RwsjuVw z&_?1rti62+R)$0!spQ4u{H^m7IpjgxI55IQk2-{wSx`ql-dLCRb88W9*@{2`*6j|w zQ4ScB-rl}yw$2>ftjsamAuX6Pi<)p|!X07Dxjue(0IN|$EtA%6>vZW}i4kRdw^j|y z`@WG&xq8o46Iac_x0StCwmN1e^!w(61+1Y*S*yGu;#6YQ32L_OYo!y?n`^>R`NS2p zPJnO%&fn`r<2C2A>Bb4i!d1=;s0^)(g*CM>(P5+5o@?K)S6%5)Ss#sKsIXPY>zvn( zG*nHzv_EbwS zvvUGmniLkb`KDlg?9Rm05CS__nWRHB(N$)FzTJG>cy6`KbZ2;olez@GG9!6G{Xr;p zvq)aVk$aZ4?>+#Q){I>O@kc3h^fO0C#`!koU@(0Cl%@$QeoN#uV8N!gaXe${g38&V zCZxg^${UoO2hjy<47snlCNnb9#QYtH?YFKy4jG`^lxj_|W)tvQi0UkhTJ3->cjWC4 zx&Wg1zTJ>@jno-CNi**jnss5@yfUr(l!GR< z0Oq3$5{_PZLu{0uOK-k!ZTw(`w zgg#TI`bIi8PIlnh+2V-ERNm~crHNPuPYX0Hf7A=@h=7c>0M*+U4r{DmYnbU;==IHD zd2>PrQ<*wiEp!;`wO>CyNZoxCE1sZSFLe2@jCnt7ItZbRegM8hK$e?8?eXJ+u39zyL}VEe*REA zT6xc;f|M<(Nq#D{@^In3&GX%}HpRnNkqH%~vP42h%U8R7NZmD4jp*&i$Sx4F$ymm} zeO=C+8wI^7rvX)`!qs{6au)YFzIGykC74gSoY5w9O&HEm^>%}L!&`U|9)JM^XG^j; z7jdcBWauj-~hSMKwN_Lk&mYQ$= zL7x0W5P%;4J5u*wa1d!~Z6f`o9KViH1)MpXh7`Bi)Ig|qCgJCIKDunUgH0#QyPzF6 z4@$5w)LrbdKOSb0IVYQ(J&wAR;^R{cyN-I4+!o=-#Zh4k3Ie#OU((Rfy_>PID}690 zEiYj-r&D_KO|E_lYo#%+5|74wjWTI2>tJGwYc6VZAsXa*41Jt8jfAjaUFjvWVHUDa z_3qVjN@egyL>0(`G3mU$v^Oc&8Qxo7_OC>3HA28zP78W?jrB4TXb;jtoAqd4sz&Ox z?>tL%0()fRghsUbx_m#@m*F1)9Z&Fbn7rLcjaFZsT@81aGq;b?CTy!N31KngAG6|o z1|!fyqQEH_AYgT(iXeqny-D(ry@eR+h@{-frlu!mwS4NU-bodVHwfS8D(Jvfp4pAa zkM1UUMti4FqurpEA2iv|1L_!)1&gN2;Sg7MZ*kF^6(6#_Tw;AroJS&Zz4l7Ha-|j6 zf?9r!kMK*s1x2yoIB=~xShTGHgr$qfcwD@nk;M;J;!9K{iWC~ z7F}~Sr{pEqQ=Mr!KWMNxOxnhM=vH|dzCt+xdvNJQ8-s^X>uRp0FnQNE?W2dGc^*aB zm~5NPvUjGsu5IZg3 zj0IHvsV9JwBYIcPCc-DGmc=6y^;_e}IrK}i=R{_3&;S_{{SeLxfX1_MnSuK2c1)#A zKrWSpF@WiG*>fVP;T7#;H}jYqM6S>gErd&1tvFbb*S#Rm7X*OMWHT?RNC(DT3UMZz z+KyAbg_LEC=S*uXcRNs^Q^%35(vAUZM4=+9xczhXxUmir1TjtR4>%P{5O^Im&?(>M z?TgR;{Y31JBJIw@TJn>TW&W{Sy%)aHi?0JLCQo?Mg~CgDo;zhvNe9wz*q6KQ2sH&- z34b@3=^d%I{YgpSE;AHyzzUQfz{>v%zcLs2jUYr|MSj^*r(O!dQTkxW5MtoR)^u{j zjAyoVmdhz9Yb5i9MLzPRa@zsEnF-c&bVSa0ZLf1-JkVrobCXtTv*0K(Fsi$L&w1~T zNB_m(osqbJ$^f-Lo^IaFbC(Ar)3e`9bUzl;;goF`B%h6K`rgx2xJjhs5&-$s!B%kN zulAt-VxrTWkn3c#hV--G74~mSD}_dpH1SYJJG&WqKP9xmel3>VY$49l9gWoa6c2bz zQ<3REXg2!;)7je|Js7i47L5b%j^Y5OoRx3+Y#H_&8W?;D zPF1Anxy!xFf-L(Fp%#i)tH5~JmFa8nvePcM`SMr%3p z4{k2I&-wpjdEa;)U#SlFU#%L}stL~L2|Z^v9dWdxst$o8z;!7>fNz+KTL&L)^oIH>}p-l zvOH4(_;x$c_)6Z6&Md>vksQflFlda>{8(&xtGmz3=Lqzm7>j$C)3_v|Ky!0=<+33m z*g~3cAx%4%_LPKN$<~wH=GASYy!Lx}5i>%w^vkWrv z@`+#Z?wf9WyEl-_X-rL;J&-~hD&9MoYIv_8NH&F@Fnlo2ES(q5X|I8ixC4=~MWc^t^$!aQm*eLH3w>Xd z$RgGbUAy(~je&+vf_3^Xuh;QJlV@Zr5Wzyb$MXr5);eT!_R<;X+B?vmv#%RI;RlVr zh9tC^nhd7w>wcKc#5^xdP(W*Eq?~5*iT;hkGKmTbr)k6vlw*X zcVCigs>)mj?a3&5rM=gGHz9qur%XH_R2VHdnh~UJxRKmGqxV%$;X$O%)zMc^#^(U* zb$HkH9o$&bCE?lES7>^P5C1o!{~x|(C_2`*^a+Jjh}K96ncpW0L?Y^a3^x#y`g$L; zYt|G44Q$cljxoN%D}G$n9p%5dkcm9{v`{6TdcMKxeY^XB9n|K5cD>P2wD7E4YG-m* zxpFkR*N*$tUe$oZ?Ct?nO7on@Mtey4$r|Z;C8_4K{X3aRc|5?mpaC(sw=bU!{jETZ zL3hNSbti#Kc6iDSJc6JkkGaI!A2i{HEt7)Bh#PMRC`||LJK_7u;`~p7MA~2$WgHva zBl_7aqIFfTy@R{3tzu-6*A@GGk7>L98>5Dw^*>Iv&$6-UMkm1BwPY3|nuhJs6B?9R zfwm}YjiZ9nhnTJMq8lqSB94m}sZcZX$_F?FVq3&v0?vR55QHttOU%vLJ)v*4>`5UPY$*RpAuSE>$m5imdDm1G`(~plh7d=>d zxmjX{b7V}}b@=X?NDk;iTu5+)G+`^dWV-jtaAx>TrN?ERLP?uQ06CocVlsHca_7mj zr3}De8E2D`UTOq|eI=NjxF^4C8=gMb1u@k4?!3kDen_G>wrOq3s9ITWnb}c&S^*=Y zCx$Ta^JDbkxiSARdl_xvIRnKgJbHb|aEixJLx4#|Z~6to)RA%%cq0D`W@=D>UHsMq zUmde#dble=y;A{;X!RScm#FecaHEDI%OuMGc0wkiU&CA^ zsAJ2lmeVb3Z6@k2DIFwfOX73v&7p!9roI}yTDaAn@KH-8Wh|#BTe!g5ojCj!%6FJ< z6aH?(yy+v)Gs*kCBywr)Hfa?Y5lh#7WGUD}G1ipo$7#cu2x9_%{!?Q)9-TWhg~`GZ zuET>x3{Ni3^03#h^TdsCd&Q{^9vbeXQdDb>R7bB`M8D>!b;F^!P2bgs>P63UooOLT zbYSs-AIlgC>osS;4`~)}Ym~xGiMvIXtm?EjVaA?yWJbCyB@hi`h`tW>FODuI(ur>+#DrY`7=!b-4@(y!Nsxrkw{cC!?hXm;hLBbbBRf8Fkz{Pd=@g@dh=$>? zwjsnK3u+=c1>r1KqI8K^%RWn}CzJhj`YJB1%)Vevm|f3x7|*OO)+*=tD$&P~G9V~E zyw+=Cd}x5sK@YLO^>pj?c#?9mJ{C1oTV=s-TVgjw#93cu`Q9OY>2mbc+!lVA940+A1xt@RUMC5gKQJobtT-S5!CEh4uliIbeOTzTk z4&Vq3ii(OClH^U_Ip|&fnmZ3z*O>pH;j!x$n^b~Xv2vPf#_;rvj0{XKbxz0WgiR!P zleDefD}nP*JPZMY`Lzd=$@X)OSi9%+lv=S!KVIZq6cGkv51*Lu<9ZKlV?a~JWY($IL;!5h5fF_;k0>Sh(`N_A83p^WiT zeN`^MB3A)!FWn_iX=u3?+c{DDr{j=buGE3#Q8D0bvj)PQs>L&x#>WY0fDH^-*q*1q z7l9@ik8=iIn!k_k75aZRPybXGlk;scZZ*y7(pZuDZ>GstFRTu3(R;RW{r5_nQ||4t zUhAZGD9g!j*|F-%#=zqihJPP0NJ=yi^bZLNIQf%z=**0LyR{U!|KrA$e0QI9e!|wX z+~(hZ@_Aw3+8C1!em3zLsf)cua|)Z;?*;1}Has3tFk>0;r1{vR_k_NLJn9L#*!iXK z-=;~5hm=eOfxtpZ@#KlCD@_Z}S^n7_WwNM`V|n`r2A&*{yZL!vU3ab8=AXawz|EIw zd9QE0KPtX4c%#?ZExpt4Y|Knuy&z}mwfx%arlvKTcfR=F@{`MRT@F0?bI~W;$e4RJ zd;Z#e{P6MFr;yKqU#tCr!FMe+TK!o5`f*hn!7P(r?^X3>%EW_ z{(;B0Y>WSB5})C0_pbHh?em+?MA|ARc-mdMS%2hW?Lu~_tkb&}@v^gZj!wR0#xnV> z?({#t;6FxGMR!kV(2&0_u@ zH-!A5Ryo3q&l|OJG+-BwCLcOx6253)(Gc$1)w(YfSTvZI==YtSmUQx^R8z{V#I`HE_Qe(8WLe6OvyE?NetEa!>h>=0@Wm5XKS|&Ft2AD-Y5mH8gL0RF zdxrR3^}Wu7%&)j)KgDo!?esgFW+ts(pEJb_Sm;@r)SSF?B>q;MT%GI8O}AbE4@}ej zc0!s}NB3^B^p92FR()If){k>hk)=uHntL`!{>EMW@WN=OR%OJm+UHLgjY69XQSc_|opK^?rDZEk7HD>BM? z+E{qGdCHhIvjK$^H-$3ZUiNOD|CU1C+?-!C$|%wH`SJqATmeu6FL|5SR-O*-UJh=q zzz5T?Z0zh6xKSP+o>2~El|imweSPdBYQ24TZ@K2&-%Tm!tiN@qKTJhl2I@Dlk!4rS zrBG8#V)bWfo{{Yx>J6JJxk~g0#%J%^pWaW&gnA}1s_NWkW>ZP9{)g<-6DDPHomFu zabM8I0z>GDKeDCUfGGb@OYQtK&{uH;F%Q@L066 zCEkH1LoAIZeouR*wOwwyoh6SD-PE;Uh!k0$ErT@^l*wf>mnL#n_seEcs+=tz3~C*t zf9Tqvo6L)D=?LarZrCXmxz>%_W(aMusE`$?@7=40@-XNE|-zxA%+H$#D3rl6aTmV)GK5z}6c|UW4^6#a^kw-we#Am<6n|b;BE$ z)3IxyU}Tb$lpuW(s`7nl)`4PcsvuhUD-v?QJ^s_ z%B);F?PCn$>VO8$to%V*m*-aw^T2iOhz8k~(=91?dRr@P|J>;rt9>~L0(*%aK zp2^I&J}l+4Gg&ncMlv+7n{MwF6yd;~-lx&=;4%HKC$E1O6_+`-(ZLM^6kB5|euMv# zzRz#ghWKwNt#U)(!Ya)OysBdLsby@|lL9UFdxWol@`UVO?DwT`NEQoto!Dl$=pDPC zW83M45!^InIn7Q? zIGody`jY!C%+v#C@H+_>O){SF{@FzlZ@78INAKrbC^+2 zc6p@MTqZVk&qjg7jU8Ina%7}0RH}Tsq9ko}^!VbR3%{V*%@>_jH8{V6yOM`ij-gMj6~>$WV|h`Q_W>5B2;fk9n;gBvi-d zc#NF+F{n{ZTm`h^9HeLP zy?BhPcI??%^WV(ZnmHT`^$T&>nL;-UTDRsG@$|QDy?U2Ra22M?&WPvx%IK$3(B+?d z(%qW4NTMbp{A%Lkz1$JSFwNrbMBQXI3+2={lcAPB$vvnULPz=Q%R?w--fSJ|Lyj(o zHd`ks?utVK{GO$VVuSm$P$c#z!W=`r*Zn-+8U2eT+zl@FJ4>EJhy3&dQfsOYSUuVD z&u3ifrZGJHzzBW}OcLjSvhRs@haJ9<<#RkV4}N@Q`>i;yEj(KG8e9$=vm zJrvH8p0EGeX$g0&WAH(zCilWrms*`J;OShEY&*4&xLK1D`ulh`G<42*+fKb)dO(bNRz$Wy+n^StZvr86-I6sLZ_mF6Y%U`9 zV1D&)6g3&sQ=vYs7fW>u(z~Goxz&i#V~#(@WJ!?>WRm!;AK zuj2~+T*rUE!OLlm-Zy-o5gsyZDrx?SquKR+?$R@gg}DhD=FAzkpmn{L7&@P^rydEb z1vxY2i5_H@caz>2fV(gMlr?R6o_-)w`>{bz42(-BscPO5f4E*@l93TJ-JPLV`5nI! zS@jE(-U+{C)q13kMrfEiSe?5c{{mbC&*usbfn=yRq3N!t!r}88U9%)yaWJSc1D_qb zgZgNUcxLxOYYjO~+fhzs@ak>5c#i#)(U0w!#rA~9z8}ofkA`%$g3ktXmgfKfqyD5h z^yNquC!w2W_eV2ZFha6aUC+gKuqDbQ#_KDO=`+U|C5KNY#o*aD&1~M@3aEnsBgq^# zos$hx@O`)|@%8+pHk7N3@06J`)b%BPB?lD+$a3qK2D@ zyFT}<@bqs^PQe3UF>UQ`QHJyS^XuQ%l%g-GU`JSYAO#8ZodkAJVR0WeEYDD*KN(Pg z4u!Wf^$I<|_<27XF_4i^=KKY+GEc#c_2dy1qLdhdy-@QeFh(dTSdyN*cEXWk8~@kU zNs_LnhctJIG$fi}vvodY!rXre9`gQCo$nX&HI>l)dSK7)WL9-pdi3du-=DWLEXlFP zWAha~uy#qgwE!qn8sSwUnvx(~bws-iZq5NF$7GX&bYwW@bQs!d&3?t?w{4&QN`4(g zcH=lQwZ|O_vZko70MBh38U(oXCMjiy&mDbm0nrwE&1{NSCh+Ngp4yK*zBo6wG!LYgTT7My#Bq6y?ENcSJ65%b*&#IWD^`OC4ZpO z3A5C;lF5KE|4sms7DY_`7-wMc_(8VSHEJH(a;+`Z1&qZ|9W4Pp*ZYB>Z*>4bcuyjA zvWMjf1zawcAjVLUlmX;-9~6ek4Ub+76Mt`6hnAK)=$E_1a9txj!yD?9 zMZfz@d33FQ%_wtE^7%Rn4)V>FkI_4N4!yB&>X|QkiwO)%f76|~+bZO3eeI_>l2xjZ zXb;~|@hysLeg)yv@(ZJJe6gos=tacn-O0LFNcog2R=s$GFx?aX`dYJLWH^U~dW!YO zyw!_4w3iS~iznG8vunaSQn7Wg!igC?WPJ$J0reym$iN0v;WjBRL%Hxw8klh?7= z{l_$qOuZm5>gviC^B?2^RzRy>1(9wMl9zdaB@d@V}gopwy)z;z^d`c!K&0IS34Z$r_m%Bu#yW%1lZ!I;`=0zO^$yB z?f3jO{>aD38V577b}`xxDFj_e1OTAbLou}s;{<;P<7zsMpmqHa@k$}TqTRY4ahxv` zV7b7ZNM&Su*FK(*QS>R~vo^a0BsK&jNoPI@6K%ceRUAoZU*Jr>IoPIRzNvaH>bzfY z+Rb`7fQg4rbrID1Aee_KaJIlal-{V!nTbV=Kk8KwxZ%H@&%-8X>!aHw@Pe zPN0U&4qY+#bEH~`o4Mj}qgq>pq5rv==aZe5qY12V)A@Fr^o8n{ew*B2 z<9b;}YbM|O2Jqebd9Q8Lpmzsx&=Z z>@TlRGMi;kjL?8b;7B?jgA*T6Yw(}9_ono5^HGWR@&2I+!+OR&!KjaBR|-asg-9IV z<#L9bk6Y6*Di;VKfdyefT>MXhKscTpf*gs|_F(Z2P-zK2`q@1{vQwWIv6@Y!7gEh+ zCKNW{p%_zK;S?Pw`#+z<+`qu7@idq_CMLGmsXA`8CUiabNXYV?8Q->$zC0LgBB?3) zSt>Em^hM(5D(=kT!3YZY-bX7+>!-tHs_%BTvD{6Z>5Y!JWNmcT;ot<)7R{>0L@aEc zpMp|vLBvke;OX6CO(=>fJE=_2f6tl+ivFJ=zj812O6jmpG4&b9J8IWAc5A5#UEBKv zZQ+-4=TJXglJafQvb0F+t>~n2Iw`)vEdOJPJo<}5qCmrVvZ8&!Neaaz#d_7mFHzlJ zNhsn3yKjxyh9*>turqcgZTXJdq;LhDoGiqvEuRuXO7v4F)C$n|L7@tzVIEl1YOpOX zR1#cfL<;6*M9gPc9cZL_j~EWGnC5eSAnhA%JrbPdyMdNF9BmXay%j)MBTu5={vu~y zuAHKe4j2VOw$4=?6IV(FHl1XeJ$9ec7cH%V*DR|odq>|dC+(%@&+C$8rJwrmDBjM| zz)Q4uCtU-s<16%uWNkPJ;CRF$uXe<`s%AsZPw#~Sd)=vgXIafJ>iPWIko}BV$~bsT zr<+81PAOoT;SBck-<47v=B|vKO5xeEz{hW`WcFJJLp%yW;N^ z$=7R)bX$lIIe3mdn(&_m&)HW+Q@&F4KO0vwT^GO22ed89PIy`c2jDbh7er|2=#id$rR+ zl2VS(b3>UtcwGA3otrX^+2VfMyLzFqS9qrk)+ovn#z)RyzjR)8X%^oP|J0yBC-?mv z_C+Th)In=y_Ntz#8+Mi0&U(dG@VWh=BPn!?_PYu46TMoq zTi@yx?jeWL$&O-tf)emp|9b7+#ILaQ*J8h_7LzyDU)yo4rEHPXtsiDw;-a;jN_5Bx zGG9F*uXRAuC!_kEN~Sd_&zRwyb}^UGs^5Guwk%$`dCkO9C$E?V<7~;Dj>Lek->Mz} z2WMKdb*js1jQ4O&V}Y2JJR1`{4~-Vrp2ff^uM|Vqg)JmkC^mTv>jJzJB-iqZ1`=5F z;JzqUE>eL*+iiq1A%>m>s+6r z!02BaMck320&9LsQc#xd`1gUj31VL3uN0@PZ*P0riyjK^e@*<2_UU!d<+^!}0Neo} z5q0jC1B60V;x-z~o#=USb==0#MREdgdPHtQM!(F@a4CKi|5N=Q!y;tBr1cS)fD~*> z0B18pRVe;jBPsa$HiA#=KZ^jdmxCmGPjIQ485BeW2N2IJH`Jiy-$i#He$7ir5BG4+ zO_2oK^h7BA=kfyH3MWeq!zwjn8mTZG*TPKMH<~_GqHH zP@jDhSkUWMu|#m*6x^00Ge%O)e)>55E`3iK3#2Hj3iTLaNe&*zpCiZnII$vv42V@_ zGa{G$>8D#`mfG62+}p=b_G})1YAM!_ym-NH?BzN7T*n)aEdqI$c&wjV z^^v4v({3dnx;pbGUSmKQhA~WaxLEv*P6PY%AX(o&NZp^flF8yO&Q8Pn{TuKd`Vsqy zxJv@od?MFxT#nYkQq7dQSL-Khyy~|Qe9PggbtE<0_j6PZvF-FM-3D=_$jHz{Fl{1Yrm^N#g{M2xjy!7$>WVEvi9{$#v_r-2U>Ar<& zaC2?PhSMb9{aOmH0rBqdEJLsvO=W}5G2UH*PYaM*0X6n!n4g_rHbndfc$Y9`7@O6&10A4Jz@q>0{}7) znWxA}bt-FM8BeuIHk8`?93OPGpQBGeBDRvtQQo`U|RXzc)Zyl7~a>kkIS% z*>b==PXi!l7{KAypA~Gg{g(m4T}k0=q^{K)V3}G^=3}N?*W8+SJApXFV?K)Y29lWo zVgDmz=NGV1fC`sBo_Za+mzV>kJ{q$k0qtc6Zl&pT6>Nv@@8?_x>1fQH&m42t|9N-A z-P1UgARmLrk$%5bSQv+)=9{Z z6WgWL0HQpEXr^eH^aa;V`(#M?@8!jCivb^7SzVzn3@!s0`q|T7-(KnK3jimgCpAyL z-(!HbA@JKqjGR{U8mGJlHH6noszLys^J>FRx@ka>cC+PpCg2``_vXl)BbR&!_dXDz z>LB-4xLfvn#TfwZdoAV)=-NmN&#*zjU=c7rl9Nh>6undi9rAd_8o?Viqh^7NpOsvj zHfz_69vRsqZum%GhKE+ZYPtb@{^)zD?dJWfg92&6Pi*5)`8>9>f3%!qg04>gP9D_N z%xHvOC2C2H`(0^;o(=YtbOLW50o^+LP7+e_izUOJGBxz-boO$~x9zXww&AO{cMoV> zYsYOPfs6q%#caZpO})BBiQZq{s&3+2I$$tBGQs`=HmOYn@okxp=DCn=rxBY6Qh%nYyW#g`%b_)0vL9@I-w(&Em$#K&L4;K2kNGTproHv29nK}T z;-$_|52Oy;#|7|+y$A0-asQ^PB?)u7^W2nRi3rAS@QYQYX7=i+E{elQD&=l7z(A|g zTQkIRM5zl?$MB6d>l7geO`e|~w_OMS1nJvOOCUrSZyfUMFQM||0?t+ooO*~J)YGxhS0d+5 zXMYO}s%N-1t}>^XJA%*0tl}Lg-FW$3 zl<+ULLiQ4k;&YR=2QK;@i%V^{^MS#%C!n=z%xDk2zW6I$+?YQ6K0??(u$_$CGy|GF z>xPP_%ULg6>FrzAeC-Q8Zkip}awP);8*MrRB~I9f=5|`@ggfNYxU7 zrkzDTTW#bDkG*>T!^CsEY3-_*+Fm+-vc8OxA+NBc42pT~FBR*BHcniyoE?*9RVq(u&aRCUa#|f?1m)o>|18#_- zL5jTgndb&!(aVQ59}$(|10s(%fcSk|8`b@~1wd1R{BTP7j1lnD#(t^ujX?_-8&p{Q z>1e68hv1(p$<69PG{U^hFM4mg;(=oau<5IhWaTN`4G@#8`Q`B(3XP0bO35fobTkCA zNT7cEhReY17X5WIBPq)3z@J4R^)YtjM)iBQNJ#>a-8ftNix=jD&Z+LKL%eggE}Zet z#nssy)i^*zTBRQRiRPDY@6oR~e&Cd@-y=<9a|}d0qx|@4WOKkBPKT*uo(4_fwD8ZQdG&>KL-YZS+WYKl>s&GX8FF?#esYFszDm_hf$x8U^n2>ZP}TBahjbRuGJ$HBEigGRA8Zgl%-s1WsS+ zJ^j&1FE3N|@m}er_gjRFj^FWqEJQ?i{26;D!mu?~2JR2M+bD%o5uOuH*|k z`qZvP%Q-{>v_@Wvo)7vav7IlWZjXMsK2Fo-6_sDD$6Co-*wezXnbE?Obsz5aCK(&L z&^XxWrg8Q=owGkvmZ2Om<&g}Hz1ivXIJh*cc($XkxUH>}zG?fNt0G;XJd z1gm8Bn>C~KsryVa0R(R4LIEFePG9v8ZXBVjTM2Zy)>*?vSC=`_5hnZo-q{ZdEhg7y z(!+tLgPP(l!#|3Gf@4SRw4NaH>U*5+aF4@9^h`(5NF9WKf$`UC)!UiRhl$ULEBK12To<^Rigkh$6;exl=5kP z#aOnYJ9DVk>}S2I>Kuj=+3|1fL2opdpF#uZ5`r$?Ar)@;;9WBV#VFf9>9H@DzD2nI zL&F9_*JF* z4|^ITnfu3sdZWBL`eo5Y?dvjjeYw4B6&=KbK7`yAsF0ExzwD*EeNLwQRDHUuKEDkl$v+4) zDXgoAIuKx5WuNt1h^;BQ5AefL$L*cY50c4NcDoqr?m@as2J|A*Uo@^26{DrIeP{eZ z(`EAK_vrmzjj-)1WArsjhe2Sz?rO*{K#frHg9qb?-0GlzoWIlzTV2!=TV=9R;xz8> z>1`C(F=E;rx8!^iJzYYcH)l-9tuLqZ&Eu$1=kDF63SJtdn_-hr{f5e1WmJ&%+<0{* zbSVBy%N;I?x!IFWVIb)zMqujVel#^HI{)Fhq73TG=)?8Ss%>wWXidxqBqVrM&AjpI zY~8fu=&(e`p;I8RWX}>H?$N8l)w3v{5Y7}sN$U~F*O|hgpSPMPP1H1fjCLtLBtz+*EfE1fFwsKAQaclQI5p>!$!&1C`efX3fK%~^i7=4kfeJ8_VllQ_;<7>%U?b`i zK*4a6opG2Xa@yfmcda1<^KElR77L#O*lmM!F5wxPE%K!X!r%_! z#8QA2pI#fTDd0+0(cdx@xC2`%;2Dw|5=2x{kDe7**)KzY9G%GBC3QIlMU!woTgAyJ z&WeqVl95ell=|vD>>FtB;g^Rms+1fY`z1^EzWwQTy_VpOR~^g7=0i>6qorYE&rf{} z>_1IBZT*vb|Ci>jDq*AOw(MyY9}c+{CG^o5*Nm>FRTuxbOj02+U(_N~eZ@3&n?GIi z&+D9XlSo+wlHy_u*)2+JrK%%C>@-Z8b5zdw;)ZHOZ&1ko z%+k1~QLEp>aI_uZ)RuHs%a-t~*^mHY5}ObiRI!nm2;ej7z#v_B&*6zWwgXUyh5D$l zGG0P>5jP5K_xr3~v~rEHjQMrRv2{kNB$j)A4Xc*7sD^;XH6;@x$=wjJD4Y zT^f?-b@EIjf`79$Imva5)>9-4k7s$Tf;UszE|5!O?}^1w{nX(#tb;wCA&~5mRn`${vl{o_-D=wAZfnk(3S1ca-4hl~_^HCHc#Yrm z&pYj1LC&<#T=9F1kyr`LnyE)Z=c~wex0U~eeD6szRI~jTh{FIO zQl$&ELgj}TuG-YEj5c%KY5CP|OHmEqiQqeL9a*q$-@=vdB5+?G`S93P{Yot|I{{P^ zRZrG+{t==AU%l*EdCcFXWd3R)74dKZ7fA2ai1nmDEVY>wo_n(=7bDw?%c!(pMKFh) zMM_^-1R`{J6{~1eWP%6y)@?rTUpA0mbrfChUA&=j`~prw=DOE2cWcYAtkyYug{ht~ zJ(K_^%eOqsx;=t0mB(4E-_Y9&x#4IImFwYl_>U#ZBps=?iMiJ=&Riptl}hMy!Dd4m zu&+Hfl9xgqVdWgtw!mgO|7y^8^I0n{R_3;};kpFP51R(xZL^i256&!5%0UBohsMMD z@y31om6U;Tq>a(Q%JXaAGmV2?{iPHv?)1Z?TbtyxbbXkoLn&F%>8T5383c{f7h~KglIIxh79r1i6Gog?J z@wx5g^{8-;__Oz;!E>#fV9(yyI?ewDA*klG-H(P&eThrOy49z9c3$oo#mEs5!<&il zPM+Qu9V{|4xSlYQ1tl>eklO516K^EJtDoyk>hQ9~6S2jHGs?hnTyklt=!JrJDZ2ou~DNnZEzw3r<2D8@Dt({I1YPm_j>KTuJrtDTx9e*(on8v zW!QsYql2(I5Q_Oa3D75*+zo1wx+aGb zsRcL@U#B^#8!wp9A;DHuaO?#D#I4-yEjF&9wJq zh%gifYNBIF5_u5_wJ<9r)QFXY%2Ll=FhrklwRIl_ZUf8iIdn0XjfV(grHy%8CIRkD zz29;t`H|8f_4v4!zez{pV{wK$E`uYk5%F{F1W%t#2+<`bK~VJIB~e^{=ZjZ>$&g3G z=)8}nZ*t35ZQ2ma??tl)K8Kzy?qe&lX5lL|^R@cNJ@s0aTe*6~TmT2S0t;MxKL)f!PNg0L(^x;+z23 zBuPn2m!{8jQTP(-Va|=KUst6tHPQG|W$d-irE<-_fuXd)!AUCR?zQrmF6TQdC~bC5 za17P+iBxQNR(6ZV>gLY((_U7MfSN3~3wrWhpKxa**mh85kC$&wPZ| z1>NXPLIW%Wr;0>HZ9=N86~V{t=`ww-{1f?-MjMTWZL(eO&NQ$YC8uzFD@A{2RC~g7 zUjJu!aQ;%!ppL24?Ia%b+rj4V_|{QCL9u%QBrZx(grlC|#2*Gu_|8;RvTI2`SPcdIoz+aP&vquR-IUr1xh5 zE|GLn^LsVcma3-5V(MX!T!hqDHmj`H3HO6*UI^q%V`rK&G}o*?R6h_?jmhKvSv0=e zKAsf!>`blD30YOn0cE47yh$CDRl6(c6EdwzsZWGuXY1n$9@a2IeMs^G~7rcSK?s6kb0dmQEl&qCkUh%K5; zT7=Sr6#n^jFq#c*m8|lOCMlZTQED03`T8l$=YW3>=OA251-@M2$}>IJ5fZV|cPPofC;A?K{|r;9mET zit9q9G#|AGDs+YylMJ2!Tgi*VcEZ-ZG)KbCNe%ajz_|(@ZQR@j1idW2iwb+p4=$SJ ze3CltErV(jmimF-G-i3g#6MyX0CE1Hr)S;gGXzmOq&X7=mXOmxLFb8lg+{8Q%6HJ} zgIcb_Xq|o<+J!Bwl>*8O)>{%hSn8x;YCfG_85!aboIfOPX%bSfL`kl8{t{` zcjY&w2+tAR-+>4A!LqRE?d*wQ1VqrN^I`>4c+jSOnLtdf|XMv zGMSwyc~o1H0L8ytSq!M^=ZTaUNPbjf@-`V&0LTJ5PDra7_*OgM%F{YAnAeCf~okjiJ;G({;j^yhK;Ce{&CyZdqLNG&)jU z-=RJPLnYG5v<=i?e3os#dv7KYmt><{?=2HA+SgGEmeedo2}ku7vIT(Y8zjg$e9cdU zh|`|In4!4inL6!(K1wf|OT7#tr>N_sLSr*MjT-v}o|pl1;!B+fHZ>y7a+~;s9)i^& z@9AEp^3b_odJ2?wFjbgsI)nloV3VDO8FW)B8vV=Pq2nX?Zt=sPzQe~*iR;fx|mJ*{y+ zLg=r9yYKknJRSwOM%9MBU}-u>Kv2iTV~4>p)~DQ!1&XUPEQ^Ne86qaj>KJ-Yf_<|MzVm=x+(uMe{<1#^q@xV4fA{GDwt19Xo;=uwZzWVZ& zGeXhggW|hZ4n}3S#*2K^ZVzr3mi*muS1ToK6KJN$nOq?PZE_`YS1daow%V=tpw-;4 zPuVLQ619$_0m^1EaL&*!OYp2=!?~^R18RoJ;~c0Hf-{pwI>wQl{pkTlZh111BD*Cg>y@ZyE{q@cKc%33o_5lr)E{P zO2U`bI4kAPb^v~vj|Lx*(@#=slmS$|5xe~21mFfTvIExG%{|BK$F1_u^}|pwWl10L zU@2Y%?w3(|d-^l1_u)$980bce8EtdD1U-lx;kRF`LV2x5Thw%P$}$IS%PzJOOQHsn zbBPg8j+V(AY$KyQiRFBw0LU9$p_^BGHKB!lijzDxlLQC6#6U#%fAcGaRsb}+W}zmg z{FnD;rS53{y!dUm7}m4oJ!gGN!_9H9PKBk&|2c{?{&f99LPgS~v z)YiBzVX!S-uH3`FH$L~Pz$Bp;Zm4B4_&IT)#d$=0XDW1QaZ?_3f@4Yk7BKbfs(p^i zuKgVmw|!de3;f?e!S>#_}b`d zlT9%BdJ@gwV!G+{!P$S?CtkIEv7clSe`v_~fC}J%_QT64c03(~Gu!q#s8} zM0VqsBU&o(H+Gn66m$ERm486OTfj z@XF#S&bN8H4I~sHLgLl!oPoKv4~YsxhZ8L`R9D{qpO5-+C|ld1{Kv;ZMUs9o4Sq<&L^px6zK$@hWj|TMBCTMN^HKZ z+IIAd8vAy!T{?XjgHHdNp_WnkW<|1gVbLdNgV3S0DbG}UimNaFZ-M2@9hj?AS702| zjCPFluBsG%{A`+e;=$)032@ZX9?X6`)x2%scfRHLg*@9D-{rh&7Mz~Z>qB=~SRnFCW40x*R%VIiOy^@VH_v{3t#58jesLgI$?vL^E7cg#om7`ET( zCn2vs(5S=#XL2U_#u*M%N6Eo38@9;Tl+jOx)1~#U!G4Uvkno4rb{j=|kt5{b=)amD z)ThI7_nu7h%r~v*OTg~dQnWp#eI+jSTMk1whMl5ptGgo;LT*NBrWZ89vQy~ zd?#piin*B7WWGA(KqH#9S*KJl{F%K)>Z}`ZWFv}N{8Ha3o$LFO10t_ijJB!{81H0~C$seMo8;*x zmuX8nrM|nu*&13D&GC}(#al<#P759kkcfwuR=YQe)pQ)h2f9RNg@xE;B_>+NOrdF| zj9d}BycR0Y_#7PRjq2hKK*$B<|G~4&JKY%`iq!p-<65P7J|_c~_Yz3iSJMl0xM_N!N_w z(e2lSy14uBIU=|kcf(>zEbkV`KuY`TORQNvulWr65%=${f_u`3N}f^mpADQ!H)Iv6 z{-_368QxttgTH%gCVYT>0SKxwY`fHMnVs5%uNUT2J9Wxl=qCUv`gTuz1$++v;JF7h zGVr;9ya)Mn6N>SwdAhpORA_nywLF4acZtd1N?7ug#AW=vWB7QJT3Dh)KWH{*O&BP%2p& zP>ocQm)jeF#>fex7jmztaW#rUZjn*0?Mbqi4`jcrQrjajg6gz1yX{ZOAlCBN$R@tL zpxU*)ta%IHG_{){V%hbo_f&al@w4#Q{BJT6iJg~o$nJ@)jy;sU#-G6#^nn{Myg3P1 zcJdA83)wjh`fJTZ8Gj5`2Im85HFW0%s+scyHpP-a)n1PJBw|E4-+2p8Pp!^ zaaMf0V`P_SO3dk7>M{X(VL3k2ts94}wp#|E(@jox0})PT)V()#Sr?^3X4TFeIYI;e zn;=`tG`tb`ruz^I7vnT0NMD@ckHBXNckfQ9qI@;&J^GL*-SqYbOO;ImRVT@~3%G*Y zaf+Xb3yS_M4I?V-iq-zC9H$xMNcmgwO+7~6ktdGiqwuXJ0ypJO*cTOXW zbM+)mM6HlxL_SrPokN|RWdl3k=4$1z&y$mM3iSOB527q_L$8fGDcPypToD}>vNZ#? zQrN8zJ~OXMmj?GsCx)eru8lqdcI|O&X77ea3NHKg>qs!2YAs2O3~ULgGJ0w`^uaxN z7%u-sC}o_D;md7zmE?STAzlsw!hJeNT8!6|-j7gN`nd^r3V!h3%CQzVR3`#5Lb zQSxHcN&JaAzN2U%h9{)dLY6`W67PjQE!miuP4+b%9v^A+&3WZ$Ko4r5<6MI3FOQ}) z-5XLw+2fuzb>{cnM@|#udvu40F=oV-8~QGm600S^E7_pr6eXL&IfawvS%AD}fWR^g z^GU7^KO$6(NKU!b%z)p;ldg6nyci$PiqaF-Hux%j6IHI5W|23bN8F-3p7>Z!oO2oS zObANsb=Ufs3A*?PtW^kX!?8Ka7H?zszEDqFE@h31lYwP9C;NBT_V9 zPrd25}&s z+i{u~PvQ`RO_)8|Y8gF5ec11lmJTd^w>pcBH4I)jol+eiyZ*;AkyX|5R42o8=$@7CsEWPaW;@Oz2dq-U~;zJuaSAEsf^q z_TQ$uTWSl79#BjvEHE59Fl7pO*fXlAYppmCUOitb7hVCJK2FhHCl)zS5eAJz!x2Q9Um8a~&w zNcW1z_;^&{@u~MNp9ai*&9Oyu-)79&sf2fv)(PSGy*W{AP!mCa1m8(G8$o;XYE6KJ zbTOHWR(SAWUKP!1Az!@Er=l8OZljJGvFGEt;RBUO?X(1wvot;SrKmCizJQM^YUrQZ zkIK28qxC#}?9PN#RV-GFUQI_T;@;g}ig6N3%+eSx-z}*itk4dmczVF`F=6!Vux$CX z`pXy6508UkHf>bz(0Kz0#YIXD^?c%Dx>`!G!&6}ZEHi&qu2#;yT=WeD;N~g^l2DA= z08U#~l)v9ovPGKKa@;6BSg>8a0Us!d@9bfS6wrY)=HI}J8DHRs<83$n#*96N)X-aa-9h!q@B zW`IwBuLGcOlX+8>XgUGB-;7L^bW}xD(7gC4rzb4KtQ#?^+o{_q)Xs@CGH^4mhlMvY z^O+G1-~fe5dikpO0S9LlTF60oMIJUv$I>osgs4xKn0F)oiK*Hezyp);7_Id9S5 zbSsXo*-#sUt&Ap3MeK%1&sT&Q#z&iRV<}4m`?(7GlFdW>+pKgl1$Tc6@UnzGn9c5{{;T5r;W7$7d=;Q_cI(x6l0jGn3bdx+l- zj-C+j6qMvIye0mJ`+?SfABjH#8-bn=@=k>?(VZin&bI2mIhwhV)=jCb^;$O-Y2$DQ zPbWkXr7` zzG~c>DK=7jKlHovsifkqfcrHf3uos_8ftX*fZE>L%A^_;!vez7g6I0#|5bAS| zG;RksQ70(=9E7^CpU1o3TZvI4w)tqnDLiddD(GxrAKT>l{q7@s4sNRr85#vD_GK7V z#B5tb@;?A|o5^Zao|YGwFRiEab&v9Q*Pe5wc}*r4V*aZ@Q#oB1ZUk|MmGU@r#L&)= ziWo$%ZH0J7#%!#O(U^|L$FngQDHQ0MeMKc7uQkU2WGek(ae}65r;5y!HVtR{Ktv<; zN4)9uBN}2ulnDaQT`~1UM1J4S*Rd(5#HVdJDSEcF_-iXIcT|4e<3{l68NE}~ux7uF z&`=wC{wuJ=f4sy>?>x_O-~v!HYzR3%sqq&>sU%wHIXhOdMoZLeql0Afq0JdgGDZuG=lHp|l_{ z!S)IPYbI$+&r!@OR@rUETklJn-do#gL2!j*VMV@^n!wfCupU}>vn`W8;~*Q%-X>(T zwX>GL3>kPP5Zkt93)(7k(Sh>ssedi7Is?*Zf5B%n;mk6qgC)r7LZMxt%jxT?o_22? zs&H*J&Bs=a8?W&pl;>q}>iMlKx5yiOWo_8%qQnAR0!m#}2m|MZa^Gt2)|J!mOgNRp zf3i@>5Xva%JX7;+y1_MPlLqboIXM@`ji4b}=$qoQMpcUNC3W9Tad%;~?@4a4?K{Oa4-(`9t92I| zuK$CxvyO^#efzzNpn`yuq;w9Y(yb#60s}*rBHbdLB3+_Lr!zwg-CYU-0@B?fC|%Nd z?s4zmS?9cKy=R?s*4qEjq>KqFmX>+ z8={?s-;91isgKA>7we?|Dbn{xiPvNYq>#*L@#EC3&7KwcUyodGuSsYhTgyY15!!sS znHme&ML(@-_1E2;(Q^v3?{UwA(j`h%5xO}Wmd+giaAmWYfsW?=8K8OU3DKW8&;yy0 zrc7M9oA>Q8$=?1S&NLCpw-Cf~lDoSMFKs;Fb;ziHfTSmAxKk%fLS%G`%s=cA)a?9r z9FUq5gD%2fWGS%A@(x*+KSnC{518>-54sFw1^?TBkjcFOg`U3<(-U+gkg@(!1QqDN ztP?$4RtatS(+D(z|K%?NegdseSMmU%)cHebmNudc$W;HF{{1qj*}FYJt35DY3?xyx zeT6X?arGMD>k}VZgi+GzTmR)cpjlqCfbGpIeu!FMv{9DK4|rn$dQ}thpxV~fByKt`RxBQH1*%WUjOy8p=KMqqxudq zS6cx>bKY;RY3J5~t>c!#-Jgy18M>1Evoa%uI49HjMKLwSrN>*&I#B}}Cu?QH`o}2? zqi=rZ*JE{fGEv^Lt2a}%v7+9bOGDh{WID!)Qjyr-|K;iwAljLtvAQ!_#^%gYJQ77% zW&+3@`}(%)4bYbzrXqf?NE&v2;vA_f3Gx@^w@Vy2iq*6yTYq74cBQv+L@7Q^YeSrlnC#$2OCQ%W73AEM*t`laUZ(0N#gX{)m$Y)*z;1fvf_hS_9 zE&=M^`0!HD!Z0`903vt+ed^}(WnNT>1t;PQ21mfq76A1)ZXs_f0W37|!FUOres(i} z)SDpW1BQeE4js+ulR)s(1V~*BFI=~X(W3)#{KcskkjG>6NT(>eixq&ddIPvK@;3%# zWyKHV1vx#frgkdcs9t-^X{%ET6paSH}Qs*blw<5n1pY`co@sxdM z1lp|A>ZXH24Hc1PP+rlboEMcy>hDN0c2q^dAodB?K zID5YgoGzMIkP*H+0dyE|6bx4aY<#W&@SrboegltdXQI#7wY*1AZ0W-@p2qz_V5F4$ z=FeE#8Yqq7;XMR$g+?z(T|J;=%mJP@hB19e^y2NdeVewZTH+1S;}#_s_LhXH3dY}m_^Ep z*IQ$vI|;RUfyZ6Z4+w;644wmS%ori9_s&;o3;T`VbLYHk@gs=Uunllgb%A%WbV>Po z_%#A>$GBg`wt{U_;B}*~E?*-_gIcZ)iaao!S<&^Okx3W}hE>5hQxw5%rq>(K_qy+w zA{a_Y5HS==;1*<@Z#wwd@O?b|!Ph5$B7Z!wybcPnE&z5w1K*t@5{S>r0tB(hJ5#8Z zi!4%uy}ImmYeIef;ciTKHr_!XJpUFVGx-)HV5ZX8#QyLyr)qcUlWMm9z}uj3DkI?v z{lG5J#tYfyXWGg)6fMtD97Q>B5ds29z~#H~Azk*g)8Q=ReJb@FaKjqDie@e9P_X+Jn$ zfC1K!bn$NZp5S*=D_yLuG}R-|I-=({^*qB>#32**@)koIHSNFZxSuv|68arKdw#|5 z^=>NorrR7>T-`)O9FyJowGm_6$e_<>B&c!<-Vo1@z5!LAkLM^OaE32C;MZeL} zHJEgFvfkiX=+m0C?`V*eTvvq;`c4M+_tNlXm2y2Bqu*1SF5UIgzH!FT;zvR zJztEBRTBZwSb1~8tFqWcB-5)og*!LhrT%eS$>wEe6X&+j*-TAIgijk^C6({--VSG5{);tY16cWDo z?}5tt^Ru|cO2yhHf)XqJ}pA^RJ_AqCvE_2W(XFwvj#TOT5;WGu-uV|=RYNx1J|#(u zzTtE&AGE4^8irtqMb3#bM&!L}E&H|brpMPEY5KU`S-!D zD%V16OUKqTNHiJQg z%&XtzV;zEDAQJ66UG?~;^GMwWG|PY+H^`+awpXBb&zUUUg*2!EqHmbus(Da5xESY9 zn{&yx2}@EZ!ZUngLonpR(k<*o9QE3)l8+VIe0mSkD^w&M>N8!OZAQ|)u4!%oIX;{? z;S5&04orIN_CH_%lr%4RtY(###Y-Yc&#zhH=7E;=KmphN0lA~$ zVbr(dFpECLV@z|_*;anq<>d-c!J)lKxY=$Y{>B;kMh)-&QD%`%f5!e-q8@JM(EB_W$ z!CBUHEURQ?(kit$MXtSN!|=_fZYHnYJs=8T4`;Ih>(IY^8W)(Zn+2^=56PRyp*IRFyI0h9D>pyJ0y1 zFIEw6l*5+W{Sw#(C?g$;1`O`H0UOwV+eEoun!T+}gg+3(ljaUDGvBui_T{L2yS!B~ zHSX4YHuLknn2Lfh*`cknM9vGn6XtGmP2$7$J0e7ohnGR&v!yw2Ou5I30>cGiA04Fq zS(1Oja9$^E9KNb#d^l-5wsvr?_Of#gB>1}v`n1nqB?(zD zbY?;}>6)Xmu+eaH)fi?MW^jJG`HDlk84*#)VvNMp<^~;p{0bVq7)=&-4}UPm8Ba5;FH)k6+ zWPMqCk?AL8Y%PoE!p!HfAs6aE)@mY1P+Ic^-9%w=peK68rCe0*DXHd}xpmAek{FL< zQ+KIIA-Z1i*2EV_8l;>|1PXa3A3KokKCrHsME$cEIf7(MO#ktxvT%kCRj+G94(kp? zYdstjC@41{jTMcUnc)Q)Y|AG(*3S%P7e zKfL7l4YXoCC^*LHy6Kb;AgTLki#lT~3b=nhAlF=(5O7~i{@BS=I4b^XggR4#hFaU@ zP`A57zsA0nf=Z_&Rc;wrr6rM?T5|mvi1w|p{zA4I#?}FzT}{cIskuwn!zXv7ggA6Z z=)u!y9~AD&$B!HLjbcb0Z7G{qLgzYu(OGE{i#tG@X`w z*YKGDE%JqZ|71&L@%gsw?C7;lWca%(&i&vFaf)p2<)(*`@3uVyKyg4ufoGey)En^>- z2@*3^MYO zN|9&|Z;3mVm<|PTM47%kWgWn)Ek;6F3MdagRZ`Vn=`-M1)$07Q}ch#4xf&QJwUGrtW2St zfUQxMn7vYwEYS>8u~u=84Tl<{gv1Uwk52os1S$$PXZ~2vQydDMenYmxiG9V)HO5gI zn(fp{rUtXwFXV*Fj-u3UaGyWrwT9o3$$2?a#v`9?eZs7$87rao>lJ@Sj8w{~n;FXU z9BT1X5Ct04+-~82(9Shgn&$ah7D2o2m4lRoF%L;#6*$E8))nNCL5lcOW#&}JsLlRo zggxof_BY22T1Sg+YbsrTFS>?~f1j&xFeVH@Qa~e*tnt*XAZ@KsH#GG)r?M;~O4=o? zwa_c?+mUJmqozUr>Q~kU`x)$`pVuZ3Z*R0uT=c+g_+PqH|^6Rb`QkiJYJ!WutStSK4YPk&Ta+H7!QiFz*Pdl?{ zgPzkjxqOw6jjQ%+p-k_;6Rh7RdxG8v@<;Pjq3>KdgL#qhy3PC~pBg8co;9CyTWS@2 zh$t`EoxZ8+p;0ABEHg6!>WvPy%cYEP1DzK)dQ#2CChrG^L-MS#R$Dww`Ar?ZvbV*R z(u;SM1ysHc)-meYR~U}WU)qwLBIRUt&x6Kr2nkVCdZ{3RfuyiG2vo$|ICUUxQ{L)CTU~i=T+V zxZEwKJ{o;;%o;M2wfH12d+Ce>GFg)4*d4xD19}LfhHf*`&E@@2yxJg%qPYS^)7TOY zvd;y~Y8_}qvQntfA$bng@IeDS$3rvwOjtnv|Q_+HLNZ^vP>bm7>T`jd`6cnDSwE8LkW$fHKun}W3Gz9fghZI_u z8fX>?G+*MZz~}5ExuMb1<(a|Zm{Z)KpNdd+{OXIkfxIF4WJyJ0bgJEL;GhS`VTKbU zMi}DLJk34{ps&vvT91wA;VZwFI2;EVR)E!uWl!yS+ZKBE{RThf zOyA*=DVdmbhi>(|icXZ*^4nZ4So?b&-m|*}w`2==R3m4AX1>qv)vP$t{@^}DVHkI} zdVX+O)8#f0Srpk%;K{7M<<3qwn+f7-ekV7h{ua>x9It0CS2WS+Axzpd^y(X+?5Svx z(`we-tB=|&eo>G>S$3-?JB9+~SRQ%iZRQrmQo*uDvY+Ta>bI4y0k@NN`S49F1H%Z_ z^I1?p4OEPV%3QB$r~VjbzKY!?n`nsM0GaCm9s5PTNl-l*Bnk^2TP{y>;$ruldvA#L zusw~22m&aO?{efm*u(ZWk21J^8mNCMXA_-&}AR^wwxv>^8f z{k?hO%2ava7nQFz(9pCi%hj6V$%t`&e7dy>07rI`>FJrrpVU1y3*j?Cuoc<;p!h7g_p-(8J%^n znapAxkY*? z5gO0-qbZNA=!R)T;f;#d>HO*Dv_nvtr=*YH%gNaI)DFt2K z601fwsaR=$owRPnSoFyq&e(*mXc87<8_iZn-l zS4VyqQLJHkZ@Q~DjuL6Rx-)Y-GmPh52>f{tc3&hA4r-buX7hxBMSmD%RtG5z*FqjgE;&OTyi zZ2cW&$q}AMpbAf$HSTWGaR)!?u^NJrSb@0pQG1!b_e*!a(YIx;aI+u7zNNk@HeV3N z@lC2?D3R{UNNbn9rA~Ym9>410cU8|zOnDRMp)!|I8!(&V#;O4^gq?cXvl)=XkkHz2 zU`$U$V$&@`p)6_dbvw5jCKA4sRasoCAyD)BlckiRQaGT4Ua_^db+Xrqe8_#f>UNwh z73U++E)Ncg#k(g{ufkF^n_;l3k6s~y+o3hL(#l3sW=P%?0IO~&qjjae`d}PPkteFE zOC3eoXPeulN&X)DRKGCmBE|=k4vPX2em?id9#60GPaUFhuCct)5ffDI+g@|}BX=PD z-q)ZqNsOxaYBA4`LJn7lyc<+|jFx{#ZQ-Pi(-k`6Ox!H%jFnQf*^iTGUvCG+Pf|n- z+M=@a9XIU*r0F+lz7n#_d>lbwl5g5ge`VxeBXf5pYsFN?-F=40SecFt5~M7tdlg5+ z`)s?>Cuztl+EO)ge?b?4RQa66y`|}Vf8ynYhSgA_9=2m~I&1mH=FniTTn7p%@4v4p zIOD8w)NLtl9V)&_@)Z?BcGoOlqV7zQ^!abAG5W$+1MD19g=Noc_@qA>kt&+xiXJ#k zq-oxRyajH)axyqkX0|1^B)=ZeFSu*ukKvugI!0uH(_(6;)Y{PpSgj9PO$wat0ULSJ z_qZCNaF>bX7VQtpadO>wanjLJWzeioYQI_(y52o(BT`Vt#+OgGMYhrszAdr#}F(wBzLQ=APwO%zOYU9K4Y^0K?{qPO>C>_yLAR*VJ3QdTY@PPUz@hn;c|9qMii=X+ zP|6Rzm`s}xWQ#v=-+9aOqG0Xg31jZH32t3}IX-LJIF8^S4#`3TI882`u3Wfr1wAiT zle)!*cuT(#gKljs=QU=A)pDlo%xNmJtLkZiw6l+x+it}O-#1e5j}>a<*~gBTX+>~Q zHtm2@p<7i{9_T%B$`~f|o+V_mS11iyTEtZZnTGXKBtY9JPM6gzokqQwRAod@mHM>B zs7-CAcq0h=%qM(gs^iD&`!{QwKp`wkb+L++MhLnLTrA~}lt9ft*z{*@ zS3^7x3$IL!L5Dxk3ZV^(z`S3ZHD_su{> zV_GtDtLV@x7W!;>|X z;Wela+-}o&WI^8i_aGR@Z7i&!Cn7Lu=ZHl|mPQBa_=EjV@3xjr2 zHCU*}DRZ*XgCGWJBKp-WA)BM|pzs9KfM4#RTouo8lr-yA-re-go%O9Q*C>Y3iUC$BQ6d2V(<WvmPAgetvNoIY7P%(%IZ~SXg?=h7?!i?L4H@^#-+4x%%t$aBJpYw>0+1#X*z`TpT>wtJ>&Lj1$@8$@5YwTOg4pwlj1PH?YB{9)42$ z?BhIa^71Ke96-e7Czd1(c$>o(UWPniUEZDwGheSP4mC+F)6M|%JzDUOWew0Pad+I* zcF)k3squx`^5RNC`Q}`#hIH&^E!#OUQ5xEFNJ)($T*aK-GizdzYZo5-d}$qYdmygz zxJt>t{!=yz>iM}p$eKjrCl1l$mb{iv&N=NjzL%nA^jdD^$ZK~yo$u66r9l9lrD3A* zBkDWb_Uy$@(jsivvbw(@$Pv_JwGCkhxw-h=0ijxk!b&O6(~C@(2OfjAG-9w`?KS&I z_TnX(rWBxw#zaf3sVa-)+BTKdS311NAi%tBBK`VW&NQ8(*nI+}ReZ~t_YU?nT|7-= z42lFKy}M^;ac>paD4(H8AF^&ZjTC(+$y$^8mH3KZBVFE9+kMoN+@Pr#?eWpjOut0i zG`AQORZpwW!!;`()zYs?#y+a!YZOYj=w-GluOy^=HE~+*Ff80Hluu#!a{(N+XT!U+ zB5{B}cx3P-!)Qp;EKtO|{h<E~WH!(a$YG^R-WHc*kgY*LEplu&#>c7m!pfZ>bEtQm8 zSk&K)LbbB1>WgfWqX9+0j&gpqw0&^~C*9sXs@51g;O~NF*?!Or;+WLeL*{nBCaq1Z!* zVrJ!}xf#>y`c8DgY&LengLEO-&jc;k0!SfHe>)c&qX~^+nR!@l+2l;XtV8pKr}cPy zUjF3k!3^UR4K{J#zm))Rs^D0YxhvIo^$6t`=Yn>YvHaJEnjAK=6R5|)5}UK8AxYvR z+Kx=D-unh(aiB;{@6p^ztLAX{`KyVy_bk5ZkA54InJ7(`5sETIAno{xPl{>6cI2z` zJ8Os_%W6X1G5a_)U6*X{z+RkhKk^@H{x>fYZ~3rL$1Kdf`{w?KnujAztI; z`Ue2(iTb=?+*k5vXm?|tWcwSAC&gBw2a>FqCF)Ja93Jw?yMI(y5z zicKasz<{Eo$oldiP)n5{hmIzwhXzv4L1osP4cEl+Cf^sXiLcuJe5HhjBWPJ0b3z`} zK8{>0QM+dLXv=g~F1jploi<1Sf}In;J_Q7{Ju8vV_EpHbD7Nkfs43`duI)k-0PG$7ip(A&5QJFS;RGY2XP|40b&w(DVSWJ}OKxx?BoUk>UpazcL;4f1!w z&Wd@TaI+LlCqSNHsrFI;TGlcfn-FxGqh;Qnc#hUBNn>l-Wr=17DaPr$Jpw}Y(J(@RFP63xTDJR1(HIM#vINX9(;2(?^+9~MDfveb6N^IJd`Q&1o=8K}5}rZiyE$TYW&L;SuC zPc?#NwU!*BX(;X9eT``emMb7IB?4lxXE=c^@v**gLoQ_%u>v}HLEW64b)M^MS~7kP zUd0!o_8R2}$uEY4Gkv%omet?iCBtQ=pfwB9+bw{ULQ_atT#_4 zjs#Ig8@A{S6?6=8>%zjTn&i+QAJ9i)ce`E!W5dY9r&@ekue9E%_t^Y?JJCvp_4*y%Tucy-LULGw4ZHO zWknr-w&680;Uv_3xTzF_e~}D)ROJ-W%8qaHwx@Xa2ML}Z>U-OVt5NLw59)Z~E5?WQnbzy-!7s7VX6imzQo*C1hFQi>$ zsq(|*Y1%cXM1`Rp>mYn;GvvvO3MqOV`a2Y=%!KW#;x!A?}pH90%AFVsAYp}REPSIKV zcHiW*ol2sk#gS>rq3DjA<%Q5EYgD!ib*$?{)hafBiTPx0*66$qQ4p#?=b&c%V`zSN z@0qF-y^HZ{JTnV5C{0uQnX1EyenzLwBZ1btd=%xWQU}8t+}ru_ZjI5}Wt?NrM?CUw z)LBc+A^M&yaQl}+;IfEK^Puo{LpFF>TCa{DnVg;Hs{-Zf$Z{)$bd8}O-f75)f8>0* zjh_ba>~zy~$nN=kdLQy0h$*VUJdqwEQ&NcCd83b*am@L+NnIr<;7i*n$s# zBKrNK_q^}t;mFFPou_zGuyw1B(06TOrkvXL;EL6pnR261Q`s{3v)p_Izdl}?lFCNp zh4vh(b|LFUlkVB=sOi-`i#ueSjOL)q$^T7E20=WU97iqYzYe918>sjsTLo7{r}Zcs zUK-r#*rZOJ%y<>jEl(W%!ukI9kx3&fQkX-&Px$B2&y-HUx>#_$;LWG!f(#9}fHysBBs9o~?3!!_P~5WR z(N!e4hG^fBfY?tp*-Jn)aHvnQU4p{t?ltcO@eXTpSc?NIYTRcK@U5T@Hp!j%S>YOm zPwW`yWLV|27B2%_l<>kS%?^qdHVjmw6vd1LG>ij=@AVi3j=U|Hc>rADRj*d+hmD2Mnvq@ZReL znc6RbF@rzz0fbGv?bPBN*Pfsswp_mkKXmfhK9IeIr#m>scpa<-MsWQ9IPl^BR>u2R zIsHFB`@al)_`e(`@!x5eS$Y9wL3yzQMuV*F1nQnIM~>MUG}J9Wn$@0LE;V*$XR#W^ zNfw9`Jj$n=;$7=aJ-(o)UP#eE4BZ4bau=vnWc%0?8(t;EEArZ~L_=X6uN6)?l~wy< zt1*J`c7Qc^6;L*K@Z;JC?l@6|&Qy`Ck@#Sg+$4JFeN*O%9#)}Z+YH0^mt{pS+-)Ei znxS|}c!rEv<&$DbIosZ4bF>2%4bFyg8Z$b@u!s`Ku)h15^gAHk{vr(O{H(q61SI&aJgzVR&}q5W6=?RiDZaCE!R zumAk9s5~d5qYe5{SoZA?fENzdy}yH5=;6WUl8+_CS@)N2D}MeO8X~!Li;x_A!aaxs z-&<2|AE1ZKi2VtsRVrqz0YM|iuAP-q$0E%oGI$K=P~8tkwCSAtpX%G!F8>As)Dtk^ z2pokO;FUb+zzjbHf-KDVP0WZ+VC{1Pj$zf(Wr9&&Z9^ALB@vg)Z+b!CX_WK|M!gq> zz|z43IHVp4rZ%lc3C+r<{s!8Zw6kU4Y0Z~w(+kuff`#1zB_VXaI?-0!jYmcpkj22M z6X*YW(+)o5xX>sa%Ujwk+q^|4&MwsHjTEkq`9SpMS&j7dZi!c%7JuplQo&iLF^`lte zSbo(pn}TDbueo}0MOgY6UKcsCdUwXIjx$$9*=2AhF4IK2I#(q}R6|o+8FE)em=8iP z%)m|`pnT)TJNob9xuK6kZ$IXIocrUQGVX0$#?U{z-B$^2ZExy@3QbRlImUsRIb1 zGcH#%4jV#oTcMyIH3rfRyJ3BtFvX3(BX?{$4}tG~de>C5Ec{E7Mf!w!w0G}3W~SXo znPSuG>~S`D{U|aNSb{6k(6S)epo?OFDB>ofK95{dR8oy?x~6irfQw(s|Og+%F1jY5e2yeZGg~X=!{2 zZEESP1^iyZw>+G$OujhooqzJ%Q*HNze*xdS zizIKf+&KY8?vks6$XnY1Aai^HJEmY!#(oN}bo{e)*5MzZD37N|?`3C=7=RGd-blqv zGT`32c`WAldmFQjHUslgs^`D-TRwOyPJxy(IU6L*+#6J+UE9+u1DfvU0Mnasp0%TD zWqOG8Vk3B#GR}>>f}e_?uAn|ml+nW;vB^ex+$W+uW=GyII>^(;m+X_?%*A0gEy22z z@#!HMShAZ>rI)dVf$-3iAGTPt1zaeXs#V~=U_>LvdxXz=Tx_d`$?O-T^6CN%Rys;N z&vmWXI_oYLuAG>!!=#t&RM_fVmVG2MuA8I0rq)vvkp6jb&!Xil$5`GL1Es$Ad; z(Frny?bprqJDaWAwaBFx|6GhCGA7kWE_!3YEew0_O{<7-dDG{s%wQk|&Kz)Dnl?Fj{ah|F@1|osPur_=qED|8kodQcCbqg?}eZ)!W2Y z6D5b;u*~g1dEObPZ8XsY*`EPb=@$&}O4+5qcjTTRE-XwKG}A=MYJeoIG~449%ok(N z8#!F}ww3SxH7&0*%#g*o%gs63;nyN(y?2hSq}%@7Jl_GDr-Z9LV}i@@CGEg0mIGV% z&tNE7PmE`A2HMf}{K$9vD83`1SNLZnc39UqDfZjQaXjL<2%b929X4qd!S)2!s|C8t zg%=E%nGehg|2(Yy(kyuzm@p&SKAZqVMPB41_CsPKH>XiIkc)3@aPy=rItWFU?wJ^} zifI!2kQ+AS(5QS-WrDo#gpWfy2XBDs^17xxXT=+zfKYoIQlcWk*U)>ZP zM&od7;54dd27RFT`xL)Kk;H|92d!MrrIj}In31@5hs|$~%}ydR<+{|Lq`t&g%#aSu z{2MioTbw0epQ%hyeP9Gsr$@|KHNe9O>^WQv#AN%c=dm7_`C|>g1YU&{r>!QY+@fxN z&~R4Pfg1Ed5L^Z)OSho8OU}Tv{tF*{RHOZLqi61j}tZ+q;A;)o(&SD%nj)XN9cXpfPDn z9RQAZN`wo6eWYZkR?^tpqlz5!U6Knoyo&A_enTz3xQuo>Wzn1jMRJ_-!G)>Ve^=}k zQCzZIkp7O*Zn*aTcp`A7{(YE#3%|YCXVZpSimf7B&Syg;akd5`S+x#4@wONsha_Nv zVA$dCAB7t=E5?Z#NCKATV_=k+H86+f zb*|HhiI+v1FaeRf_{~b-&qNxAD{&~+{6}na&Z#xm6peZRZ!w_H=k->S?|0o+DQCh zbchI{wI=rh<4s28dl#j#3+sGZIu4G{annp`?!c&j5FXB5PWh!oplfo9Len{YL?s|^ z`6oOwzWLdxG-G}|$=<-7e>MOvB=ly4#5qu|HY~C5uEHkeMGc1R$qPyn96@jBATWcn zlHw)<|J6puMR_lpq2Me)*6k14(O;P`w^iT~f)mF@_qOKG-w`Bnw64E%8mXTdzY%3= z#SKV{Kz}%_)}hP(in$aoS)>zihF4LV5|9smxf+G_&-Qbh`W^a3jgKeE`LqW$4Q}7& z5scSh$4PXb&;Bx4d*AfeCNpFnjE^~=&-Lv6_UyxValsW?K&N2Y1A*<_anu||WF%ws zW`#Wi>Ph`(<_9BMm@cJgmxC~<1>T}=8D2}$u5i1LY`N$7Fnh&1#nDGG1%2@Id>4Cy-Cd+U+JAoN>8?|H0Ey^qg$bvZtun~#nzjm}(vk~+yAMUL4XF|RsI z8y7R-H)@PbSMhuYSc%>Dy~cP}%W$)|T$qG{83LQ~0??MxVOhd+zCl&@Yx-w_1<#FM zrjlJSGi;w=hbi_kgqvKEkJt>p!@qw3DsK)%$e0W~#na4>!9~B5PXf{7b`Vb9NEp{U zj)Y9#RC-LFIuKOKm`&HsT zHTJvw?Kj)G=%mB;sj2n?ckpL>%cnhZW{5@uPmlm7P|2u1JLkrMzi;qoY1CAie% z>f*>4LPwzlFRED#uNBRF{X5wI=h-U`#c$1LZddk1F(ve{g~*N{y1nhIi)M!(fpZr#a|el?U#ZzJqvY2q8vosze)c!vN?ac+SN6garR(6cZ`hT+ zOaxe!`x@#wl$9oT4g~n^1W6pZ22BIL5%{kAE2wCmMcDPF0-LMp;x?KMT+gPbRsZ~cX?Rz%Q1D^;k(2kkh}l1b zdpEAy=Z>lUF2?+No=2FnH-Wt8PUNHIyJS0Hy8KC`bWqFhwLsW$mv+ zSxFvK#3~R_G39F?A43FNDy|S|N;yVGM^{Mi#m@GuY$?56AEGZ>PhDACBCS#*W5S zO$Qau%3C!FLwo@&g~yF^XA(;C_ntKaBhq_hSRs4V2h*T|ttCohc8kv2kxOHX6ce`I zlZLm?s69HKO&{U_aFD`fD@7U|p~Bd3hYHdO&xe(Dr&X0LSst>N96petu?y=`-&FkcDAsI}g%>EFksIFUuFBbzrh`SPAj|ez@E+lS3JqEUuv8I4Sak37T5pAQfpbd_ zZ~Vl-O6Rzxj?3 zG^nE6vF@W;YdJ9cMT;ymM8*J2PwK^=KA+ZFe?;gdnZ<}x{6%av^V6jD93b^(N%Ti{ z(1~lxC`KFWigirr1qNhI#E~br+w@Vx7_#%@sfDgk0f?LfTcU-!Y-re#6z}+?80hgtX$lr?%>6zuD5=P3nJvdG6heN9Y>d zUT(^?WKM0nan(}^$hF@7)xy1>!!pq2d5YHUbI@ukM|w<;?UngbcKpnarH!U>^SrM_ zFZL(nVj|<1k+w`;3jRAco~c{9P>YKqsN!g7~di z;}3u~f!;W7SEa)5OzP_QRYB69lLquo)B!qikS_YQ&>%kOwI0sK!=$krHtihNVb5Ma z`-&BD@a@x^Bi0X$wmbb&zc;qGzWAL-UzJLPnhrnrjd3$)>tVXKQBdCdIN@X8EpPh( z)6SjWV7G4TeAtC5d(3*luae$D=Qr&qOb%N}qCJ8UMJ6z3|8wFdph-tY1JR!!I~g=- zOw|MV!(PJZU>ocFTRC66Tfo9(N`*=~DENbQZqB__c{Qpl25GYonKHXa#lod#fKkzCv^aXuw?giXVp z-MC;c1zoj9oE&>p;9mgVJ~MRUk@0Yy`O)7^X}m@{3oXI|j`p|58J9zTlc=57P?S7b zjbS|kT^k>49btF{c)v^n%*-`V>YJc+AAJT_^AkI8Img)zc_jgv2buPUDJj1o8w60g z7iA|3go`X9Drg=sNDrGtNXYkg|AtLdWvO(VedxBw0caf8-G)8V(`45Eo#A&`F^l$? z`ZrCw30vJoQ&5p3nB9Mi%_Uu>KGyz<)lUxrg~=n)+WQLb+~%Dl%Xg%Me$voPu3B|( z1MpS~w6ZvPm0#OPo4sq#$XU`0Tp)G$BmH3Rb;kn>I$^Aw*BAhKa^ry#{0pccP5n_P zsE{sugpz9`VToQNF$9bhyc6J8dov=JlOx2+22o%)x^pS&fr)Kn2JH?l4#pd2) za~Jh~37^Yk?~d>zZ(Md<9n)=QjQ7Np`ty0hwSr9uA}ktD@%~0$PscaH!a4j@;8d}n zf5EOZBi2l*F!COfk@%~_>3!{QQsB=Mz2IODTsAE-23>2~UzS-H@XOCz5;IXeynC1`wRI@83oOpmibD6p z_#a6gVYm+tViFoMn}sm;zsRg7m_y*^%Wh^!FbuOlCr=r@YyarH&&{K^+Cac^A6sdG zz(H|^8KMvbX#cBDBAehl7I2!2F#`3zr^vEk+Bn8Uh9&XiTUYaRTzmIb8s;-rVLdrS zaiFY%>-IcqwGGePyJaj?@!UfSh7bW&z&Cy&ca1EW{sv_wGDKv2)VxlzZNqXr#%#Fx za&w-b)Rj7?W07a~leI}YZW>IQ1EcDR&bXzs;(JG~)}`C5mEfwsW$vec7R<2#HHphV z7_R_{-g|3SkK|LZVPOXEoPS_u5RRYQz5wDM2BJ3sWPMQh7ZtJ1VM-mO;+w+I2D$nC1=5N93?GF8 z!`qV2FPg88+{Rzt+CMP1e+MFIS~7dHQhkD3UuPov8S)qSkxoAzbqt@Ta_V||6YRGd zpSKy0KYyGev!Z?D{lnK3SkV(oAdi+!N=;O1Ok(jOl%(gC^oBL8>}XP^Wh?kS9-u8>HJfpE>+|^;NT_1QR!a zD5VQzDXu`pH?N7wS+C!pgzh_?(erh$XekTmfoEv=gIc2y+PKakrb}-s#Di3+=E=a` z$@_GPT9{!wbscF?y#|-w!GB+4B0>Jsur5=sK2+ItB31Q|Un9R)%R33{ADlH`ok@he zF9PFyS(xtsb9TzygAu`!=s+D>*zm~VvZr|=Q}PbOX>pr(8>bP&@34Qv)OVrbpgjdk_ywUzQP0i zi0;`orUWZvdngE{tI?tV{^FMSfDvXwb=2Aapzbe& z;)=iRZ!jT9a0^aw3-0djq)FrM?(PJa;K404kTk*FAxPuy7Cg8FcQS|H|K7Rx#Y|1r z)J)a$yy8#=El0n5@3lS)=?_4;So1wBH+tM8;DdC&RDj38)BXjWaIg8F8wT(_^=0hM zuf)?#^l4wP7J%g44Iqs>f6N0L_wHkl-2mnOyGZpXw*Oq`Q886_l(yBtL7XiTM0xr@ zw|?L_z;lGL*dBc`_Y_qAU%OWcd#9g*1=v&nwZ9i|z=qYtXKMe?U94EE{2c2#-1Y`( zF#2Bqzdz7_egc03e767Elm`Fj6<&V(Uz@<&{`VjJ&!)7@fB!ph0RLZX{?Cj4U-R`} zcIJP5tN*`z{{I`j{{L5p`v1-G<&}FpPw!NI{y$BA;JlEw2;ji~@6$&bJeOg%E%rED z8CYEXf0pLJ>6g0r7tqK1AHrU!Imdvdm00P&{{f`)?&OV54)oRkyd5YLH&@yMcuwup zFrvJ{{jYl$X?OVI&QDe|ylI&qETsC+)%4QfyVxp9QFVm?rBA{P8B<GH|8+s`XPDyyyoz@k$-U7K&ccm)Y^?5qx8*Edn+si)Lyr(~_ zr^+I570Dh4jh9Jizx*6$<1J4}EY^lJ8Uxdw96ARCd78IFslYf;L;ho;& z%5w=oWW$pa(d@NO#)!I>W;rLS?o(e)rOv&?{ADzhIUQ<`-=d|KOP`HNV1LWq%&zmK z8)3*<9`*;h*G5X`XbM`BP_F%tYnURFXNq43RbN^z(~Eql*(FGWp!&}Z*4E=i{qN9p z#)&iu(|O7r;;z|^5hlA0F`dz!OMcQF(_5y}KT(8AFoL8shn|m8Ex@tYf2#igOk|4r zL1aLb^9xxBsDJ!k$o)UH+7E!%&DIMuDIws<4Cp`)zudvEa~=R#&eqi1KW+L(^5++i_|x^WU#{5GzM2}Y`Lk>f5LYFvi0{8AMP5_takQHt_I$g} z>I%#YxR75)JzV65PX}7lDKA;cK-=LjP$yrZQZVHs`vc&!CHO)H!R-K`eYp>iYHoge z-FOM9i2~vnpqVfTL|iLd7+z$}vyZP`Rd~GzBCGj+yH6(sJ#IZi7qiM6Ywh~V+wbgfDp)YuYiZ; zHOrsNZh7gHk^^GOUxqy|V@s#q zI{?SiV+4S$h&wlBz-%a3`Llr9@DWc-{|v~sb_R+J%Wn1w<%}1PMODv_uET_1D~`BW z1d(LE3}!(6pf+9s7q~^T#t&3M4i=P7Yv*Z=KzOU@iwXl6;~MFGWbRusm&XyK!>rm& zZoG&NfW4aKc6}a}H16^hxcmn&tZ?%|tnt?2|6=Z^D_*+2b+IJ_3uh>*lUKcPK&ptZ1XmS5ytAy{EN|rH6-8MDPAY$!Z^-Ty zzRMX4`l5jOUKHqP7tWjj&7A`NpqNjk4R=$=&p;Lw%X(E)?KWx*oR!f>GrVLf41w*$ z1%dD2x=@yIbZM|_Nta>rk>{BcuYJR8gsM}`?9R#y*x)VbELv|_AV!*2=kHs(>zTev zECuFsgn4lq#%3vs=Qu$}mKsA32vHvI#tvb`dB zx%VF6^9yZUaNSGTXCe`&-Zw#3LAR>ofImz|CD8)^vRz+5Rk8xsT6<@4?L-K^iO}1_ z^J5y0;JctW1y3qUMneqg;ry;S<87AHNL~;}l}4fClJ}%RecJgdpaW!8KWp;7V;PP^ zIJsmq@)7{~05cB?;ra%yT;OPC5|s}qJf%cTcqX&9ROwD+c>~G?*;J-}38eR6Zg_EU z-;#@c>J?UMs(RZ5z^grwnG}1en{iyspUj51#Uicm{BTua;@fy?(=_)}%TYHttC@L%W` zTE=)lMbQ>e{pHQY9lSxm-!IJ%SMCt?cUGV_IS|43)V=5=L1H&W+*v_{OOjQR=;Cs~N}sTu1y!Cfq5JNZQBn)Tz?U=bRfu0)aebR$$^1!mirR6 z9O*jTD8SdIk4|sm)=xh5rK*y5?UCaLY?8uBFF4*;>i?#Jg z-Te>cN(N=<3D2{5z&*f<6Iw8nZvbhn#?dmuR;^E&m1&#?4!ps>?{MV6^0q|=SfD$x zkT49b&-wxnB zQe-Zy?uG|{s|x^Z)-rh1y>IEAOqYIy4Hj-Sly1gbybmYdUCL7ePg#pEU!Pz zud~s)C`A>jUJH_s<7P$a;=TdDq6~t4GrYuOiY&mqV>k-&l*m6a>mq|GTr9nbCj$*ogSx^_lWY0OEo4A z9#zvy#*n0O_ilB%1egwWzbo zlu{h_fL2O&F4Ws;ThaLhRG{@Dy(3fN!r0vg+ONe;sO~ZR8CH(48pgYL7ob{%)Gcw9w)_zs%Wh_3##wi@)o3H*kZ)1F|6DCno&Mm{7I_>HEF~r($Hi(I&SSf;Gs(N)NQRBT?(HnnDx&N zk=;ECn%Bqw40I4rOhKfuD;$D%OB$>zcLb@iA89e!WOYW=494qod$sib28;Bm>iB>0 zZX<)3DwYe?SjP*HC!d^`3-YDa%0B8XNPh8+QIq`KCi<4teY%ZKTPkY$FJ7drHtJK2 zmHquBUNd%==Nk0Zb=w~Mb*$;Z=ysen)aN(hnl+cDjM_IT*(HN3Bo2Jm0~54>I=HGi zM74>6ZI6~C-zXF(2^P08<$vppA8avMTK@chiW;SXncDfTmuE z;j+-*8w^%x7P*ZJL^6TrQpt$(l0-AK%QPuxlF?i&hxGmrRlKAvRlHky^VaVu#nlbE zo^ntq=Stoe&K1-;c`ha-pS&^*>CE{eIvIdAd0;KXlvfrkE~^VGN%izHLB5KN&dRRB zUa)Du9!r)FmPm;4`pO2a{#;X!keb0r>JZ+0I8mASmLwgzK+_69^9^vSF-Wi%v}Fr@ z)0f}5L8j!TK$lQJ64pgmXmE%4rB(Rgb^QWin#*QNrZ|bM5WZi-DQU+U(BpzOkQlkR z)d$Il^)@zI&>pcaHP~5jB5kI?7r(nPTeA}gFzI)W{RD?=FNJNc6x+ulwv}<{FsT%L z_p{(DR-e>_WVr{ayvJB@HH+8SJ79Pe1@5GbrJSwcrpM;Ym8LC0=jN+oMzXvagCCl1ybj4I(5GkvP4oWZ#~7VqH7sXdCNsXBVZw z8A=sFACDm^z2Xb9*;>73_GLqad3ERA(~ig>qRSLxcn>p^F?k{EV>Y09qpC;3n;%R` z7q|z0jhifNP3(mj1(_Jg9HlpNI{yIY{X3fp&e$X+RP1a$jxUCxzTO1#N zero1Jl`}lJ3-Y1j*6lLPjYZ_iQZBbFzf*oy+IujcfIh@CSH`-0fginJ2D<9zl?pqE ze9lBCmt`5j*h(=Z7@LQX(jxqsIQdKdVwx7_#)`iE*O+P@aZluaJ%6NJKdhKT)Nwg$ zu~M&x4uEkyN^~cKqX^VCk5ENgM!L9dTJFNo57T(G$ugmft&v|9^(iYC>P|%#P^wc& zuu=_FM;P-$1xVHhKr`N(`ey^`$|Ip~SiVu1D)+T~;NI*0+cl)w!c?rJa2u3RqtR}2 z(7wa(_}_;=Rpiy*NEG}Q&v;n7fV|~yIhc=<%eRS750!s!PyBm1DlL8)*-5|m40=gC(MD-?{H=)}u;&}S4r!*jWWuH`o>F6y(_;KSFFiik?JjTgdh z9_&{RE?n0$2zydrCmiAGNO=Z!G|1%m-VQ$y{7yKYr<8f}Q6DMKlA}G)+hgEuw9|-_ zMo@A8TdYk)ELyxt$2k{RhY_u~>h}Z)be1fbl_@%9dLR1l=h85f&1E6#bue+G-y!kK zuVHl(U_pzfM%wfZf$%6g!VnKF-f$J)5aSaXD2spmzveq;fdFtAt+dKQ{lh}L=|>(b z)4xy~lCGaV;1R-f8d*lxi$DmD*9eW3rMfpHar=h$KO&=rVLP8qyDB`#uD$4TLEDY` zi~iCr&^|n*Xxf9_p}!j=?ADW<+w}J;zkrAf4vwvpJ0?F<=XEJzZLCsDyIk@}2ed=_ zO)>(`0{sac+~7tr|4{5(3ZWt)#;7*vrnIGDA-;luXo$U^Ps2l>XR`20tr%!oyGNJb z^oJa+<|{ZG`tS_DqtO#ZMqlvTpqNSD<87Pgi>@!*nzUTAB=u1xF%m1EqS5v<2|0$E z34In2$>w37nyyPH^HSjre(q{%iY#}NL^lKag0JMLY8Vr>EpF&^&+nrZ)iiNtbj1G6 zQIrmIy&9JR+v{(NFc!Ym@N2{A#$)HX?vc*nQZyepvK zw3QMKyZlDUQ&|#KR*+Y_U{I7bD+H!H%Ct#q}=oL-?*PI zgt;rAK~)TGuNa$&|K}sUzy7EX*Xm~i8 z)ZDSw8|+MU_uePigRe=F8Z4>VE8;xXj||#xjGL`supIa7p}bQr|J5wn`Iv>^_S+YcrenxXRi$mPVV7rH zvMtyKF4+uE^vU1NqulZ9)g*O#hE^bBU2T~6Elo30`Nb63p-7mAi%_)@N09KxuEJ0% z(=liS^A6f3#phFc#z9A0Pl-}?%A-f#JWVS6c(t5%GtJ81e~w^%==J~Rs87||)=<6- z>Fgf-+FfU(t#%s!8Wn&uefCG1i2%VTSW#?qh^kg{w^EgV1OBwy)3RG83dMEV^#??+ zA1zU2w2sH(jjphBeyKYL{8&bnSPDJZTA6V(K4vg0O%bg8yqy_OEY32#6$^}Gu6`mr z2pKuHC*>c%?nooj#C=~){@sVgRvO{Y)U-*#zTlZPR;qMFWBM+1spfA$oOVJIPf@jY zOlilHEaMUU>V@7;`L9FAaS9m{{8ErK?Fq9?omZN_1kc<|9YQJ=-Y0o`<`|4pXktb; z1%wJRMa3v;unE(4SpN~5UPHUq*J`X&ng%IifLke)e7{4>cr9rEW}tAh`~apQ*eZvB z1of&UnY9!vG|ktBvL2ga4!p+*+=1UTx0R4$mA;Y3Om59d(IS465aXM=7cGn%p!~-87?MDUrCQ zIbnN-Y4Gb-IWV0xp`$5bF9+UZ#7aGHn?18nBqe70z4)soJ| zoUGV;bAuSHo9zM7As7=u*(`5|=3N9!?KR%M+y!!9Ph2aU-))3dFd&~AT}3Y??5nb2 ztiN(_Whj*ZFX<^s;XllB``iYKfov{iT@$Pv%pGc--q{89Gi&FPrQRyLzNJ4YxwC9m zSFC-bFLv12a7!9{#pW9vE27gTFvrM ztB7jR?Z(_9jUvdqZ%@m?`gO38m7=qk^7p3(9phbbAQtExp2o6?_J8QW6@`Z%r1EEbv$5(4%TuvmhAR%nDITyhZ)h|;BacrK(~23V@l zhM%6XN|8wL;MbYSZQ3isls7OIlsaOTO!_Z;!P=Q=f98xvF%;kEpwSdir6wYop08SX zgBjNT(BWN|eY#{cofP=>McRV)_UUZ3Z^EIbvIXHcy_#9V7e)4|>u5@2thj$FWf7uX zh~v!lEVkb}rfpWYB|^Qh!}wbuIWuiYDUB)852|6PYu+!-r zdHuFC<|ecE>zvs*w5*NYG9v$>Crse3xhCTWpy9(k&^-n8 z4gyWU9EC152Y%|Pg2JUJD`nb)8QGCMoAA3(mrbrg-%|BVXMAV{vnpd&<<^ImZSGl%B9vhWoMhJ6C9P=U{cd72%4)zWB zMyla%7l6*~h;`kudtz<1yAB#~#04kz*3K-7*Nu!7MOBNB$Hr7l1Ws}%#Edpt%l-Yt z5&SWYN+`y}`>dt0R~1Ae)VApO=$JT*2;!J7dr$H$heqJ4MNy5Er#2(r2M+YwIx%mr z+knW0sLy!_IEPG+ygF=nqZEIjk<$u#Q;>c4a`+LDA?m3W*k$K1*h>D&q3D)I2Pxpt zzR7)V8Ss6_>}ABD#xyKTL-K{jp&20NiVqL$Ao<5xDn=?s7RYAA!L#05VS_i;`ZZk@ zBhdhzmE6KNpqZ4KD-dfmJY zs51*#7;qX|Iec6Vf#zl|r~eF&W#hSD&Pe-Kqg&OSX=R^)rruRtZq(wR1I6PZ9#S6G z4#?)@!dvDO4ySY7-^yYR5*~~+mk03S(m+!%=u?hN-r9BUn~R^zcnc%GjKBJ%2Knsa z`Uo|1CzG3}aOpW|1@TuXKQ3Eredk97nbhk|(Fo1QH}m;;`$*F^AIv_=zaX~O6tStJ zmtDJ5bEe@NAQ5lc%YQ9)P7WF>(@)hZ(W1BrUvS}b1o(}&lf!iHf)-U*Bsfq((b04y zUDLQ2)BVCqf35P-V18hBh}-pi509M|`V?lFzE+1&;e)R*BYs)Nvc!2f>2#CI!bZmG zZJ-0Z9UKeM)1a5=E7!GVDYiTgXeyeEM=k1f$J2pMR7^YE)8KYnd%H=FYE;a=^RcgN zNj52y;>i?8`_DAI7G6KCwEQbOi|z1)z`+j?4~YcDHow&Yap8xr@F8Q8V1GoaP1CGf zhzUdj3?a`jE+^X&iy~YzvFM`>wfWxnPh_*u^G;h;a)s{CV^u>7G7ibg+s&lpK#2Mg zM26L0nJlm2o2{J*Yq<2no;0oMrhd(Kl;M%|_(?M^g{i^~lv}lI$0O=G6j*E5otpj} zti+<*3-TdL&A>+-rM@>zApym>77)r1kCF|Vu^OHNrRCV~ojX1sPDWboB&2eSw>9ti zD4W5A1v@Hciny>D*2syh_d#*m91RRZE--EZ()@9(Jo>X&4aE?Uk3_4MC|9-cT}gk0 z3x}W~#8AU8&u0I-=yovntJ=+FLsLB=p_KGj{hlfCy7dmaut#CCs(9*(-58R1ry85d z3!fl5WLtf)iu%}3tbwnLXJkYKW~-k~=o-oUW;QQSPP-PoB<>y!V1M#2qi>Gqu9n5D zXc$jcb&$ujvq{i#y=fS4gKUv({IRw?Wd&1~NWY~ciZGru9>FowLE+Z-L!18iHT?oEzR@Tl8I<$Y;Zh zoB5iJK@asUN-LYmS4mS0?UywSKFB17zdw%ml9F9eWC@tP)*c$NOsyAJvja}tkT!d8No~e~ z8>X^yos6?;=mdn0cZH7Q!UHEI?UbmOv}IagaRUOX3V0j&({-u;p(}v|&k38TEzQn3 zDCX|+yo4kc#=F1N^UV;Cjo2z(1tURsQ;3NrYeM;h*D{UoD}WVF_9|gywZ#=MXQvwN z_Rpx)bd{L{0bkyfZZY?XPw(sF`jYY*B56E@9;+y0ZwvDIit%JrN>yMW7vXUvv!owZ zQR`Kowa}2}DF*YIE}3ec2E?OwSiTFnxtP~N10EyxckfAKxe1avSe%x32jzWuwS9TW zzUy90R4yp7yBWzbggnohLrg!UuGp-H@1okSPOzDO(_=knMxCnw9v4I>^&UiqISZ^k zKC4@cICr?1g?-Dq`{elx??TXQQ1x;c2aA~$QXvvtYG@8s%BFKIG>5(p)W*qb6pVK~ zrnO;#EH&R^tiZq`le|$W>_=s|Z2n90lD*iJ5E&QR(5L;#;Vf{FL}o0)*y0YnIe|%r zL))ORKG*X4V$`m=B_$&^M?7oHd~rlzX3__>^0d;cP(hV9rxnq3}Mj& zJ?oL?wUv9M;!)~(8_B#c;Y53vp?q_fl=d+vA$$h(i>syrM@?k&U;F23xlQAlX`ZRb zQm>!7z{|Bmf2SIT8IYfT%3z%#z1cmG;n=Vroj;xa^#gagxpI0t?(7mZg{9MWl#ZVG zjqB(R{jE$pD8$Y7$JO7Yof6CbyqIWR1kUD$nhe#GNz--P zNjfE3#(}8?E>c8!cn=)-Kk#q3JY7_4`nCdE0}m}EE!yk0uNQi4cXWzgHBaJ$CepoR z!|`@Gds&-x0riJxssD5|-xPBwl%I-P5wS%DtUVmK^Rvqam+0if`DUy0)x(?Uhu;GQ zL3%5xgb1ACeKcD26uX49T_5QbM#>efE!^u>j=Fk0V*S84nYeFmh3M5;8Ji0~=zKh5 znU?I-r&LHzoqdN>Opg!xfQu)kj33Py4fj3{%U-pJ=K9eXKV-3K&?Plmu~mR{PI{vE zTqp6Z=!HZpy{{o;lb%j%4Uq3o!SjsyU>R8yBQt4uvNd&OWp^4a!RW|}*_Pv!R3n7kG|;Vj;He3AOz-IWy?6uP zxq0Let8%Z4Z1-a}nTwDD&iKu#+QRjM<`#4#qopAl$}z3=?1MAdN6{DChg<*4w_XLD z9C}s%Wsu<;#~Q{F+`NvyI~qsFhabv2`$Jj*E1yi@cHT-}nXEXF8n8GU#l(387ECjI z46;Jl=?Et5&6LvAx$oM=!=-KxfReu>K?)Z&S*PNW6t5O$`400uQDt$Hg>1%|Qb!eP zwy96Uu$R8TelAjmHTBt0XvOyO#4nb5dwJ))fo9xo%lUpnFH*T|%WUl5{bh@Tw`zfB zR(PhU{&w@XG!&Ps_Z!I-9r<^@UWP2h*_>Qx6+AmV&-n>=R1A9Du%VvH(UA<%i4*gX zv;VWQ(lBv<1dfXQi9=NnByWo{=oRHDJ`-2(`cwkdR|$zDxgH}?zQlaA#>I>yg?HKJ zoHP_c?q7myP(zZl$Tf-72(W9BF@e}Y?95S6QX#;apb^iTh1Ul9-Y!SGo+9_ zIcXRZ6aT(aJqN;ZIh;C=yc~U{J`;Q)c7M7%#?@Z)Zjs@O?j3a_XVZuAhWJ- ze5-70?sMB4v+xHCkN74PwvO7~JaRuS5wF2rpVWJxsVl=1zOB_65jBJ_32yq>wi^h6 z5QacA4$C({92z)5C+mt*X2VazvA_&J4k#Xh##;MV=Tv{c9zo765yrfy!7ZX>iE1$_ z&YHBA@$-a-HtAe-=n-AONZ_Q-k>mr3`C9q5-Cg>1*Q(OM+o`Z`sZ1jQ#B`v+YG6ii#&=Qfojz}# zfMOAU1*OThbXn{!E>xid^db>0O z?rai0xd+6Xo2rsYiA6j~j}_M%Rkqo+GU(KgY-3_P$Uh}$t^6Q=Vt&}84Kby?l{wl0 zCp;xCt7zx-2LGwve@v{msjbuGXjP)8q&ahXr(b7c`Y%JNKCb8LC(V;ff*i10N{G7m z^bphEPMKfTb7e_OHSq}p?J+b5uTvnD=@zDQUvLa6sPnu>x`1lelaS#ZhDi_^lIj;l z8W5XN(W)sDNd&hbu`aPN)oYAvd!y{XrD7NOILSUHn=r&ssQyC^hSORe_Qk*Xh%s-B zO|^(hmIWFiSB$?2`XnDyWDvQ}XO=K@9`(_sr|&kV5yOrH^;P{FosYO`^*D2wM?1P- z)NU7bSgL%4(6magwJ|hX%?dnamh^wm>ZqD75NdobvRMd@zO;}t*Zj*?1ZjAU373yN zpSB1uMO6s@fDF^sH>!5#Cfij1$c*KN#;Vc1#$OQW^FI4^KVt@seWG>W$iFe~Y*_!~ zr-{w_40a^h@Q|k$318|vOf9O1m{YqK5L#(KT-v(F1EkcBZoz*B7bqiR zNj>mu=NUi^MP!F}K3kc{PUNjH<-G$UxgV50$c<*nRof~Nm1AF372``+i}1;Ix(t3r zqjX)EmG)nd-6Ub9jukAnkzzFeC=MDMsIPH}9%48XNw*YBFVX{;1~ZtNoo;=r#>l04v*|kW}siAt1$=~xTTnrj+P4DcLD_%z$|(Ecv)xp`aHj}G6%Zwo;Y`l$48sM zW`>h@OlN18lW~r9I+x}6QR{+nBG=VP;eHf~QW@fJXef5M@Wt0r`uMaOnMgXFBV5pL zB-xOnQW$+5iZUNHlr>P-9P{2^yG%;jLP=JSFV_I^YQdO}@Qo!nUCv|R%aOanX3eKe zL!PeOe01ydv`;H!>4lMjERx_6R1ghgXben$ZHMo3JPg`8nD*mP?$e(dea<{5!n6kRRWxc(0JB;IR#w>w`Q6$^LgtpTx|SY+?LCE#LkhIBia zJy{lZX_o2k(&Nc{{?g^4W8NKAm5hOwb&a*_GzNq`dE@c^Bp%mkptX^p|HU}{g~f(! zj&7M*x|r(&tnRfg&y+SbBTR@qy}+$}sk;Ee9^^bW`;!rXd&PE2$Up z(oyR7zUmbGj6MxP=;N%Q4R_Ywn-7tONp;4+H#v0at`HDZe$z4cK=TFD~Kjxgt{jjqb+p1qWQXy4D*r@=; zbitg>2TIQ`IB;#(9dv#S=%pc4Z@bke-cBs>n{+5B*`==xK6EZ(e9g?Z^Nk34i{*;^J@SU$eNpwuGrLb$F9^G*CgF= zd4%jWQrNS)>A4xch~rIArKwfs@GHOB*{zxOxg_loyokO5Eq)l_0xKIEpKm5(Rs>v6 zVVxBJd9yU)W?lgRwlk{hcw_Xn?%Q92{~sC){zqQ^zh3lzZT>Hb{Quhg9}B_%C$+yx z09NX$|E`eUKVkr#uuG!$0x)>dG#_BTM8-y~0STR*FS2-xARvmY2N2Y*0=W(S7nQ!V zrK`7nd>t3Lr;k9wg(r}*_@Z(?ty~67P@Pw()qrH4Hi#d{cm0{iS+Wa+n8l?4%oG6a z!iQJr>*O=6w+fia^8sPziyLiy9ALaD`uzsX>OGx+{`elc`V3%X_zK1ZfCx`Qrj4u0 zpr`d702&o}wDE#q{&%gFZ5+fU2!L4+0kvieKw)`Um;``2m~J^tK{SlFXVT&OoLteL{1u zo!0DM0086_A6PElL!BV(3h1)4{=P)7sKl-UKvWK~(~bwn2NPED$CO(QKM$I48jYC3 zQ`4}g!%zA5?{HHYbzm5PnF*Dx?JuC>K3Sx4DEO0-Grsn?!?%p+^}^_Ia^|MAp`NNV zg1gDQGo(SeggyO=AO7b`{MLnR8rO&wN2Ez>$6{vP`?rYU};$HIlw_ z7XsiO({`9y!Y$PN2l{I<>usa22?()s*_isWEMS;Ihl4Q#OjwQOw_fAEr%AgqRfW}A z6Y4K`ofr9XsM3_s?hBkk2>T-EfP);16HpPhI#dF=WS9i=9NM1@6KxqlV1skw6$v|A6_TaxgJM`v=G_K;iK^*$ki{sBX zBNAiI!$GLAI?9)9sfWS#Y*^**KL|!&Hy;7SH1{-x=&HJU<)g|3e}FRX@eoRZsY_me zAR!pm@pSA&??k$iyv51={lKZZ66^Uz8eJ}<91M8QcY(-4>r$O}sg?1)P?OR^I(&vv zx%_7iNcY7^iz)1*`HOLYZdr%I!g)HxPVyhY()Qp{0WX*ajzk=@@y-cm~&`z?&x@>mq0l-9y0d zKssiSWP1;OKL3Cd008!4Zxx*au1O~qbd|Z)AAm)HETB=I(X}(?NeF6h0J!l7N*dl) zSu9T2xefOND)YI;S$n+cHgP5OJpZ>Z7bz)l%A4~qPjpI{@A&nOAkZMRgn;ocKnV8M5$k=g5s;3Z(RR_I3V#5*l~G_hK31 z@t-fT*Gm%t<=iU>%HMqX5qf^YOZ4?gqHv32tWTU_V0*$Ah0AF%Sd zTL#-*qxabIMX!~pxde%qBmB5$?%c#oZ0B=#3DKgr(quR1~>JU0qA63hHu}W zddua!jf1LnC(&esy(gz1yh+@{M0817&#BSDEAfhz${MVgkhvnh%0AeoX;E0=o4?*a>7s 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 z zk1g>|nf$xr3AFnvcZKpCn%mmXhjerOopwgEIFpC6b9>EZd@{~-SNdynppCz67zU`x zWcqXDe3Whx1={mMwDUE)6Zx(oVWdkxgjXL97Bdb?n8>mz3!Nfdj~cgdCf^rAC_v23 zG5ZvL8VL}bidAAeHn?n{LYRMcsMx^R*a5PN=VA5qS9XINB1np`V>Ujxm-+;R{+q44 zd4pf6IEVwC-aan?XBPG9W?O7shQY$F<)-_Gp%+Y^ihJ6`Jun7Gr{CiqQ^l~=4pj>$ z$zlMs7Nr}O>*F$B;D-l!r?d-~whBotsyKi3#uqx4!g~!~kR6rsFB-`5^VN%Ku`LU7 zBc0`tl7MP2kz;nBqa`)i6Hud0A7oc10UjWf2ysp_g_p^SXW`ZtBJmt2nSM9>*nSW5 z^wC-<@xo#9ty-^l`wHk#fs5Zp7tYyhB=R0dDPMT&ipwanQvl8e2Duj*>uTq}Ro7}; zw19R9UBeJK48L4}-e&hFIWl<`8QBx5D&KPls`qbKd4^u~A=3)b>1Z!8ND5LXTG|B1 zlPY>CdYFW|QA=}ZKmc;JF|e+*9A;69vGRxz8;QU9C~H05lIZ_(&=k9})8uhy1{JRn zbYWj>!7D6B+lSwB)W4!P;+K!gm%~d4=%Z1V(V~TMARLGsSZT?-KAEUG6ma9bowd=whng#cSnUYGw{}ci<|m;G=@1xd6ArJ})TroHwK%#>>rcvz z;N+WRrt2YVDGea-oTp2w^>J)fsfxjKHHl&Hxn8{kmKV=M$5n&N?n3p)E3Sfz9j7;% zWHhx1R<+?J5=O(WJwd?%&ir z`r`6bl_Gv{;8O|gw0P5Rj1bDD&S07!WFr^|`o2USFC;d8^d1_F-UcWUAG;%;|C5;Y z^VRaV`DZU8NhT3`&4A>YwFsY1H*e-?U}8 zLY-x53_Qh?pWZ{`O{-U#|I;k@;-qK1Ua~u$6e7ZM%_h8=EK`Iia+@{ie%f|^g$!hoxE#z~RB;(MD02j^5LUQ* zSGwQeI<41xI{v~T*sQOg7h&cL(>+}nH%eg#adse428LU9-&rWbWw(8;X)s^LioF~< zNQ(zFBB$eKcYcH!g&*P6$?L~vG_Y?vU2h3V=aKG%#>`u&cj#zp%grN0CED zNYh{+qmb8>8K=OEW2zf@-ev5o&dHX4kEG?-A|`L4gGmr=2ZVS3+WPYb7crQ8LM+|2 zq@l&jJeZxg8KulYfq4WQ;34kwm^Y60!GS0(c}mL*RLuI z_~3Uj;W;_l90VGYo&JHkE}~g#OouZWFV)FY(eBJx!?~n#a%A@w@;>$`$M=vHsU@q# zn>9A0*Xzmn;`zWzO0pLY#I1K!+%Av!;GnKZVSdC0*Q07Yt zJ^v>s=PKsZn!0EgsdhDCh4j+pkHcJ_Uk1;7l1TQ zn7)yl$ZH%Y#vb2~A3+(}CB|Jr63O)IT>(y^E(!EGNUsdEblX5Wj^VGr&K_tKn~Gs?W4^>4>lR-QCBWpgEww+&Kk;f7_9ljS zbG)mLqS43g*OXMDItVvpNm_N#mSiBT>0+_r?d@)p6gGz$P^9@WW|ND#>`auurXWkk zROQi>I1x~b$^3G<%T*(qIjHS-klk`S&Z%51AgajH+ztgiYN308*=DA*RvaLtKI?rrRsqp#g&mx1{<^6E3NW$r zh`p(wZ-MC8g%+vLdjJO8V`0EGWHYuu&}o@76E#n~)O`#EkI-jyfz}_Vh2oSm-e2u< zTx#~%bn=_+WNpXmh<1+GhM2!e8W>6SHG?N3&<3_l0x_lh`O32XR{|B1z}INrZGYUe zXJpEG|Az{Y_z!Xu(7=E#yAgxD7aKgXuYv-GjL9{wB_xwV`$bmQH=c{Ip4zG?>Dxt- zLe25O&kWF2IY`->Lu^NzUgr5#eF*Le{%{u@v8Dae$N)+tcMp^|o-#PFVGfsqIqN+B zJEsfn1^5~V_`DzFiA;X*&?vnb%f_JpW4RvEWxIAMp(S_owF? z$AUP4oXBC71`3!h-37;#iURUn_Qx6;xLD8k3e{DFl7u}dRtIZSAag=wv=P)`y+%NI zQrF5XX`L3~T>OYWc81_At&{b~5m6r?knnSl0OLXqkY6q;m3wobnZ*s5=-W{32C|r+ z&o4(feEWWrd#>%RPCqL=7n>ptqMT~wOOe^EvmlVQgMN zlcd_;)I$F)^Pz_Dc1(Qj^C7Y0t%gdmkJZ}werYaDZPqdE!hb5P zGWTz?{eEePpLF_{l)?D?<|wNO5tz{G{o3i@G=L_sIMUjaf7r2tMqH&ZbKhIa^N|iPQ|od0^*k!esVz z=$-s18|Fb|Ev0x=G`%Era@&4%;b_|Mmq>F@C{$v*8?OJx1UoD^0FC^9rXbTmy zH1lDFD^+YrS{*+#r*57qYq>owez7ej{uSl}w2S|v?AmaEN=48;9JXn%M5Xh#f#Bvk zz2hs=zFoNwiqhx>4)|a;unk0}+f5XroE*rDP LBRM>42@*!9W~~=G(c+)>$$Q|4-^R2f-Rib%BGu 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 Lg^s-R+sK~*ye5E< literal 199739 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 Dq3N=W|9HhacBO74V9PTkp@(C)J(T zdFXyChE@&0T%0v5!$~zpmdC3tK=AzV96mqak(}i}p`iX5`#%%HQUKmoU1kusS*&(n z<%CfsgrOSFg-#wy7?$`wv;}gD8s%&iT6xl{C zrGT|*h}3)*0J@7$+cfJ-8*8HDz6TeNT`REiB^Hlmaa+mPXNUDlQ4+X`E^mGo)&0$b zj-D6TL#wO6LWgc&z7JAx#H@U%j@b zW^v|B4_l$aE;4HJp9+8i{zBI5t}=G7?=1B&jYZdYxi;KQwF*GPcW#gk2|>GUoY5Y z!pD}#K5VVwmunA|#wjNDWq*OL?lC=z-?ILx@*HEz&C+qEbCtw})!aDCdLI;gPYBAb z?i>BHk+b@Te}sjOMfRggW!Q!;D@WTGa8P}LM2;mrQ-O!Z&^dM=567q55D@(A1tzj9 zo~pLP@E826GY-~#TCH(eQQH^{U-Pz3=y6>VjO0V}fW2LL`;&XVUG-Z24|C=8Y?fx=k(;OX>&OAJ)Se40n@ukc~ zhEDM^WtF$(NLW&f?qn)Fsu|ck-ojlZ2c>O2_LcgNq|msZcrCuUX~i=4OV4m`b1U9A zpA}~KhN__c8*=^wvZ^wg@le<9_hf+8wzNGXvk_D3(0-$$>Cla}HLS3`ATKdv^0tjupXT z;9HCzBF|9G1;X)yJE{evT#mvG4V_BwX5N5ln?mUw!oFRVS;~~RnhJ2G!nu_;j^jNA zFRi-9m1IGjBh*sYwp^tBo@>@@6PiC1DI(H}lAjXOX&1W2ey8cI5^}1h9FiUbaO7>8 z{kukM@=xO@bh^9sFRTq`tKY1YDXi!N=@YK8{42HF&enxDened#rbZ9){N?RbzLM9A z2>9Pjq*rujjI!>|&tlNocLeT~gd6VFHpyyKUyO-HWwi84wlOx?6jP*UItCe)8~fO4 zHvabUICxyscG0=*m!Q;+EWhT@y}*hs;Jo2D$0_@4WPaimrVIq;e!o6%)i{2!KhSwF zP-DX!iPtb1@}#*(aZHc2UT5MpxG%G+O2r$`{LSU2A6ORGXWBS zZfm~|(v~&2Abrj_YjizS)DMf`Dcpe$jBLH1QmM15GU1m#t=0*hn^!$Zug>`4LC1En zDodN$kJA)=Z+AI>OqJb;M)zWIRv6Gc~S$UwLgc ztY4gQabFc37!04c`1PNlmdA_qk-zEVsmd4*L0vdyJMm?m*Jsz(n*G>`?df6aJ6RQA zh3h{zD&y)QvZ{NW%)A*;YczhHT(`gr%Y+i`O0w>}86s@)<|ta#vDI#5B}#S6*`KH= zqY6B)da#%Ni?42YIjn?Se0`lL!H2JcjqZNThGtnFbCPu?f)03x0GbDfM?vVvt9a^R zQQHzn%Q^LzMbzZI)Ipi2Z0uO!ZtIVGd&37Y{{Gc6E=r%%;rb#bB(}uUDM9)m=BM9L zd*6>|Fx1Ja{G&NBp;@HS02Lo(N2kn_hvkMoH97m*-a=&XS+#A0qhXiJS=b5;C{++w zF5{}T|H91iSU1cP@U&L1Egod2id|-ShFpvEn?rQDx#0HR36P>7^ekT#X_RQ6Rh<7~ zyue5MOQwN10ks^*-`bM)HmFxYY#jCP!0IS!2tu&&_vgQde=A=8`ssfU|8B-#zx&_A zzt8yB8UDxNf6w%P@%-e+pfwJ^t3JTq^$4KwJwS6}Jg52$U036z5n;3c(1VW${qFx{ zT>){j{Kr|ZN*hNOoI7!(GacH|06WloW#8k357|C^54#AuHIZm25MVOxdj(X$2#?bI zOs8IJpzu#=KFz)L0uqnCck~vPBN@p~5MqpAA9BD}yqnT%7swv;0@6!_0mv@F{$zi` zNBI77Wk-}R^ws%rOXVhAe;ybiN%?Sgga}%I6(Ia}cVB##exr`)WPmKgLabwK^8=tH z0XW)TfRuwW>5`~%5&8bf^$w8r%5&9znS~IL@N&A}?HKt1uC|!C1nSEk_kdExdgM8A z`}^i&g0MD(Y{wAQ8KM@O3?v|=E1%7elf9y1K3D;Q>4tzUFRJhS2|oD$s(ld7beA&= z_pU(p(D~@aFMf8w%^&%ofTZsOAStFKVNP_PR82SpH3HfN$53VU@N931ALf9Un=IID zXx+Q$+>GikEfb=D1qDD;F{24%uDt+ptfZRbdfEqHpfP~!^Nc;<{d(ZN@dmiDo};Ob zjG(hQKj!(9_>{&~I;yK@q=E-m0992kbzE=Zdi+}CO%edm9Rb#?3$Ki>p~&ZHRsMgPCW_9M{fZy>A-xWK`MnNZDznqhT|N)2@roAFIa|EthiGNxu3!o~7# zFFXWfE@xR^3NI4?tx{-Y3&i}!NEt}419VP6({9geT7pk569iR}N`21?9{}aS>HK-5 z(QRO(vse7+!}^631lP|fA-Se$&-R=??dZ&_2#m6m)AZeU(BAiiY{Ny8%7b`)#i!Eo zTQJ5oF-hXT@3Mmm4#7`@x?{y#wfiz@+V9;ffnk2vL!qMPZSu7OnR}D-9J8HgYvOm5;rSd+U-dGF{LfTr~e_MMxBG0rClzb3Q?m{T7 zv21ivKXC_$iv|{?T;I5q?|O`UN|$T&dB3DuaP?@--dDwlx&^GcsTRYh9uY>4w!xv~qyZQ{^Jm(yhcts(g0N@5dm zoH+^&a`tO3-zB;!*4P?LYC@mS>^vUw3Ul2*o^t!J3RBYgu-*$zN!!zdFxs_#5QqQ! z*u27^wLJ@JTvF|6#}^7eckkbNbEz&D1PQl)+XrI6^6)Ix!ojxdoF(k#UdvuS`caKw zx**}H?)Esf8%#| z&!AV(^>R?uH-3nG*Que!6KU!F}RSjl%mc5rSjKUo4IDaYr ztG}Fl0KpG|5o~%)i-W%E(X6e#(5ZQe{7EVDy9hbqaFQNW3U^ig@QaMajGj{wF3XG8 zzJRND4HLqcNeu8@k)$#cRi4ZG5qy?n=-kiiLghTvy;^-Eqj6k*JY(DnaN`;N)4^yU zraw=T-WzTSy_(48@SQ;j`}_uh*h0})94%VQbgD~Mzv5C;bLPqtXovGX&(n~B%{lN6Np5m8n628tKS1@NS|D& zL|*?UTQTB^%p&l~^bS8O_aMpjaXqFaK*-GQD-!Av78`wGy^Oree@%c0u`L>i;314I zW-;O-GtD98m+DPx@R3HhlkFK0F76_E1!(_%$}bQudTd(*vp|dWb_1{#jvqtsx86l& zfBN)j(D%IJ>qZkDB(a@Nh53Ce7YT}0o9RN1tW|sYkPC02FoemeeulZy)rriTd zp6E{(^XmXm8Cj9>pJr71dR(ogG)&9&%0b-3&0>U)WxQ~0=l$&Kc_+Y^UckBS*A{L7 zG3k)YvJgk3$}(V|ddhfGesBlmfgt5L_+je01FXce^gb%1b6Cn$$ARZWLqrSv4&4ts zN2(X|(h~FCvJfXgennIYivtWH1VHAT!!JvszWBg(V%`4I4ZONo-sC%98Ne7i<+B!z(Mdk0m{ z>y(FrX9#p&<#Au4J88%bLo6|!6xRn7sUp6W2WeEdgUSDQiGoa~c=@@LNWaa^ z0~xuL*vP+@7gNr>?3%CEFDZG}Y2RhzJK$T`NnLzMS5;6A-!K$sq8Vp-av%EUpfO<= z)9)ClUtVp0uFKk63m3Z{2G37F4$wq+Lgh?6rWDsPnK-@+os>3|p?aA~(9>#8Werig z7|!~14kTmVn$5MzXt;~ce=~LadFTQRpPW5xj)4(YW_8lR8G6efl!cE#dxn3ho`KG_ z_gFFIZC-mTcxMsVRt?T}fXm&ApgTIfGhvOkppHj3?I=``kD16#D2{2MW@5n-K77>q z((taWa0p(kdCqtHooAzch14dFC332ApM{881IE6= zg(v)%olK(dcvMebsD}FiKJ-9#{3ra2Ra@r%OYe9__++(g>ySg)?vMA)hi^;4gnX7_ zdQ^LKl1Z{dl1c>ZTSSdq&T6zEqBjAc`PLVznX-k06U4bco0a5J6;zT@z!g zPSFmfs=~s3mW5>E*L!-yG_$$_P@>Rdg4TL{TFoSS(UWp;BSz?1e8bV_(HhZjlU~yk zBiB@1cyl$NjK~y$u^)wz`_LECpL5c%Ad_$(>Ku<2gXwqvNc-kMS^n-Az>-Z%2sOHQekVsD{NHt! z6ZOj@IwVyunvXwgLmiPw3d7STBme%=0g9Kjw{`t9jCa1T<|E|NS@q8B+dQXHYOdmiy=af;3zSC@)9N=ur9P zaZnMebKnEgz_dy7nL>{AN=OaQ{-Xz+$M>^6$=L;&OZ7_n?;tW}qVX%Rha)U3tUfc-s?nM}j348WupM3t89xtFz(| zoI(&Q?;YHvz4AmC3f{o5Uf1vlTQ-SX9fbb}@U*_*KyXD^D#(cxAuy4NZbWHiFB}Sl z*UJB4g1~B2hBg;~0;RbDu~;<{ivp$%MQ7H(-lVYJ`tL?%iF<+~S)SW3YZMVlMd_kA zNlks4moy>11`ch=WFwS+Uqm7S+PEpnK}y3CT=PZydJgT5B@7+tG^$gPxAg5{=z7+G z?MRpEUyc4w%>uZ`KI{bnxS}8nElYbABa33^5Jy-qmwirz%wN}bV8%;o%2wcj(2~0Y zmdV$=ktOt49P6(uL_fZm9O~e{Sg{sNOn#wi5G-O37BhM%tF=8VMCmq%y`VcN|40u19o%20Q`mbd;$U5T+LtZ+51_m zI-DOMmc?}vN5LH4Z8K8*W?=fudY80PLX7PoYw}Q)lNPWd8SuK=4hv*mFScZdCSCKwP^%ld9Bf zZiiW*O-2-``iI>k#am|3xGaQP0N8d{zBuJnR&!;y=VEGv#nTXGz*L;F< zni)t%KpX4)uv_%J+_BW|z!wrxf zYAoA52TbMfi5eRAV=yuSTq((eo4x5&E!#7XPU&5_Z2}4q6~5<)cLz9`^h*+cWdUi8 zyd?b5a)kgqeoZ<-uB21P1-MpV&p7&icj(+it&()jsg$2 zCrlG+Da|lWe z`krivJ*1Vd+46)h(A7w40PE|tq#UkxxonShl7Z(+ks!?oX{AA-0d5hTDbPC&+^P2$ z%ZUFHC~n9Xy0WN-q05B=tamF2ILG=*jf_u!8YWLP$!!H0`4dheVXmV7wA20vMp)xz zyy;=w%dqcp%@)`<5c3pg80(M|Bw10aywRk8acut9}Y#hi`X1xl}*n>{yQ zxLnh704dGxw3`PoG>mGt(+rDsWnbXc-B-om(Qy8|m!WG9B7Q#=%(*GgW zEQ*En1J@61wmj-;k9RjMfF}kU^m?sBbYHbxoi{BGaMl@99)r+jUytSkHcD|pMYU^z zYhPa*j#B#exb9aWsVkXQMt?m{sAn>cYXx4!g?Pa;22|^{mc&Y4-(>`V$^n5fg+=rq z=|e^`+>?>@ES$s+b7Qx%Xu1yp$l+tMBapn|Eu7-xU45Ah#AqPd@SQsXE2zxOb(6I7 z(wlYumb%uD)ATH8Due~!vt(v6@ve=^`&auO?ICUS{O~6$iW||30Lmk)9^@3orp0`R5!SnEve)%^M;8n=P7RZ1 zMzTwG9Ol_qCF!OH9?V#gZx5{7Hu8V5bXHMqw9y(yin~K`hv4q+S^~k{DK5o}LyJ=+ zxCRUE?pm}+aVt{XwMd~S|2gL_St}QlN#>hx_TKOF44Tf!-a+pqC1pX1;(!swxGqEp z{vT5UiUHWV%5^Kl*C3@*l*8=J@o){@`1DibK2RR?fDUX>G1ba%M@D{e7_{UooT&eb z7j>i^ncRJ9dKv3U@LkO+Cn@J&EL&|fNk_ls8Oq&D2Nl?;s$b9thDsr+`aD}aFCM0j zzTrNE^)&Pj@F=i~P$ zwlBFXo1}k-DX3cbn~CKaW~>bu=`LWj55NV8gwf93lzZXx@2@a=!IvH4Cz@^;kEs)e z7$SQ|%nf;fQIg|y!Bj=2eYg|F^lR_iNlUjH#AcnWo|7bY^N0L;i`k%8AJN*~Q z&F-amZ(ZSc5NWvo3Nv^w@tBqt&3_?roSynAH8c6zq++@ST*@39l_L`(Fx?z6vxqRz za+mm0=EVnuxBP|`iD(%Uk;r=tyJIsq#bBR=NWi!S_yk)g@};DZZ0G`2{w_!YcGCm? zdE(Ukpof8dGU?9Dc*v^E|T;~`g%_&+TTooM@A$q`$bos+e3&B5(YJyJ^q`Hom(DTy8&f--9c(&`3l+F1aQG(;$9jrbQr@AxKy@Ta;C1csj6Mbl$;dpJj~pUvJ5Cx-R1ht{57l>LQ|8`mja&Dnfax@3Hcoo`6?e z8DtQ75Phl9{K{)YeM3Q1z{|&Tpkt@jmfx7KV6CN6X`eey8biQasTt2y8xwQ&9iOLN zgfvXp+>hDWm!#(8{Nf;2Py&G2%f%Nb8#7Tb^Z2_pAq|coC5oA-rc}q()XJZxQOoJ1 zBlCm1{ zzX%_n$d6Rcu_ciyTz??!Iq40378UI1~D<6w;GXi_b*wu&zzpJ=+CV?r~#(& z9X5~ey`z2lG^;&E{{2So&%9FIz5ryxCH(B3KHOc9lp%u{{DYr_lkcXtei}%t@ZeAO zim%*ZV``^?XRt7Yov89a#$@F|`X1`c%NSig_9bw2eM&-Xb-eb_b0M_)1!EO+Itbr- zHAkK?BdD8zUdN=2{7W!A1YrY)OWkXh(Uf0rcN)3>!UT-D91z?#S8`TNTl+hC@QIw9 zc>merHVO|-@hA4--Tc(K1E5D?B;}SlgAzZ-n1_A}Vy!Ld;Xy;-KOZ9#PnyL5cv$p? zFTlcMZX;1zU;rR-D+CJS8MhyPuAjb;gojYXG1o-23ZEO?ERH;-{fTNJX|r_O*i9pI z-}tlCr(wu9R!&R^6I8~Te5mfoytsmEAO9xYY4*XopQ_O{&@+FNGH}_3CZbD>8Ib86 zWZsVsu@tCmeQM>vKT4jNb{Vv8&}kXFF)Y^Es$5|Ww6__-1i7h=YQJ3s#$05$L>7qx z8{o;tIT15^=+ur?NCg#|XLt6iUZ{f1{jqW64(kRz8Zx2$x5Ebk0v`d4(kAyx)r<$S zAIk3eK7|oC>q2A$qZMO1$}|w1y+~$$ncc}Vz;4yqPG!uP9QLD3iTLlQ=i<5|mk zO7EC?u=opN20!1F{1^%T#38%vWIj8PfH+e;$k`JNH{9%ZpN}wSC`C54r>KPb=>x*3 zb=@EJ_#zuVnERyp8y|UUc|kRFGKT<6`mO)%OhOU600kJI(Q$N)#c7i>w|FpiGvJ&q zx>I7mbQN>&nTXY35sK4Zu+$y7c~Pl;SGv&8G|YD~wE7K-E8Gy{iA%dlUS?&(90+}^(c{OqdAvliHAYgzV52(tkzkvjjHZHYUwQ-s z9O71dw{S<6f2YZi#iBEI6ldoT z3V@ke(5&o5tbB~T6DV`VsJJ3~&B(X&pfJ8qxef)HPE=J<>S**vF>ijcmHF}{upvBf zG&@D$QWj??D-D~R0l=UC-f0?c``s`hKP+m%j_^}k$1fDsjF12B{rF`}ua_G`ZRT!N z;T4o2s-77yHPFz_d0Yh|>)@anAQ2SGX?zgmY)Sa?-}2@aXioTI@a=R`pL6U24LMs<|86$5C3vhSdT0EGge}#Vc zTjFE_D2F7`3^r|iM)pk&!9zLU<$&>+hH(dXU=l8~+_ubUSxOJG@9W&ZNw5+Xcg^Ir z-UW=dyhvviA|Y6%6Qw>v^F8}XI&{LXO7g#p4|+SK2LanZ&KEa6EBfHemUz2uz<@CC zY&#fp1~XF4W9z0`C{{kcmL@~%w_o?|JkBUN%wiFFw3Cdd-pqX`Mo%*lx!PuRQjm8P zFrcPQ#3HM|vooLr{T$yV0Qu5xvd9&^7VE@lchmDT^wrg*VJoal>aT{Ax$iy|>66Ui zyQxoSgGThmGsu|%4a>rcCO=YRc%TmNl@-)P7j-yYe$%?u)gO1 zU~rO|h4@`nTS`n&KwJ7sTiE;R7iNfOKQ7Ualz_w3(cd_QzMQVwpKI#4sEK}UITMjg{ck=f`^fgQ+M8)HKfR>xfRB%y7)S>0L>4`>k z+;auP7<$JKRea|V{J4}hf-P7M!+J&Ox;65jY+GKU;g53rqQuM}_+00wR;qxsNUXj+ z+dO~lap53hc>Y}>+^yp2b>__3=#y`5Hjjw9$1;-pVzw|*qQuziPA)&vFRSW|qLJFv zR$}%f0nyGC$UK)=ji47m^QC~r(bbr&t%O!1uJ;Z0dy@Q7`)Poy?_kb>KTTP5gmM&t z3OakaoxW_6-4oMy-3)s4M@VZ67~}cXhsz-ZRYgnT0cutudiS64z^`&3x{evPUq!>x zpc4gRl`CEmqMq7^I;GiNQq#HO-Du0|bbgAM2_}7+5#E;RDY7J$QYU)-ODx!~K*)VQ z)n~N6y)<^NUaq;k`tE&1R~91CSF~0ibM6bj`yrL5Tbz)(em zsICK5Lx5~AN8LU@m1OA;*YReZ$_qv(mF7HQ!C(uI%d{BS4|Z>}Gw)07ciUN$+4*y_ zM?Id&8}Dkvp)nqM<+&mAw&roaN$+x^TJ6mn^p0Sfe+M&(l{jvb^}2qm+S|bpX6rPri%VGX`OT?p@DeBqL9MffeV|5DsLKMU3_2I~gC5*MBu|34m8sFwyRo3X+%&M_|Y0yW5>=Wst+?4a{DB{cGsTjz&~EDa(eF*H5g@ z{$gGrhh|C=>hD5IOYt#K0Lsp>X8kOc)HB$-{H_J%^8$9At~W8 z;wxJ_Imf*N9b#j!`l`(l64&qPk(9WBiB^l+^C4p}i^_vhIF>s>gVwbq-V`#t)6yHv z)#pvvJ>nxY@A=tx*xm_yW4h!RI)U4`|nt9=wNm&K5f9D z;Oh}0qpW0+=qJh!Lan4}=nH6m6q(dfsUy#p0+nl-&Mf8h3-CRSZmB zo%){)Z3d?x*Y{-)r&dp~;m{RyYRuXyypdp@R(joISUp1u7z0)(UKy-eAi{8}kF+{h zsMy)EaYZo9fLh4P9j$BbO^C9oBQwg#qZ><(KWX{&r;#&FU=pj1Rbc+|?TSNuaZA6KK z$*J%F1PZXw0{Wen-KmyE;Rn78cIPjjK0UV(REyYNV7Z!cy#MDE#kJ=cuR8xIv!avY z$9%ywD!%5lH(4A2@}p@>>snpk95L|!<&~tm6WK;RtD~f+S^Ks`;aQ&Af1cO4UJ*A6 zqvBs&?CI3kJ(?1t-?k0#q5WC2NRtQ@SrRNu{iUNu-w#Az`ZPO$6yi*C9m;m&| zkS(ga4msvgHx>Uc1C%{|(W++mPKKRv3EF(5esYlT{gE1u-=|5r4o%_B1hDd^iYX2- zNV`e%H&BE-I(8W%m^85qMXcH#3FIcLwR%7dE@N7Kg4F}^Rez21w0d$tw8`77`Zj9g zgE(V4)^eeQ091B_PHhxB19JDVfI$a>Of|Gm9!^R@CtV+A`M$+N;lONx&s0S4ZIuef(;%aSZ=hyMgY4g)f%E|nPcZmfw^sB7n zt@j1RVXd`Ly^_TEPXN{XcGbNqgK;Wq<9925*oc2uD}M{wJ7%#PWkB8SIJ7c~cdBO> zzQ@GgXQu{ugio+Tcso!;4#ZoE(ca-5a&aCV;crAjACb-DpnkN|Wk2!??w?+1F`{GZ z`~hbQzP3&1nr^-;9)#SIwU4_(Aegdf_;> zGjc3D696~BeE)*9rK}c4jg!aVA#@RcBXYensxEHd3jG-)OkpB4JR?rr&vlGyU*ns_ zA9}zYPzu5!VAdklz#HOb44S2{Nj}1jaXtBxzPb9qvl5gZYsiA)ATBA1dhy-Ps_|hA z7UCy7Rrht{(7-49HHiCVpf1rc$6KOQ^v>z9W76x)4xGu6uyj$^xQ^qW?{WK)V}jtj zaZa|59eL3T_ITQTTFGbAMOEz}M!{nW6Bii=ijR5xV#rGV&+?eBYx2&5_8~UD>#04P zreA~a6-PJln#2LgCo6qXfl|kyDJZ+>e$}L??XUS;=vm}sG_IAT5;VrFO|(u{j{rRQ zu~iph<;3I~dp)V%-O0qk`~+jVlf()%{f;3?G0^rf>yK8j1BT&PH0 z*b^^}2t%Ho9?*aR{BuLz%VC@4_FYongtJRp#-k^UV(D{aaS6)}<;RFp#4H0P2jUvZ?U#CNHRi;y!o&y8|@8Ia9EOn4tvYFUdt$ z7~t1ikLKc-3tOUJX)(!3>HF$`ymb#b`mX?=(s78WxvN}K>?WFZ9VO1O<-odYw7*P`EzZQdAC=UdZ8} z_2FU!$ZbM*_I2my}A*cH0;gKwJhlw*h@YwW`^lM(A*{ccHaJmz8*mu7z{A$_n zgv9(%&p%!|wJq*WZ=^li{PY6hhecn3AA_M2XQ@dPbb-%l`iiG+@@+JUKSuRz?2*`2 z+oNj{_W0=o^1~xH{`!|m4=1k8m2ppy*R}_5y)xyuesO&{ldex6za4BFSEqPA)zHx; zh9^7%8k;gq#J@+V(V{5)aj>kX?1!ulUe`8#=&UgE??YUhRW+>Zf9-}tD z0?u6ih|^w`CF@dueA-bq>xbZDp|eIIgLatT*tK+c^y4K1%LaSibs#L^qQJFrX8(s_ z7*zj!a0v?r&u1ENOQs)3D_%qrjKaH(0twA9nrA#@TGbC+YbhRO8HK;!qTD=J_H=K# z5P+EshUAG?E3NpB2Zmp#ebh$&ggHFO|@jk*H2)||h%{|z&v5jc^1YWJs7 z&TZ@@bZ&aRH(%CH5l3bs?yXXq3BCgO!ypv75}$Va~|40%RZuXKl!xasqLlVJxQx?DqoC%(MA`NOs{ zWP1XuFnUvy=1KA#sktY489^MhS`j4ZudA*YiK$LpVH1d}{dqxB-v1;nv@0Pl^Ijtw zJ;;W3G!7$Ma1SQIh6RjK2oeTVy5Np*Ec^5bY$KQkw2n_Esc$#vjRUmh&Jc#A!6J z2cj{bEg#Tv@4A2P^t(GyM}WHRXof78_X||Wwu(iip|ih4zGbqTIcDOE%EKPo5_{;@ zMLw+d6y%}wqbpQ@qLzN{R2cUk!SMQ}Riki!S#B;^;3Cf|42JE?Wiy}8Wn!-s_3=$2 zAPQgl?A+6A>N!&W%Va_P#;TkNJa|N6tDTD<&*tNVM8(!!)WE)m2FxLnuDU}Fm3!hK z|LPUhzB5k$nVgEqDJ4lTmYw+U`ca`CiLLS1>3)M*Gxbhh3o=9VeL! zlVaNARy@~0Zt9#;;w##chg!rnE8C6#l6J~piRr<@!d;<&hByhCeeH7Mlh?YHT+B2yj^D4`dDVHV0@VwD9s_c-!EKJBd!YdtOkpD5 z5^y_UVX|EM5vdGD!>pZIT`Si9sadd*d6ry)d5!?QHs1V(Gj@`1{grcAu+RP2bP?9S zx6_d@Nk=+v!@S>)u_;L|q!EZpN}|G!_1j(kjuJiRpM0@^O(15>pX9nU5iq!x7~8j4;s?b&(Yqk9Jq-Iye* zK=5C|W=~!A!iKP+9C4{_#|_Cg7#z%wPc?RTL%~@jXP5oVe+YM1?<);g(caerFdGX( zKSgd~@Tp<^fw}w6p3#I1=;v_-SBfkZ?U~hiak5D7~++ zi(e`+i-yVjxBR{oyx7nVza;%$*E)e+Iv$aeu6o`-FLZ6(&g3caNOqsq4A(a=6j&;C@yToDAy5M0>>!mL$$ zTHa0HWCMQ|4{|H2#`sWmsL}=&4~40n2PpD$nGK@;uGFk%4&y-k^P^QGRs>iv5}agK z%lTl6P0c@Nj*_p0)OB!gUD#ic(a6fg{yYJ1mYj(@{;uiB`dri1IeK8XG_Gwdvgzv^Ciau5`x205i+mONb-e>a)jmB z5q5xTTeE`}No)8(2Cj;P$S8Ri(uRGq+-sk)vfrDPp19K|pH^KHo3=ctOgu)2fT)cJ z4P;6|*xu*KN>r;z5d1Kb9IR-=wV&u&0!(9qc3y1=eSb;MhyNMjyw|&e{B>nm#3VUg zO_6g>t2cZ|tlfBi`?nH^!y;D6XZmT$__F3@smrbB<)AjpF#qt{@9f$_ycc=Y+Bp?d z7?WGD6@}iJ7oP^wPZKX=!KRcVJ!nJknG>5JMwPlgvnqCa*1TxQWi;ExdUWV7~b4 z_4Kl*>Y*Z4bnI7~<44N8TAdcBW$b@{l!%TH-Rd3sj@f>4Z1nPW8?tj%)J;)aFXE>- zqgYW!^!|#SbKov3X(l6?o98iOlcG?2rS*2XCXk&pK=lYKn4V+yEOD8R#)W9Q1wvxtk#i6@qIep5jL3AV&EXaG1=}N^M--EhCS|MJ&Kq3_ef~&ye z@(i4zDP&iEcz?BmXN}F|p{9!!`Lo8PSu)fHAFOQ|^mcZtp(a+udD5W2Yz<5mA3|p zl@rZJ3(DlYcV*9GE37UzTJZCEGEW}kj2TDB#WC?-2Td2qJ8*H7@`~mAq_RgScS(Gq z*EIgJ^p%<*erWMRIHDx~HySl+);=Vq5yH_8;DC`1-)caGNR&}l&Pv!>>2}6Z9IF2_ z$KQ$SZaQo@!@6oCS^Q9!sgF2Jcxw^f{lt|auszK6!i{5NbZiX>kvZYUM@g(pfGbt1 zOQFV01Q8H296 zK$V3V=z*?zx*+RV9BNffObnJ?hWj){<`1pp_5_pP5IT;C7^F2h54dCLmj(i6J*(e# zL;-rGv`!{V?fA=yPPH9n4vyJI7$`q9k{wV*;+!XSUWgmJo-Iih`*ky*E9@?BKk(j0 zvqmc9OTQ5&H?RAj;KMhtPPVWnX~k*E0mOUWtCBu#W;Xr014oszyiEK}IRs;<=#{!y zr|H0RaDhk_V$cb51#_%ct&y9In^H4`x^4-;vb(77{!yQQR*C|BT64rq5rvKgdRQG^ zX{{(Bh>iSls-jhC(cXbo@XX%zPxNay5^rh8pfBCt;1lTQLE&Z_ z#Z-Avw0a9O0_wM|0YEh8@D*LlKP;U;zviNDP+Nrl~$yk%e_*vCAnM(*+~`T1IrVxL5%P z`Ul?|$bnRX+(^am4rEj_ehb?}Up|-hK#PLOz z(H=}X2OrnZp+8i^cIQ5lnqH!ZN&|+DS@h>)*&`;rb7w*Sq66%yz|`ayP=n@)c$JyW z?#z@vkiWePGny5&!kdZQ)coMrgfc;cLpRCCj(3gCb$_ zQN4^Pvly!A3kzW+ep}UF=TDD}O2Fza&b55Jlk@-tmK1@wY*T)2*6kvDbJdex3z8M~ zebi3WJ&Fn|_ZD>e_(gUtpWICAyrIVdYtc_guc5YBLyDh~qM!}|yElTiv;a>$TyS+7 zm0?c}U~oYcfW7S)=M3*-wgTlw6va%c+d7!nOYj7O4)PzkFDLrVb%7XsLf>x@?BNX6NkNml~u z!v{7I^;C7^Z1$DUqTHW}9Z8O~ExCu~WEA2ClA=1x^$vLTzBHn!oRAK&DgJHh)6vI! ze}pkJ-=ir1>m^2&Oef$HRSJNZ&54_hEI@ESD9~_(0l36^7-2r2#<4Q(V+UUUOe&Kd zqC%)OmuN}38^Z@Se|LP|8X{Sq|FA+?H6Ne-P=Bel?8FSz(@|3`Juxrw*);A7o>bj5 zrg?tv$n!VgQ2FRhuL3OaA^!tDSdx$WW`?Zq)AbgA3k^x-vB-y(vRsLDo$jiM9M$RQ z?Auqnp14qnwxv^}w;fOmuD9(O+s(3d9Z)wYhZ1 zYXmy5Jcaak^;1*hZI8u*>Pk|GiTi59L#}zLIL^>B12pr4L+Mj9+4A0-x8M@ImbSbk z0DDw(bmsp8W@a-2ULik>N+lKcTPqy1c+@2NZZuK5RPB0xuUPpt;a(r4tw#`deoGLAZ!Xh-N=_M2u zvFyeGnkcBb?T>L?9dAWlvj7z_XIQ4RwBK=@RHRNT|Dr<38D%lRR-*KnEx>4B^{e#a zac-X0CM|rMs0Km7zMu+Z9ucv2W<0J3C3*@^A1LpkLd6H~NcBg=zbYkeS<~`o(@-xc zx$30;IA$^tm~2ZTUutw-XC;;P+IVwm-($asPFl!cN~_*zTmJc6_aw=z-F$n15>( zBbq7YGdPpd{U?W*O8{AMEsDaq*}bCYgPh}cJ+-~noXxF(ZkM1z=6VLJ)WetsO2P`( z2PRAKz#S1si0$po792yiw4>%@+PV$>%D<&r({;6cmW_BSlHtIa#C0(x3)pGWT$!h+ zfc2SiLd`}jswDd>p+^dy-2NKMlRue%&%GxrbrQ|PGr}?ouU67jeHB-+oAlc<`gYax z5q!ET&L|;3s1}5z>ygi1T=$%=OzdONftFoHcFgjeYqD?{qq>S_@pZ~o&hM?#VbkB#M`P%vx7?3;TyF}P2w zFta#yo?O?uWr6{gJNRKr29_>q4!@5rt8(%7fJLyxT28bK!#8DdBbPE#Kktv#0#m*u z%10fQh8FXw5*FE^PVCazik*xSW}YO|M0*~i19b{3F^L`7m)+%*EYZuaSY&XAw>oK3 zVO4Qhx&aizSOhGUy(3~~38x7d@@#06d>?}@Qvr)@)H1`yx&y~myM$``833@?@=sSf zh{>f3*=KVMnz-^yjrAF1)+!*_Aa(QwZe7g8OFCf`O>`d%Y{uZfPgC!OF+{$T`Tuvc zN$%>lA#Gu%8n}^tlE&qAJ&GCE`hf#+#}$ix@d#DV1g?B7fia)e|1U6t5AM-QEa6|J z-^}S-|C2}}U-o|ijRt)@aIUN)6)U&9=2AhdnO-(?xi0?wif^gGq@t&f{`nV~I?4hY zcGv;@cl2n$>!21HQe!{DX}=iu~E|s_Qt& zPLg`d)T95uB$%WMsG3yB8}<~CB-Lvk{-5K=63y-DX8%>x17=&^@6gxL`3akaWtSRA zxaj$_CEizAS0@FtvHaH8HxKfDe7=OK0%1|_phaE%JntpLZ+ahKD1+M{Vr~miV3NcG z>HF?K3FW#~dKx)`E`J}dVF96mV_1+J7BbI+jWX+-`R~J~jt`g3LxK)}q{Ls&n)cN{k9pPMNIpu(;Bxz2S(snk1^I3H?P0y5Jt{7Z98Tw;XwGv* z&lM}xne~X?uf9-hh5QTprt*)>1qX+edwT$ddZRZe@KKuv@Lbg^3p(oxgelXs&Hoh2 zM6eg&K@f6bDM^!<|DDXI2@nGU*^OJLl5YzuasJud{2mXunE<-OL4_Zg~(qMCF?#-XDwb%Ka_fQp1xzO~of z&%qByao4jEd_7#xw?9AR$d{sS^X#Lx|A(!3WUlLduFMw?d^m%&k&1YhloG&A(I|u< z4aMbRCQ%CeK6##8?F_jz&4}MlNxdF_fc?K7|C8pyT$lqT+|Dc9O4T7Sxo0Iy!lKpW z3EXQ0k zy#u={*g*8{-Izs{XSstGwi)KZ9WC#%C`IpJ*Tu-Gmr=AnVk!be@6KPw^uLhh2|kfG zLUpZ$k@kK5@y~L$mnbol?Z0rqicZ2zouEnEPS7hFwCggy|Upbsj=POGDW?Y|F{ zH;0&Kv~)wX-t&o+Vz{4bAvKpSU;tD24vjjaI};rWs8HSrK-8|#K9!xYL_0k?@Didh z6_j7$gg9H3k5XNxYil6_jGKi8AX$iuAVMRjKigso=-Wr6$_Gh_bVKi>c}MYy3~~_= z9CX8vDX}~%aScH^#x*CUo5gU&omH_-l1cISxWCj|ezp40&9K#Kts0t=HN-2&=aR6m zt^J*Y`K5j_O6)6)nqo!o{Ga z6K^Em$b?85KDHWtu842>N`zpF@KcdPkR@)%oGeo5r|~`}Qt&=l|87aM-kV5&2t?#p zabT)M?+GhkI7}ud2>o;H@;=pD>Ebs)x>u}J0|mGPv2ls0Vf#5DHb}rsc0Qx=LT9s6 z-6HnFQR4o{-v!s)v_l{5N(}*blCPKJIc9hriNiZ(1j@Mr1k$eR6cj}MF?ZtpB4hY)b%{9UDe_KrcYx1 z$7$Bx-d%z1728WcTxug>x_w&Q)%xj4~BOcL4z(6<>OdgE#`S1tKywa_dxjFlf z2J>o|?U|i%bJA$XIlTS9OU@j-->n`KqOCloMM^cM&TTV>+x?#(c-c=|{1PUa3}1O2 z9UZ-z@%AhH@H@eIqa9C$(jAHQ!E+~y#O{^2$fWQh^V3*O{287wHIXOxRDRHd;naky zweT~ltn1=g7E0#k*hQgYcNikdvrBC*d9iGw8NA9CAY>}HpMZAV68I5L;){xq&zV6r z&_L~)>U#c~Y7TTLQPwC=S>sE?EjvQQ%#kI#t)I5=E9C2(Gt`q?&OGqErJf{|?{8?}MMpV<2gOdv!=Wus-vSY< zx0yBKq7|HGc5rTA@N0msfYsbscFlYkY`QwDlh4APSi`KqzIUAwBUI^6yT{(__{xA9 zM3fg5?`xJQs#okbs&-Ttyx!%1d7LBu3XU(Q7D%nOE$FN8Aip9768Kl~kMkwsSVLm6 z=+wijR+Q2tMY>0yg~e!yPO=4@VqHTRIkZUiA4FMN%R+)+yckD~j}zY#-|ed?4V$9_ z$#|?HF$t@&v-yu zq)={s3;}}*ow*JYJ@sm^4<74Bz?viq+CrSenBQ9zvT;Uj&5G;<6ApFoW0CyrI(RKu zQCWSdF(;tzihfL$W7#(vz4wlQf1L^bDJFK&oKE8XmYZX(y#QLc&;bKr-l&{(|LL%x z;CY+-PlYhupT?w^xI#H?FNK9PXyhr$lk`#AD@Vz@zP1-mAUy2RGJ}M@2scK4E;U<- zf|ax1a_}@o4)kq>`=8tZ((*_5OeoD^m&V+V6c_rzbE*+oMmZYNr_ec4-6buhn+9D; zF*I0jvT?Y(lEaa(30(Ow=8AO>D?bsxo1_YQ0q50N$W+o)ApupQ4vsQNe$Obm3%cNJ z$y$6m4n8N?<_!%_XJqsK9V#h#CzeJ{5e8u2RyL9^-tjw+pzBz z@Yw{=<1kM#Kb_~m$~;ZsX+lH|0m!RMQp+e}HI%+OiCmpTf%iYC2&1ZQK4 z3!YkfOA3-ZEM?E=cvR;gy~ zL)0HKUvm0CN6twrc|SUG+ZovBcMiIgAZd!HIvXbk#)Z9_%2H zJ;gMXU?14nr-6hm+`uzldm1Q~FbbXm_BBImi&m^bKW1YNx$fr0!G48KDw`z=zHABK zPxj=^7dY9>srHx_1O>(18Fl8Ega{aeypt8EoVi4n-0vP>dQeP3408K^giX8|vxRuH1v8azcPPW@rs^B8svBd;id2&#WZ<7iwkURz-*7D`DK@ zYH8#sPQlQHhLRyflC&ApldHJ?-b7}Ek6(>t(|Zk>kec&XfQ6UCOK~DdICNbiGJTvE z_AOpYOG>jZvT#_=Zk1y7JFpcVoZkX16r7tJDKrhE%{bvl5Rr?MjG3fZXfr6DW%dK( zM&-$ByZ;?)OnQ<>($w&eMz@O;_*KEgk`$+QQnJ)nrs;V7_}!@7ez1m7_mX(da-Ld2cZV9h7z&RaF`g%4# zQM2lWgTgupgV`=pl5m0S?MnJ`;9k0iS@q^nT_AatX{U$AoMlv`8FYl(O)^QgU0c0* z`VNrBkLod9orlEf@{o9S=?8NXa|6;jO=ERGoh(#5h>|)RS{^;>vl+(8RvtNi6Ckr( zZ)Qj0Lk+fikytgJ+; z#sFXdWXTCsmV)fu;Rmy89&q z|DxR1V_(`uq`S1ml=B;P1%p6u%4M8=4F&R)opwI3jK(Alz`&h0qlz5A#N`(Tg?6@p z5)Ci><)%4Sl`M!|JK|V}T9BRC;$PD#6R+V*I0n@_Ip9o#jNtp>bAl6!KuB!K@zkcMuzss|liDcTdz$epfw$tG(?|Oh>AYlABy)jA zwgStn-M#2BFxqv+JaVYk+~V?toSt9uzQ2e&}W^_R(6kjn^)TMn=q7$C6gxMZ>chpAT!nhlKRFQQGNI6Mf^hv zQ|JK2K>w23IL#+cq`b7IrZJjNs7d#UqxPE^cl>8cvr0!IqzzH=m^gl z@ChkD%!kiQ@co2^EuxyUl;aXhulK&Aq%SRKw#t_-%{_>e(tm%NR#j5BRENyl5{+u7 zlvaK>2veoEXZ6K$kio5?-`Y`Jx?NE{=~TBCxyleR_ApsWWA|6!kM5)k_1H0VFNV;9 zZ9l|7YRy|u&v-#pK=>{-jS3SpEP}qOrm={!!0?dzO|?eOXnih>Z`~MY*-w{X1yI`F zlXmt8P<2inmrLXbLiF4FZ-8QnNir2*u`&M&|54`^woxAguNbzYCsO_1PgT?X57p#S zRJ3X~)faZwWRoe3YJ<09Nr})6ND8B@?lMGM1lT~wZbb@e+QSIvmZ_RUl=~4dRgO=j zdt{)OD*C5kcXg5J?Aex$-4bHU!6aINl(}H|iWt?I|K;0JD8klLYRjO^VqS~ux)63; z7{P#5AnA!COo+iJ{`V7Rq1<3a?lm(h`sd}mHA7Hg9?;0?moqU?rr_u!X{3+$Aoouo zN`wnzQI3IKLp7_~ocxm{O*Y#{d8wC5>JWRCgg1aFst0errWyKyn%=WgRl-DLYZ3ej zTzE6Ya9$P&LC{y!SU?M;G_%z!h@cg76U@D>k%gov(N)^`5t@Rk*eLM=`xF#D^E7oE zHCP^HM(i+he|CAS=JK&HOeT8gL~Nq>34cu)u@5#&?5<#G5cR~1xvFptGc$K=UT(XAA4V<5$*b9n-NYSSfWl7a zbwnBhF*qeUc9D9>Z~vC9yv+q&AWOiI2Vh9d9T?=7c6NzQNX~4zu3i{iS^mUV8p_w{vscn6NcVo`76It z^I7)GJ~{hzX8|p1tN<2R3ABLn!S?#($6@^fDwieLPg}!S)0!LZ?s{aPKVJs`oKr)S zn{0%ReN2Zw!iot<)Rah>=C;a|v5jfbnn@pRxSM?y%-J}x_f7h-dQRk#=r=n3r8P)# z5f&Jt9Kx<#jpn(P&(n=?!R{fINwHkn{Tt=$92?hhTO^&B;7;xHai3~gmSsaY06rCorV(tIcvAA1|4?m@A=?%Cdov93h{$t*koL;lg?7To~kEC%MYmW;Li zu&ay|y~I*n@$l^EE-m$_==_W2&WbsA!(&J0sBgDgCNt#2SE+F8{_Buvw@)Q93ZzVc zlIQ)t8q<0Pa0SGHFTA?}$_Ydh>-GZ>jLq`jM zm7Cs-TRYT>xi%LH<((3G4~l9iQB4T2)%f@g04%*wZ^f65g7qw_%_V`!QR~MP%Qum9w zg+lnT`dZQ#T26|Z6bYUWo$lWVf{?B<2VWN7!D+oG=0F68FbESAExayJM_TH%ZWwKW zs0g4q{j8pZ#&##zIZk|m} zyed^y!YpG3s{CTDFHYsYfA}V+U@sN!%q8m3}W7E`>I^%3jeg>i$elCU?o4!&i39s%s8J|EUbMMwiRG^gOzA}}6Q z{2F%o zq*PUU6=~UDdo09@8Nzf!p~9(a*Qzt`BVmIx0?!)(g&&2(+Bx?^MhQR9&EkCw>) z)I$Wu_}I_+ton!M@O=0f?wVAD2bTxTbzLkomip`1fhtJD48rx&W`oKYhcI7}XbQ?Q zI}+@dG6EgJDMA|UPfq4B!)W?zbQK36S=mX--mw)soY9qOy1d@D4VGzo&LeApQ@r2c z1v1iaaAylq6J+bO49wF}%xY3^r!Wf;KT)Z_C{by$&r(I7%)a~xT3K7Zw=Oe4uOqH& z<;o$m^`@Fe7^T;7Ug}Yw;CRCA1syoxgT%-^uhN)>02|OYEv7Li>T@x+Jt~@$4 zg!bRZ7%AFlA0(e_h=K6GmPWWkN>XEW+LHzLOF2hc`^8HfyJ`G}k7usUBCojaHWN7S zh@gTp;USI6vVL?cc}UiaDm!4LoS21GL-w=$QygUBJ`vxBe^Iu)le9Gs+(8y0!&{MT z*4vhjp|aLf)-_jbqoGj3ASCcGG!mnJ+g#aX_cwpd&2IC>=~CRR6q7XNj`87lg# z(|YZg1Mz)!@H7{iZGL_+=Y230X;zP@X$xWUtL{my$(PJT1uWSjjqj0AaHE91b~&B+ zgnR{iwR+USL^-5{IlUK49{oW7P}oT;RNR4+eq%FT7_*!3^XzkeFBduo{lzjoGcrN6Pk0HTj6rGIP9{Tt}KZzC894nf*WO1 zjG)|CUJTZcyMz$H7%7M72AE7iH6 z&ex_RL%qt7Y;0>SLXsbC$n8G{L)%!jlCr7t;QLAZ=NeT|G8=P?UQvlcTE)XY|5D#5 z5aaTy8A$nceocssea~z~wE4^pfP~yBfuAOyLHa5ltGFq2opNYC9?>##EKNw{(;BG$ zLOmONI1NW-(gd+bub0}!PaYtii8u|4bIGzy7Pa*Kj5IBd%x`$r?Ej)5qTk01tM5H| zcD(+39`2?J8`J#8Wg3;>^?m;dav32$QOL;hgy>>Xkv_axu$sggs@hk0QXwUX!FFxu zDVf11-!(XK&@$Oa)e{Kyr(mK2huWF3F!<6*WtvTWBB6blmwXa$gud2pXx>*cFDWBq)_4I6RIvk~O37wq zr(*IetlX=+!Te9yBnz_JI^_H8GJ{3Bj+8q?FKPbhdH$@)vTL2_P;UQ&0u2-{LNo~}rp3GX_NA0E_cPdga!o*-(onkQx9?S-Be_E;)N z()wGyW>(<}C#nS5x_z)4sVh=kL7Hl&B+_bK+~f4dGgE)B5t)t8l#E}bNl)_mk-kbv zizd8QeL>&*lM^sHru*BCRNgla9MYTnaBlR zjIsw8Tc2i68LsVz+AGaHYJ&|18?F(Jd_!Hv70ObCsxM~PU6CyQIT6u-SLo|`#|wi3 ztm8`Rjl3*p@T8mP?NpJYe;PfQ3fw#KX+|7JxE~uKH<5W|jB(dEQzKGkq6Z)~KM0K! z*S(ffoj`eQO(#Zb;rl#$grOJualD`viRcb$trws=%FqzT6X3;qLH(ZcReEazN4rYa z;OP(TV=6*zB{(_KEk%=s{zIbMS-1M>V3?V`i=q`Bl0=F^QjH-c<0N|S^gs*uP&E(b z1umyJaq;A1`3jTFeNJhE_D;yb>mW!?Q!H$zfOJL7EM4CFqo)ai(c~kDf`q!6zfWAP zi2e>FsgK#=qN`BRx5$T&*v`eC|H^u36ZDRnSw5c9yyjw4r64{>ahkmALk~U@ zfiRWRwIS}viGhCkBIz)+%`Q9`orCfdgP1)Y;f`o(K^^Jp9_PohQ^~H^OxP0`j7iB< zQ6l`JU-l^nWJ@>=kkf3U^d_k3H&lz|gYOGdel5L`r)E^|Yh&^mQxEOQi5OTEg`mqa zC~x#8w(%u!I`Rpl1FOqlrkxXAwz4 zy*e|Z)K|B^DkaPmHnAHXrHY`~7OX#t_M2bCY19It4(yuJ9c^PJ!%6;2>l0H-s6E$* z9qcgeeCryOuoej4d=38B3@mzAbWjgQKo?JS1=}-y;U;2VjP`_z<~govm?niIqSQS3 zv5R6d98^3kbnc41*g$5#8q|3uJXVH&;LZHH4DVX4SA{UPW>t?!fAYx!)5)@QTMg7X zR4V!wN1(Wfq@%PemP?)P7~vz4`2PL~p)3S`#`rvN57r|dzGtA4XL@M=7zSj>Ev8RI zD1p?b%?Oh|4T{M@jCmP4`|QQXku~SDCXgA_ren2_M^}22_>iMxD+Uc`NBs+I_45yp z9Tqtd<-Wb$p>A>0l-&ub&VFG^(I>Eat6lfA?Y!giR}O*fZeZM2&ibrY3a1tyis6(b z`+8N?M++~O7yi|1hMhWKC{!iBvLgpNuhWvUu;i^(^T=Qo6315HTyn$EBMXs%j2Tv1 zBRV1vp#~CXS-4d#`-$|a$p-%Pzj(WaGjTzR7ot|BsW0V?UpI9qc| z;bp;##pPf8-;XEepdTw;L$ykyh(sBlsv9{c@i|(qF?@^fKDx%i)&H3pi_+T?W(k>J zRr5ff+F9qJtMb@xssmYzK)!>7xl%jLl%6h85x&fnW*o!oL)i*~FcUSS`waAIEC`T~ z%8@_%Ob{}aU4yD@A&{n)Lw8zL^88Ckz2Ejtl)vM>p8(Q2UvgM)J})G&HHqs!`?9f0 zwdWZJ=q7U#uv!b$gE(GZ*}oP~gw4ce_D;Q9F`RC_e`#gg01f5!1S1BSy29zO=h7kJi&m8>@m7p<k6F|b*W`Nr-+O{G?iAuTD%Psc^N$fSUyV~d$3LV>Vj$ReLN4&JWe7|SBx zyY+H)KaX3P61gH> zwkSPJTjpf2@2;SDna;HikMP3VSK)}b1DN(m&&38&-+v$=e4xtMvu6w#wvSNTOY`TZ zSymJH#qbR8HDlt0+{^v!p$a;S#|>LJb49~%VPQe_bd0xoMH{$BJeEmDt$ThF!dQ^= zF&_9GotU2q9kG)?QP~K_Dx1|2duN36`N5-#?ENayl^jA>1j;DrYD1phQr+$Kl|Gr>I?EwMq;<=p^ zLrE4naWxHNQpLdMAL>|V4FrYOgyZ-E#E{L+C5`4^jj=y`rOT4;S~VHjl9&qX*)z;P zq0mLa<)^)9-qYCqY~feT0>ExCCMzNkE#VGN`p$=L+MFI7+nd2k?eEr&y-`>C9yKRh zpO13NR-mUYsHiL~GATgV^Cw@ywzF$|30_xY;`TCf1h0!LwKnM=gV{bJyc3XDlykD z)cw)vS~n3VHeCA)c1%~vAasvI(-D`yIa*G3D2o3I$J!JX2ff*C_?Hf=-&k~QQ&Y$d`9$-ASS?mo z8|($tsza&AsmOtfU0ONg4N|4r%H*u^)MKlAs(lW10765ab z+rd*?*M3-fw#1zr1j``d^V`Y9t#wep*971TDS~SoJMXESW~;$enq>x*8+ z2;J&z%FyX?Q)NG#6tf3ldZGK z6PyWZ$LL#rXuMjDsw|8{Gfb1vkEzE9AfzNLruT8FG()r0#g6M&cq~9jgkj{i;VTQh z{7}0vl{R8D4*O z;in9vuf1{|xbF+^R8N4ihW-3Yk>Fl@yx2+FH#y5U8N}a(&{Ch72!i04Su^044ZJMO zjV&1pJgZc)gy5iOlN$Qgs;61Y5UI_T}U1McV3ay<_PYMtgd zzJ|VKn*!gy7X9uZt%{<{jJF2k8aPysj;Ua_+9(XYAbf5h1MzxAhvBgP-B~c6ukPuoe%oC)wS`Wz4-#){rs~#)7_md4x z)^<;|vL%`kO0}sf9QAroY3y8Y=H5a#Vk!S<&P0-6y zOg5swPvefT%~_90oyqgGAx6$<6%}JK_;SYYHbpU_XT!f_IeY=jg0d8;zas2ma?Xig zK*C0qGtkg$bQ5S(4MAW3LK4cy!uSQ_#S5ikSx1J=h^3}FjuHw3%5DDeSXP1Wbj-iz zuRiMT9_$YRx=@ufKDq01>sO_cytHs|FOUHrGNl!8T+#Y9y0#tN*_p8KoqDRt`0F0z z$Lns9Ta7nzK#Z6R&s20Iv567TJ6M}oO$EAs{R$5QN3l;1xc)ZiSZiUo(nbD?Gi^lGm^Tp9>uB}9OR+8(B!dnNxe>`anGiJ~sQ(evdP znXN7rU|HE2;9sA!NB zWp4nfLDY|i;c?T@c3(418$hzPJOvL|G+p+tlf?@?zfEVXL8USd}g zxMm*HIzr!{tCTGUpXQtWlD2yoV}v7A&kec@t<2$54x)Q;s+tiI>P9ag8yebj$wE2k zbL6m!aF@b1XvX!-3XMOKQaHYRmKSU47PTgy8>Uc!cc<*X02u;6I1JhzL{s7-W!2Kb zp`2X(Q0n0bkS*z>VNjH(!nv#gLo1VfIJlf%Q(r4NP3BHNIP^CpBvhO4+`?G}MbBSh zcS7(>aHWQgGN*uLV*m@|qe=*?yAeSM4b`*YkVkQXU~oChbb)sS_O~>ec4cxh zqK^p3n6QII7FtcH@;}l`n;27uY5f1TaKCg}!B`SkzM-Ij~$4`j8 z7;*P4)QtF^gCeHz{UI~9I>*fO=QDS^VDxhkH=HP=L0_vpLmW&dSi6ok2OdNxvL=pj z>VKy$&);SNR3d|pA=NNSrWrrwJBn8gy||hYt6T>cQl21?sPdTJ_tqE5!1pl|-%oH6 zaFZw)fFZmBCD)u1G{IfpNfTn+1Oy0Ij7Xb(51Y*b6*?KaLccm1j{CA)-=7V47yeoc z8$wQIihqXtyRxbUJ;W-$y2z}{Wx}HDv%%c_H8H%2titmTY`?ukYe&+@?62iA8>+nP zD><5;x%_h8MoprQFp`djAg=gEawnBdd}|+h{J1V~D=81Aga)9LO`Re{+2vRjrjkah zgvkA(W$H(F!mp-4R?8k zxQi9~8azl^c%1v5{EhRwEfrU0#yMnfl@IYR5+`6upPWP!eYE7mwIL}*`dAs7o`05Vm=sdihFcEBC_A$;-nNX_M13kv^Iyp>rW6?t63F5@-jK72ljs#U*} zcZbWEc^C`+N2_M6mvbrp07h-bo_8y5L zU6-+EDl=T=rJ~y6^jR>BQzdPb-^V!?RnIrrIMp3JeA!#;siW}>?3P>QZt6dEiB;?`mDZ7VN(5Ctv5OEwJudXmrnYmyQoVhA;zY&QK2NGd&pe2A(4#QPsQuU48n=2 zDC)ZrC^nNiJ*6EIS1AO)=K5U-cs*xTONks%VuZ{Bi!Qf~6*Of+^LHzK-nLAX6kfb3 zUcZe4iKyF4>)^}sW=gs8!wLJmiQjc$#ALF;xt;A2<@$wV`>F~>6?@M${<%R2dKz>B zsjo}%*C17)T1B=5Hr>I=Z0Yl23&mLS?-!~zgCIRxMy}tBj3j#1dbUjFe)@%VwS)I5 z>t%wg1Vo&cwWEOoIu8lAhv%$*P6<~4we!5ia}$XCHo4yDJa=)EIsV|gPB*`J+V{!I zWCMMPazc_U^?6I7rXCDV{V;It_w4k(zIMGZlIV*@El|!jwkK32ElciTJ^a3}VUfMf z9igAvEfGL2lusi(vR9Z;7ar*`1ph%u;BqGlPY$wCO078nu9$!D4CTb|xJX`a(HUVt zuDS{p5#dSVg>FlzFDQT#Ooi&(<1Zo3xua;|rNTi3KmyWX#+|TrDn`<5C8+p6a0p}Z zcYZW#--}~FUg9{2U`Z(Sv8oD&^AF16XNH<8*ekIK05E_FWU#n7;R?!&Kh%~?q|=O2 zGnVubM%}uy$)HL6^X&=5@mxjxPkTH|9^+p6RZ&=fj*_(PrIYys%Lw&jXaQadDg=?o zEbtKlTn7&5woA(uR}{D^Y7uaL$|cS}@BT$g{8#gzEBX8Gf0}=h9RIxgUri`*2f$hX z{QQs2KWF{d7yi@yPp5xx`v2bPfA#-gO=wKu)Rg+e@3AqJMalIbll$L`O^tV(Kh0-d zPP`M{7#=G2v6zo$3$Jx1AJj&@Y*`6~MV>sF$dvw`AH+t@NGE&W1Q)@sU0{`@-&`8y z1)=8Dcx1he>LE8PIQJ9!3i1DkU}Tjo0QLtO@qqV-67j))EyZDo!EGZjFz^pTW3?j$ zNa7#O&r5Ry3gQ2$yPG8)>2-h7Jze}Ep(OwyP;?Ez#NSa*Kd%~B!dN=_ib6huk!IIL zVmVcGGVBTzso(dsKFYaVe>W^>j-}({wRayW}Mh$tR!wijDey-^s~|_W)pH>~4t^JPr14`|&A_A)$UV$+9g$yTSx&UU%AC|zQr z8GxA7+AO8z1w^^@nvMRf}aq}koZ>SEK4zU@_zojIqhew6)H3C z#S4OcM+bh-b^bn?jzwh%e^>=b^T1aY9BrvfQU!9v_zhZppi{`dUFNFHT^H@WpsSjY zY}sp#VD!0XxBgG2TJ~uGe^6d@7%|&rNa*Fv37SeeYuJA-1*A{MXglb}7yJiC1Mp5V z^`X30OfnAW{{!Q}xmxl#8wAQZZ*>yuH}2#nEAj8pKI{R8PJ`>&rY(lP+YX%&#i4S( zb^kcKtBOpQmiWO`@dp%4((Eg7RfJ+Upwv5ax~w%}%I`iJMqePr*HQ7h&iw(P{|e-J z^-b_>YuNh_2VC>@$N9;BsG5R9?SZ(i4v;gdb#JlO75;}Wk_bFoWv*)B@=Jb1ILy1P z(e><~)efxm$JJnTP!lw|4?wa2aqL~R4C(@cpeRhQ)pP`=P$zWp3AyXsccugHC+~JA z3snP*I2Ar8O|qvD5m~GhWGe@GmjaX%sRAy@+DP_LqGvLT`CCOgKRHCeb_)2x$3KtT zFfO)^E8!#Uub<65|Ei~m^M~d`?QfG$sI89CF1>s0#LT z@kw8cw;ldPeB=2I2Acu0V6;Iu%)({X^W5Q8fg|?-)q#m_UUTzfj65qC_cEMk5%u>u zz@jz&F13t^Q1+MeIwE2hFLt6_@4bUT;b;TpHU;DY!p#8b$zdjbi(bAeZ%g-(Dtj4KfpV_U;_ z7-EmZ(AK|Ld~Wf;Xu(G&KxXe-5r8vgMcxxw*lwo)10Er-YB)Cg==W*%Em=B=?{PlX z#)63w1-fl;%F%o^9V*tPmd1)y{&8V)(nyq6rzYSqo1&Grc;OrM?xTB zaz;ip;*sBv29vl7pNECgF{+U@{c9EIzX6Ym;&#vz#1+HI)H5cw$ro%ZMlTjHf;xp6 z5FP1#9s`K=Q17sM^>JBMID4{bp&t!H#wYSs!!Bu|TIU`es5i8TCqU@=I3)Bvf#*69 z?FJd zVye*$Vo5?C*S4HlC?V)HYQUWQGcBPZq44=INFPYXbD5e^%$F8+?$OUuYn;kvhlS() zhv*<~w5&B2N_Q)vSK+5I?u)zW7f658aqZ_c~2(3fTx4tu|p6l&Rm%0qW zZM^5W1YmEbx+pMzm{~RsxtxOWf)qpr&WQN;aFoj-ka;cLsldwfcW}J{mZggmZc;cp zQq>_(gqeO6H75=5 zT#an4MknTpp#3ulGLTztFnA`3N{b=cBLZ&xYw)!C@7g@A$O@m#Tv})hI^tEr1W*z^ zmI8}}BO53uXE6b@#gvvDx}+O%Q|xw*d#Gb`HF+cP7wg&_?-lIeluB#zD#1x?hJznJ5ACcRZH?0SDP{-L&To zW-_5!7a;nhSf9Jjb5g@-q4vWv_^P5{oyR1h585Yqr&l2zpn>N&9f>f2hulSu zztsuYH(-2?L-WsQgL1I(2|u)gp^IU9m`Doe^gaC$nX^W5@Fg^pa*n-cHYb;!)o~L( zKD}KDFTyZt%eNb#BkB4^0wkHt4F})3RG@tl^Sh%8JDMq{8S#ojp>0!n8JzQGyD3v5 zXl@_L5bFCR3;@d9a61_I=&xe<8DD5@OF8DM`IBr$_Awe3PBjhVue`#@%9+mWG&3)ZXX+ zoSl}@GZ7}r`hAR=p7zVr!&Q5NuT?RYk^xP=)H8pR!u2FlO4h3YG6u|cPwV4vGzG|M z1Od|TYu_?we4r#QOCv+=Veo?mD_B`8dX+V^`L3J6)`NuGr*(<^*@9D6vYx0uM7+zr84qmz8$&7V*K8&ng1-W_JjZ0zf#L2+EA*n>ZiT>XD*xo>*}R zIrsM~hmn~?PjKk(bE8Z%0FaXAA>ugt(D$_R2Lx5ZQw#xhU!W%O2LcZ7Z|HI({%lMj zNh=+-au}}`2{RZhpX3qc5@h^2<2$y<{V|{~K71}`K*?1~b#sRQQW2g0SJd9CEm@SL zPcb~}&8xEC==1XBsIAgSBrPrP}Ho{O_ z00wT8-|aipIWeCvM6-p6)D1_)F>bpKCPK^@V}E%Up%qs&YD5Z(alnN^*5Ysobu!KP zzH7_pNOnGma+Ijds}^6*a|0Fn_`KuT4qE6IIER%}9OUZdc^TgQ%hm+h&@!3?$4xe6 z5TX}L(lMn%$e~c8xJ9j+QfHHm|c-rc`G#%%&wrc(Q%-H|4SJSF&^i))j_Q2~% zPL~#NGA|#;o7}>5Nc53k7Om&f@m;9xiMhqg@UF1xeEYl8y3v3~Z-2-2dq_SJM4Z4r zenS_B^?#{{wB#NSpLPEL60V9q54dJ&cMN=Yvroe+jbeoKAUkwv*+u`=JV7~Zna-r! z+KA&@xIFTn&|Np4I4bJ-Z)!qA_6>%palbN7qt@Y#{uuVa&*f{ViU1|s!5{wYOf&$5 zAFkpZF`+3T@Rio+JMZ7yK=lPU?Y^%6HEDe*|7|4zM}ueG!MgIOm3;rEqYUX=9QOFz zGjLSEH{YuN;V2mhyulZ=zx&L;>N@V?G{QtuR@IQGQ{^M8p-&`2~tGD6*r3>T# z!~XxbTlud+|F7o%)AgU*RT0dkJnixn2bhvAvH?x{Jg(Q$4wmv%!Z8%L-G}j;LzcG* zgY~ce^>~4fI-ik$l$DovmA0e;B4GcXJ-{oJNp|LpufKwd8y`bn1kXsG6*_iNV7(9E z4F60aJVIX>dVO6GUz%>dl76qgfKUg2NR$chzUu$^{LLG+9@iu{Yr2RJ3?RDXl%6fA zmwjV#XaBwv;1v`F%XadQ*wKFu2931Ha%JWDl zW#S)GPB?s`|0!DXPq`8hH~1u+ytugdw8FdZ z%T8jVk?O`?egZfhf z8UT2!So#yVJ#ME;l$U0zY^OE@h9}q(%dLNXyG2sA*1!F@5LEj4dO3V^u+Zi6tfoeb zwZ;d3{myujUhLe?eUcB#I3OF}ZA;*p{(0ywxN%d6hc%dC(w2w~(!`Oa*6ETTh@|?{ zLWNUEJ&(=Kr&32h)JobckTr#@#ha}NjkeuoNk)P!^~0xzBe@%ixk|#5mY+?{^eXW8 zfnKMRWc_O`15+(WRN>9>g|DIURj#tI+ezl+@RHWk@9RkV$Bz$AL8ULs$`r?Vdf52gf|}Q;{%4H<9=E6x3%GSTV?}G zQW8(noclKOL63@@=ib17xt`lgblppi9`By(KRS6}JvObm7rRgpvo!H&b5|a$@r{-c zg8S8}#ESt>?!U#Nf4`IhvwINUTSF|C@=^_>i7J!%Acw5ZNap4pp1l&GkK@}m)~|lr zvXE2 zvH|@maj{pNfKc1Eidfr4{wB8sNQhe}|K8F^J>}|1yEPLlgQH>!oGV5+snUJoz2!gH`&e%hn)%XAPMYvX1eOV*{d@qIja$^VE*Q|>I-dw`mXFES8U zU!v+EXf1M%-?D9Mc&Qv9OnAuc@|9#Zm8qPsM^|_?8;<6?ZGMSI!L#VGm)*k*Rg=7z}G?q3(=+WwJq8h1f zpp>uwH0qul--MJ`N@#>F?Qwq@tuOyJnLwm@{ewnQU^mL|cJ;`I$HPy7R_R=Z<3%A! z#WQ^SSAiwp)je|pd=bR}rCRhb-4O+Acp?W}qgRwt{-{vHEJ%c< z#Q%N}2qO1eTf!EQ(rzAp6lwL#c#v%$LiwTf(;_wePdNUc0bxXX&R}g@)LNr!=11Q? zRD*?$LSSeWEcA9R_Q*)NjMt~x&T|cgO)BgY=xD)r`(``xNkXXE8Ix@xWlRjhFC#Aw zX|8hUNWg8t6S6ZspUBwWk;iXWsysO{`+VT`{qDD87iDnLjyv{>{C?59YOUda|*7w6o1@Bf1k@Xx2n>(oRM4l4_QH&q;WJTVM zj*2L+s__T+6`m~xG9VNGLTkC6G;i5=fxdZS`^B)cCi;=MTPpcst?-rI zd8wDjx%Y}iK*`R~n&@}&yPmhxm@nOFil|Q(u0+m9cZP+zAM(w|KgqN%Mn>Mwu5^|5=rUygz>&kD!?miytUBTm}gWmKSsh5k9Q-YOVr^CtgUTpP-EvaZ(Z~Bjz zVhbI|;@G&tuRoq0vk?V)zSXwXsM`Lu+4(N33$v`mv8$0^*dR4>Oy&L2nGS9EC^I7X z0~Bu@n^*2DJDJ^pUAeagUWEOkWI|_mM!&b?gq<|GL!+BYNXJvVk=NwRoEQWr8$Jv@ z7=3_^H{!shYxpje6s5Wwf2kw8(|N_Y<45p?#LTE+_dWk6BXHwqYwzlZTv9~r_d7C; zY+_=?Amqu90~6+vfTPHHQ(F4rw9M#sg<9N5iQHDW1e=Ym4%R7sO0cEl$X61XCvXbW z*A+JAoWW>LsBzK_PWq=@9k4M%>RuXuQc8|kAT`B*^Tf@ZG=9Bk#BwCuc>7~-T13ce z-eGMoJ-O`jrgDTuDmUt{L%Z$!U^ZVR&TqegTC|oSgSTpzxcb&J zBAnPyW0%QZs1{Y{Q)f}1o_olS?W0QW^o$vW$bG_NKRT(TMGnP61mi<1az5|H*B>}6 zeL0%EIclRF`%U!ed~Ca-Esed>nHCWNg_rN71+UHL{e{LB%QF#NYxq;oS6^mEQOKOF z_MM$X@2A5r1b8NGiVNqhra8aBsAnGLgUBC_g2*v=Sjb)lCOu#W-Vuo_Mr|l_pvd{4 z6XDpV8Qo!S(=wv4MfxdBEi2ST4WC;4uMKfL0OyxxF-)< zRo!!SD+vDX=^)q=kP%jAE^*cnDP^M+vDjE**JWZo&h{-$P{i%Dt?iwLG{#ZnqBpbl z7v=G)FsQUr{>vMd-V-_iB57mRHT@g?%9kyIX?JKDEiv5D zB|4dqA>>$Kt#Y>qz@ZTU6MXPcfcWnQry`1=jLwcf3YFyb2E~g%#3u?&PEP)rFh3{$ zik}>LLfNm;3xfh#UDczPS0Cd!Y!o|2Q6g%^NV^ z@kruV${nId#K$tdV7UeLd1RbO1Z+MzD~2bp8SpaqQ^gZi7qp^gmKjln5goKa{BT>i zV&W=2S0V(G)S1R{r3Oi&h8(}8FO(LC?AA=&0Xy?QyAj6-RfkKwBEdAqtQK80vH1H% zZ^!-3JCu?a?lZe@gNn%S7OIV!8$O@Ayr(odNVF;aM-j)8c>VF-X{|cwbF%I7CGOp8 zlKT2Gf__n;P8A=5Hm!2`NZbKUOTFEBt#Fd9xv-)W;&61}v9yzG4#jnezf0WAq9J%q9TgcV^9m_p#TTJPD!HF#?hN-%8pMi? z2)2RwiGIZU5>u3>Ss@wC(fyUTWT2dP)RMZ{bFuy{;RXyndjZdG39)s_7aqnBXfDO# zS-!uO6}Xh_TMk6KJ0fkNu;3u-Uy4hBT_5TRYnZ<{!LGZ=TZ;%5{^HpE>1a8G)`kOJ zo2Oc`A;x|bO=AbT?8x{F7jblfNttYOF_&&JR_?XCMVh7}T+aYztc_wFdazuOBob(2 z$en)-2Lt?by`VhdISHG$=V8D!3s|d>^ED$zf>#VJN|-fed)IIOEE*~ACqIfGx8HCG z4T&!On8o4MuOwBR$qNf(r@Km|WmtuU}hUXn!oRn-Ss0esXswh(SQBd+*e2{kx?q zVoBpmFlZoQ_jUsuy(+@?{0W{~&0seq{`dH0F9!H531;Hy!D$n$&E1LCr@iY!OscJ& z1R}4%y)4?2!Kv;hf9fq6``Km6naoT)g*Yk(iTdoZy8fXg-SOo&#@*Whyf-e3XE?my zQl1*9+F5_vLlhvFcW{rl0~lv%o-1m$ge!yw3~v_dY|(h%h^p?!lN zAk%G6`Yu+;9QlL5S!DGb$QUsGfczG43XFCURSrVgN52zTTjcR{QGGOQ?Y{ME(gdB(DR_pOt;YA|YX@JIn;dc_Z!MO2{u0_F7SWn%wx5ou4}3LGTgcGFh6LaMbb2EG`6^yP4m)Pz% zp5|#bcV=gJ_kc~#f7oScWb@+fCh~U(eE1LkgOEgR1#h?X2P`~O4wpx9vS4#D10m?H z-6^2Lw5oE3u$#xhU*ZM$x?{dN%gZ#PUrX`t?Eul$)J|}DT`i~6qIEZ3d*^Ue6xEZt^WR1`Hoe?)5q!93%0TJ4X9B@@L+R3+TFzD96R^kbyh7Pj6gT>VCh0;m&X6-ZC+s!Tz?s$XUVj{3J0K)-efQwLl3`)jYwVXt0(lpQYdFr- zT${C#xBS;3wuHS{zei-j^>{IC4B&kA>h5n=o%$9HD+2R2g8OI5-XxwLpU!7lK2RKK zBzl2358MyNu~-!Z@TAYqfnxsmA}0^I^h#}`b=xJ1N5f|GKkcSVkjYdAnpI1CX)_T= zU*SBxA+vMst`l6{PQ=T2b`KZyy8N_UjrK+((l1A_5tQht4fzg}saQKm-u?KA1t%^R zIb*voV;otB7PL{p6qrSRzf6@C758EQ@uvA>rG+J2C&`-qDFUn5eJ|TxYRW`jq(ku6 z*w(ktt?01^myVVbw%EkWiE)zSheVTjKNw_sPVMutr}+$%rG58A)QH|ZI4f>nojP>E z`Rwm#T*pOzb|n$`S`t$8c~M2*;2Yqs`?Fec!RFUr1`!et>$ldohr6vPZn*q2WONB+ zwh)p2Nt{&%3wE;$HvsF*xFNxQ*zsT*(Z#*vrRGt&`+JWrvMW2{Y+($)^S*IC-zK!yxk4{2L&utP`7u=UAg5n)Tc=YWN!ERIb(}Q|2 zI-@+1_Deg(FRov6?(GJ`?bL{50|cK~j8#5|ORNcuJzFLbg_r`_DQ7o)K@<*Tauf2% z?1rkRymixXL66US!%h;HjgE=J(9}FrX?fn+N*w>Z{%6JR=XB7Fjyr8$IsSgmKR#4N z#E(H=E5CA++OXCW-qm&(Si?K6;Vy4fwdUu9tpjW^4c_(E9U`^gL`kmMZijU7PO(VG zx!{}qz_d>fedJSbe-jlP@%}u!u9yh6I_-%R8u>=+@q~f*D^kDs=Z!}C!;XLCX92p~ z^N8)r5CeuXSe3F!w$mK;F&v7pPa#3>lPDHWoY^mbvpY+>uRVdvD`-Sv9C+1ZV-s`I z>j)w_6&`C1jr4eGn>H5?(r!65rdt-&irhbjhtl`GKX9U0c9+ii!`0Ih*Lxi4h&^Td zaX$&qSW_Uxzd-kvQ-^1jQwABdS*?jA&oi0%%r5bC^sLGdEX)q`Li zuJs>paUJ5q!~|^u>!ZE?)m$08ytc8|<F*Qa33Xum<=ZS|F^kJ}(Ar1NH7PA1qU&}==Tu9v z^qAKUmT#6RyE`s*vp9)>MM|>-%{iBIO+86mP<*b-t{)xjdoGrU6@Dk@r)%hz2YLLaC-Qs=?zn@ zz~dZs}dSM~AGeUUb{V+WBChwTp>KA@-#UF1PvbK8Jtc zJ>AxS%`h}yK{TU;%NJ8dgb34d>x3l|W0K4p>Way)?os&nuksc@iR$VJqLcwimnwlN z-<*HAWgwOfgk_R@!VgXlO?{VWMk(Ev2^u+R-{gQuawllVl-ef{z|GeP2s`gbIM;OY zSiGIAdj3g?w~0&CQYD zVfYj}azMMDvx4;X|M+I>(R7J)y1#uU_;ON%UX5SZx5Xs#q2YAgI=!j(Jo@b(`HQOk zt|x8#aU1CErvxQl)Anr}KYC6VytqvzC^TT@6$N{7`^;qVp8RM3-eA_Rti30l9dUc| zL)g|Yp8sx|3w#n1&S*A1|NR39099rTVG@rO^J% z<4;F@lEmf^N1nj>3I!Qkyu4U&o&!%XmFd%6zv8sQg|^fBpNr5G7(+Dj9z+Fo8;|Wb zASv2-28hbQ3x}L{;qQcaj2w4uM=T0h$^i$h-&*J(D#=AoJoDlsj{cPp6`wi8Jfn4w z_>O;(TN!R($QX9{mBUci1u^unqDV`m*|;DA_fPGAVVno7NHBd{pV(sdD0d36!&Tx@ z9J>?a%R~hR{g-z%bsmjdVjWp?gJzqQ9~KHFjBR=EN+aWov_;@8(XRTaVSUuFHrlx< z{@e(cfku4a>H0;YLJ%Y>L{IJ_c}|G6k5FjA?`ze{in5e1rX2}68L_8Siv+IV12T`H=p;VW zjoLSwePQ&%)zM2MqjrSUG)HY4nm%|WS~4!SmP|XiecJcHRfMI0w+L4eq5``L{)7XD zmrXNHi5B0`xOB><4`;yvEYX{`clrj%e%9N!uA<_v{|J7NN!Hbxx(2AE9!&vCV8#6; z5v_=oHngKqsEEuM_w=?-Q&@;;6bmUSjS>_MvnkT-Ru#pI_eR!(=brVwMkSx9&oR-! zOn%mHU`M8`ZymC5$!Cj}E^~y)Bz8R(VdKj5F;ThGCo1~*I1_vZDU7e3e2+%7&w73~ ztQ;LN?gt!v+r5w7e$QifO?fCb*0r{vUJ48#aMnscgd< zf3UcI$@~>FZmOF$ynfn99Cg#n-E|rET0-gdt>CHTeo+SH#j{ zYO3sU*^*i-=9@hy;Co~egA-SL;KRtTA?BpxbLThqz+bnG`o`9g`=XG`x-s8gHsObD zFHhJu_s-3)Jpy&U_Xl?&>(|y9zd}CZrs+o^>sRN|@wxHwpTH}ubviiA z!8;#63TH;|UlF}Ota<6(Z~((@*PNJ`B>P!HT6%lAl`>)#vV4a!oUhVy(o+4<{60*5#@(s^wO7svp>Y~qr5Kf-uiD{D zoSpTWVxsb&Ck&KI^Z&Zv!HbrBg2IkATYz2xk|j6^(tIH$aVJrM>(`y8j!MG!3T^l% zuDCMkZg3K@)xg97`q7csc>jZMzvq$L?|JC1DG$g`5DmYluwz10;*7)3zlJ8o+~$Bo z<7{154eukw~DN$K9! z8J!X%YHElr81FtCz437L`t8v@=S6p)96fM$d_0&O@#ix;4}3l+dg!9)t-GQHPe(h~ zCV}s}G~6X;YhD$(v0&7aMPKD#gaXma~27ez0h9ldgP^y+!> z@ya>z@#;BBRA5nQc>HH=Pya{T(?@Tbc|o+}Em)aAwx;v9d_?)5H4>)Nlv~|62^X{S{93~8AcNRgWAVjx8|9pRRROVxltnmM%mkF*Ge&^3i)sKY<@t9HX-T5vY`cu#ipz%lBWN z|L&{v-$n{AF*erKAy?S;_4PaBVD|$K8Fad4&NVjn|jt)ypv!(s`ZDKn*>!-(`i6{N%17f1G+z^$%70hj>Lx(|6o^;!Wwb7an zqfh6&)&Jib^#e(g_iMA!Tzy7ou%8OQEx|D$2z zFV@_AR4)qjCJd?zm?EhDnES zoAjd{laAam>G0&=_;bTeNACHYe2;Tb@I?NL4?FKYaoc^T6ZQENg`+B!Z&QG3Idm2d4> z{^IUWAK5kc>b6%dX?^vQrn$dwp8NaOR|gm74#o!%6>f zeRZ7Nd7ZW<@5<&+ZKjfU*N06@k!*j9jKHZml+ELJNtEn9T5{hmE@~OBU+D>&L%rAK zvQ186=%hnh(mT1W^GNCc48ujU^JaUWz#AQS$WbUWA^|E$=8H66hN!gdAu5TJD&p+p zX!w2uvI;(mWVvL?lEq7wEnNDM5S4%Vwliae>mr&D?nGIY>N<;Y15zd4O5z-*yAnoAnd(LUjE98IAp6`?w?8$APH)ha9`F`7!tjjtIv!NAAAv zC)+0eDthaV=$%KSg-<$eoJEOBvPvg+U-@3N?3pzeeLp(qAEN>PZ$eRU{BLnHYD1uLK^puQ>! zB1)GkB_Jg|Kxhf+y_ajbUUJX>H#57Ny}Kmh`zqr5zxg~Iv%9mivvYU3 z`R>z195ChKox=xO{A&q+@q>!L_{(4Y{8Al_Q34f_GXy{UaAYix`h^aSr$ZmZbp+j`I5YH;XoW zpb4D{N=e_h!jW}Wlbhn^aKi1rNCp*d0G1wYP74F57>`c@me`Jc31YEEekJOtShn?W zZGRStR%B4=O1qvh?j25sQ&vlv(4xqhRjysrFMDNcS+8Grv2yYaaid=KdibcgoYu<7 zq*9g;3lyX*sKSHACP2k&kzJ|yuI80hyJuFG%8|)?zjWP-qZS8qdX{2ytMA(7R6~>} zR=Fz0IraC;?mmKM0G4<|G#U0!U{ zMFJJLZ2(l#v-7+>j(CME3{*x5hj_*Le%|6W@JL}sSuUrdl!#-bl6Wybm8E@b`3zu# z`NY3DUICROCmE<9s+gEDfC|rNFS4&NP#-YsFM-%l186x9{@he<=x-gBgo5 z&-jY3p35`E4X3QZZ}k~G`sFv?N1u z#SPiR>M=YJpz`|7H2payKXr#L#m)IdC{h@xK)W)#CsV5Y+@DS^rqgTb%tqxQ#TBXq zjYv<7tLMa1Y9z%67k%7@-m52r%5@T`43I$u61~B7?jQ`nM967H>!k*jN?rT>#b5lO z;xGR4Czgt2AaXQG=HIDUDU@l7sPO#7S^YHAo+_N)5*igafq3SLuW*9*^`7VMD2<#m zABUJ`wWg3aY1;?1{XN?D7KOY+A*j5^EVZ`1OCcYyL*ZXg*uN>_8z$jjF^QN=5#Q3G zDNG`!u(~3Lrc_j)0%2YTR%8-&8FwGih8P9|hav8V20wqYr& z=)z1o{{vl^PN%2RnJIMoTP{;<+jw-gNASZstu4Nqc zG6X8G)KP?QE9)WY6sx_w!VJlV4~Kln=C(`yN@Cr6< zD%&loF81?7-aUKyv?#V%itrPafqCs$x0JiWQ*QYz>?FHWoUZhs?$BkktMAt(O{7C| zc?b4@4m`$elSS!MeZO8)Cyh|NmGaDcj9Jy*rWQ=N@?NM6`fZgx@2`>*{-#GK%3mk; z=p%RdZM1R>*r4nC+BmJ5Z*T$-|SvU$;HnmZ(WSbgK`wHOlgui+SYqicL)$M`3# zpFT-5!74EN62i^E=JW)sxaRgY|J=_KI9k7IjP}=YJUOF9m|Uu!xK*hrW|(9t&Dp-x z_S^Rq^jYf2hMJ+Zor7*H7h047H5sK0tO)}!*MJ+G{jaxf`aXTfa$iuf%aPFk(ZTfm z#b5lO;xGTuQjsqle~$4pKqbkPyC?gnj!>ZpZ*|kO#v)OPd1`%iI4>Uudd4vDp)Z@- z5R~GY^@Mq53kn{=u>_Pdih@T`$XgZlVC;XJ!X{AI7YtW?1potK^-0nudleNSd=mk_ zl2&t8*+sTwau(y)NdgsjDV3L!E0?Sn$r?$?fpAcvqEr zoSkiU02S$dv|4>` z&0E-6HoG!lUniVvFp5-V%RUKI-hA@|1nOI;NNb1t9_v_tW8h~p#ViRRR@5mTA#@G_yDT*rW7~y5@nm z2@Qs0;0-jk9*u2K{z+q+`5evc=APRfpaSbJ&MG~HcEvug4=o!?%ZJml03O0v%U#|r zUf;ONH7ez9GG;k8PoXs*(5jJz!|RydxY-Rd>0iUm5k-KCYv6VE0oQ|7$~R2T-u4ST z?n9d}@q>!L_{(4Z6jL!maLU%+aHEggG2giPU1UpiPq|x!0Cr4y z$R?AC5v<~yV_v0Ek<&G@SCM`Z=~kebK@vtTEtPul4=wY5Y?(j3Epnbfa=7G4BJIqZ z-jN;piEZXj@2FI;pEqusKOJXvN}us;!mM7W=L~k94=K2O0+iz7reB7%W2UqSXUeYB zU0>$ye6MKdh~k|?N_PxFih3k}G;HIty}x05KOv57-2|xEgS#RBBTKrnts^3U(WTj? z@u6Ocu#nnATbnA90qY`3q zNXhkZvqrZA%`jTDF_c$10E5$^nj)vw*9nIkU*S+SQ)H}qCoXN|wr67I%bYeb`IX<4 zYPwVdvtwqe&I@r(ePX>7KJ2~7%Fa*EDtxZb;BGGsfiUOwR?jl+N=rx=MTUxarKrS&ZwcA;TgYAvj}5p**Ct%}$eA$6k?w+R zV!+*Bexv(KgHm%eDX?(M%D=|nvT6!69s*RD!IjmFT#7lRM#?yoGP03?6U(66Tmx>T zS8G6={cms#xyd%9mTvf6^usf7RhzW?+6$@fSa+_=~^%6-Du*wJD&Yc&D(ZUsi2q$Q0Tz7^DI3c_m$m&C}4!Bzx8S zf{Lnv6m(dQBmAf=Y||(ml8-9Uht&ayzki&C9u~2Qx#*QPyqR7Vp}2VZ)2TDMQS54M zTm($NT--awl6t_Ix`UF}6zzD+xcyZGAZD_yH_W1J6JXq?qqq*hN={*5Ah?)mha^*EO~{Xj`g=XSGV93@p%wuVR@}{J4?x_ zbvsePD^3(X*;sRH-Q~J+Ca9>yS&cxSr6)E9-~kxGS}r+$U=OY$F2to=xuUYEs#jbH z9hSIVXo*!gv=+EW+!SsNdvxR~aXUO4dyR)3;wmSLO00oZj~chbnpI(Us0E4Y=w~Jo*=njT{b@rPc4L zGXA!|NR@t-;5v`=^MX>ky)^8Jmfah->dJ7XW!EQLbrTiN3=A;fAAdeB0gmz#*z_4Z zY_tR(K7qm*s7(85{?r)@r~b64amyZH4+bg>cs^883VJ@xkwK+JSF{)E8dZA^loj*K z{=jb8wsX_A{ftTA9AAlaRm*dI*nqqD8Hk)J@Wa84t2JQ%tZXXa6Htn|oCK&K0eo(W zfzqQ9>5KWL^s9joTTomGP+^tVuXiHi*09<%tuy`9%`vNo;1en8*X-w+0T^T!ugvag zy%b@(bOfvdmSFy6E@w3#A%?qJ6J?$#`m`N=@DROQ*Y?IuDp0wh0#pWZPZh|XIp)I~ z6pe}lp^DKnzeT` zh>ImW;Tch66^d^nc`s0TSY;vhh?2cgifAn5XO!|bDoGoaWR4K~_w_2v^Z1a8nkt0t z{QiDqdqlTT*U;&?&I?cC!AHfp#pBP zhjc{))ArtUai;dN0xE7t#nZKG3{+%8YL?*4Vk47<%FJXiQJIB|7BVwiCUxu@v=lZ% z%&pwTO`G&4(wGQ8@H#Gt%4}nc05VzGez8;QDH@~q9??d_MQ0|xnGM5;wF@)YVD8V@ zQ;IfaQioDYzK8W-=C5UybSqk7kQO$0c7rB7dN%Vm zhn>t;HeLL^sB$D)v-d=?IkylS3?{cM4BcNE44WuL9RXTk zmRFK?rOU{-$BlaX{qu407vnA=(Mq>IgIKosmj;1SMvQ*n3(GmlGdsZ_ews6F#ysH4 zlU&g;;@yt}Mt?N&o%btE36IYi{7hiS1p_mo34yU0D24a$t~5|VaLD$}A)!rLbP+~k zvLeW1@DOH2*0Kw`$36p}f2m(ab^$0w0hNDZ5y{;Z9~JzCG!*bu^!cySj0RZx!6APAS zPG4L)bF!50t=GPGkPHQKvl*wcA#| z!{K&)kF@K%sat$O8==fmyz z>8yX$(T`kb7lokNz6aZ_Y21n8($Wg@{uIoEYC4q?~_9+0}K5Z;d%4-Q4N zdu3ObertOUq^NVH=i&f?9QGWUQ8C)X(X{W_lBUmWY0zo+LmhV2W9|99v{MgqSG{%^ z_SQ!_;l3c44GiqS18ol7*XEZd&wwvX-f_-Sq_fEr$QBJ>KfDpRst$Q(wG4l6F7B1U{59FSoB$u^8}hx_1BPjw`R_jpe4qWmYL_(-q!$0bdF)L@#xHK z@CPFU*KL8U4BEOfC?s%QFzUFAqbFj-pOh;Xv== zZtL#;*I`r6WOAM5^&8=-;{N0@^Mof^q%Wpa=}EGWidrmz?`c+!OItSuo+lh9DjvF3 z`dP}_wTw1>Svuk#8eF6NjheQhH`#~WM1!wqtRiI4b+7@W_aCAU>eGky$;=#Jq+iLZ zqkH|K6uSQ6FMd$*7k~M~pu(+hr7*h+sF+^>(F<#k?UQ}4+1~TQyNs` zqarN66i`tD87nF0)tgGvdXpqP4kSQ@JB-=m+%FK8w9 zLr^iY6Gfj-psr)WE@Y~~E~>e%DVkA@#}jXBMkf=KiVF4LEpUD{pFWutU#ocuZ)V`9 z=xc}(R*Xap5|?n)Xp9@o{#Vnu+ndp|BdOb4u>qe5?NdyTA@o>pdbD?Zt!5ZD2Cw(I zFFr8V6|UlV@VUary2rHaO$Sa|V=kZ*Oe~r0B}Y!t-ea^m?8H4S3$Jf#t?`6Kx)*j+ zs!=t;{^IMJ7PI?$0=I_Q$h)x#pP7cy_p}5z;;S0{>9_RCSbFiJs5-3-(riCrP~DWl zbK1?fw=JmCA->)-Y+B|lA&miwEa4!ePVDU!MB!>Jyj)e(eYGp>Xhv3gR#pZCN#YM2 zJQN(VYisb1;E%Ka0}Ed^ie$;_lRSBW17>$F|U_v zuH>B}oK;9xu+ZXuv~+m!s!z#~3^x_!v60ssUEwgccScEPLZKTsrf*wCVQP>@07JdPmfSRTzbI zpx}1aZ7 zvN*9gm_d==47@3YMkU{HM*RdN5HAO zTgKhqJoT1lS^sK=<$lhMLQroe>ZtHTY_qgxlk>HkAH1(Mjh{td&bu&oSy@I7T}XpE z1>jM1eG^=WU5odVa9eZm2A7tQxLPf-C*_uA={Gk`y`^dLt<6x!b3m9F zPHy3}1^OxT>5Dm$bz0#>j6C&boFdL*f=cqO%@b~G2KE4%oVuIc9j+ByyZNzuThjCI zxLfK8{V)ljJKTck3RmYM4S-8iz*5npJ+mHv;Zn2Sv?Gd+ zo`n?|R8M5F6dXHcKNv~d4nbjp+h^_hs4{PSDwm;1Q_N4+O-;chz!))3Oa&lH%m5}0 z-~n|L@6Z)7X{!5;2{d3lv?YnRHc!6!DYVbHxhYTzlEr5mm9(3l5_>=?>GyYvd$$e`%uLV8 zNYBpFX!RP64#L}48U48wMXQ1AKM=Mz>@e4;1P87SS+Q>W(xA}A>vtbGl@uPGR0dTx z*JcYbItj{cSPn8b2^NLSU0DR)tE3!LWrDY)wnd+}qq)6gT}p3}Rz+BXiBMp)bbxC`K+)>) zq)#A=&QTGJ!6u?sIz8f>+i24ST0P1#^i~EcZva%TXP`2;rdv{{FpY|7$nBD;A_}N?-fCr&M|ucIOp$D+La5sYhG= zE%+lD_(8>A{>L>c>OAy9qvEog@Ec=fppuue;I;CZji3xEoZeRRLlfOJ$Wy2YL0#GO zJ@nVNe^NOS_0s80G^~9S#3)0)myDmDrPK52T%a~`p7!`G{qdj7J6?uZx4$APOi)*C zdL1(De95|GJDOjp z-RfkWR>$kMj=HxUUL)^qee}N8s3>*3o~YaU+?_2BKG=3^!;bXDJma_X&&*$;%gCln zY1fXb&dkbw#y?cAMw9_q9@Q5PcJ=E=Hkpl=a%sBt3ew!|$Qxwp-- zCY@>5k+O*BwCI@fYa>pP`mwa4=nz^3uIA;wb9TFxyZNvn&mI{Pt14Vi@@MNJe3y4t zm-1xYrJ@B}wF6VeZxfG%5LuBNT5^<Q*5cSDZx+q# zn*C#^gqbg=%ucwN-ys19)*SGIlaSGqb_jgPA>AA$2&&SRgK_SB2 z_cC`)bmfF;@-I6)Z8NUHfIN6lK`G4h1fVi|nf+@usBGk*vJ>Mk<)8v{Ab^TU!I7l` z6=*PoMkS{a2Ng|)Mn%(ehap$#0}m%a7chCbX8Z8kmD8C~ssr2XbcJ40h2 zbS%(UK!tL)cB{ShI-&t@({X40j_7~k(_QIYiaj~AFgrK)=ViFfoasweKG|jSV;#3O zXpd&W4Lfdmv?H87wmjN-TZ7K%;qbj}FlO`yr3&;LAQs#v<|OT+@LeCVe6yqA;AsXhdkN|o4e|_2MNU1ZUr;8=*ON5 zZuJ6P%%Z$vNYPOVRNx*1Y=BZF=I-WU_qV}#);`_^B282y+N}43!m-+wHP!ZkrSpkhfJVBE-||v zZWHzZGxm$R2W~%|Xj3>?MROz??=lZqEV%+#k;CLKr<5u{#f(2e3aIdvjaww$QxL`J zP$vSiIlK*}Y*m@!{E2HLyVF(;?D_sdaH@1ixI2kb?BI}Maukp)hpZW7OJ!nBAuB5B zY>zdE(~Ays!ujoQj4^oS)@W>yWq8zOHIlK=m77S}=O{j;WXL@%##cXP;)T45LW`n+ z3XHz=gKMP)G@un}tQh+zkJH#koWpK$54+wq{CelG>s&)>x`!}OfjEZTXc%@^^4pKo zzB$%IYf6s=UrlEC-HGK#ZN3VGKl;1lttYJZx`I+rVq;?!Q28^yK~+BBtK?f_{}U8b z{^AD}fBENYR3sBykMP7MW?`(Qq)>`mL6=t2<;@homg0lBtd+%g)2sfMwqNbphF9X& zvi+BXC~-S!E|5NnEa~i5-K7{NUWde{Xk!s|96HAY{n!i29mXqr z#Q1j>tZO-_xP|GLqEXqwOuvHMOE>4N4}puel+Do0yyRJUD7~3Q_?1k*+F&UUor%z> zz@Hu(6*Lo|qU$~aW>N4mr*#M5gF#y(==fzOC*qihdQ=<}r5=+=u_=_CPwDxTU&72; z5RK|w5^EoQnPQUYbRw%jP9>%AOAB);s6L=2pd)ecX`wS{f0+p?m(p0cEt8eeMNT1Q z7E@{=olT+`^p-%U65+nWdOLZUR}!IqNgn!1!N4)Peq*540RI>s>5!!~4V-BF-||b1JpdAlQjHpL!*#PD_D; zSsOwUf7@Ad?4<2rlmHbl554%Fv-i9C13qH+aw3tU;wdtYA~89Mta3uG9FL<@m)ZE) z)Wy-oWcD6eY*C+1u??F*uYZa#0Eg~tMN78Pub~taDoxh{@$<`Ir?@nK%zor?`(w5G~!97ZidOxs(>&#MEm%^es}ex%9xByvjiZx)%W| zWnD)^+}j!frs%gr6dLK;A4wNes+0?9bSZ@{r#TZdkWT@WVlB~#@WjxEU_S(q00j$O z=8}*B?+Yj>&fdu&0lpm2dw@(#X*sr(OoaJ@I8w5hJHE*#Vwb_rs=QExo?S_Zf||yJ z^@g6)*(>Vt85Elo{q|=`!#>FxJP!B)rH!^>kHn{54DZmF;la5iD0z_V1(R^7{j1FM zt3l7GyIY>Qt0nWNPm;!o3cEyn8eL4a#l%_9#M9x}3(pNPH-E+U^viH}fn65O4buI( zDPvE#a+hknc`3%#L}^C?VlN0=pW7vut;vCAoxz;2V`)6J=w zfC|85^!O>J5tC@}1cW<-wuDx0r%ijz+Yhkz@w_)oa$JgZzvokOC;dq8e@AakK3uo; zo`>3?Zv1%K+3)7y?UsWmh@3e-pmJ3g2d)b50-kDCEXo7Ar;QE55lm$@~DvZCx{ZFU@&%cVJZ zy;xS1tSQbB@#0M^p@+NDh2T{;=e^?kZ9@KzIb_ar%QgZs zs32B&LH6zyx*c<^Av3@y<-`9aK*crU27n3|8B{*H*ZJ||XH9Oq@0i{Uq zvdu0ctW@$r;)kl>8IKRJ@>x=vc_b387KfC}DJTBXue0c(Qc&^js?w~%SExa%3|%}w z-ZuLccIYpDQ1O@lY1N9o;-^k%SJ-nqXE`hlv}u=&sWD|~G1~NVy7V*pv{)`@P**n9 zb@idYx;^UhPPLrjZDLE)FFDIftj02tEkTF~7$D-qD0Qz*-$PuaWs^wqX!ltcT!a36 z3{+Ik^c+;?AT};y;L^ekrj=Xbw(i9G9$99J$c@!GB7KPu0PMcvVEU!Cgg?+83v_sN zz)hT+e)Uy5(+iCX)6Z->ra64-;+fdY{9IY`(+l$Psz_E|F8tk@%X@aNr9@{g)|T;z zuDD!DX0A3P8v;mxXfxPmNme#MXi%E+H#E3AnPaPe5wt0Xgkn zgX24tCp0I>vSJg$6);daa2%k5X-BMKW8V)7`j0*G_Ge}3neios3B~zhT9S*{6yfe5 zsG#KVSKJ6Lga$0sfyr`m{I_KsRCqLJ=(2W|?Kqemb_91P+81VL7i8t+BWqvw9}7ReyWt`h=EvGO&& zuj!XMW|zZ5{*v#bui4BWJWCiRiF?GY!Q3m0n|MKCdy-0bQ1M1eTS#d?Q`!PbM`baS zytS0ShuMQUN^lmQp6~}HF4ZJliyB=FjC(oiNt;}-^fQ`2oM!Y=vZhqPNq;FYF?QNM ze4=<(lTnB1S; zDJUf|F;M}P3c!#%uU3cYx-u&-e+n8usQ8P&T-mb?I7AlXkch{pjmP_q-DMMl@-y_#9iP-rk+}~5qttlVJHYt3EYysemlYw+nGisd`m-? zv&A)ZqV~-%kAF4=p4h2GndIi_=h?e}xyqyRTt0^)yFA_~QL zVJwFZ0Y8{J2~?a8HK<&TC{hl^FOHd;vIhhXXmJh3?34Da2z%CHL&9!j!gfvK0lbGL z<)r%Ri8VUzkA25oEToVJs8nL&@*f%2SFe!q1(j;r^L3+HShc~VR+|{=ieE{WW)*H5 zX4uxt72L|Q;x5tYnVpfMWJ6)#4pC98>uY)NOhZh9VhKQqqD^>?@-4VPcWb4+D!lIJG;v$sdLiU=Gl?g%N zXs~x(&<4obb?Y+_oI5iEX-`Fip!FNqb3qTdv_US)@J^gym)pXQG709zV9?3Ur0)@P zm(R)+SAi^Fu@bUk#fpKEW z*&g{>qsPP7|5E-bZzmK$CdrxJ0xxqGo_Nw6tXB6v*qUanp#|&9R{ll>dMYtdNf{OE zS=RPEJrj{UKCKiBdIZHnbSbD)@*nHTOuu;4-JJm+QO9A3kp=6ivv)S9&@;^a<6=4$ zo2ZC)g$^M_T$H*<+-~u5?5T1-nMUON2PTLLc~n3IXf!WZg4v!%uJ)>D@*C#OJQScPukoKwoQNfM|)qauS! zQ?`_MI;FHZPHFXt>+-~owuIk8O)G=pn!=Jca!@g@3wBaCPj^PxXXb$-N%=quXJRDS9U@Zfn zM0w_3$_$~)%P4*(#myn>8M2qc1A!$N;d&VLcibkJ(eCt$eEO&O%~#4E)%2`x?{%(F z4O!SdmYU)>f>P+>x0-GJjltdA!7a=y>Viznfm~MHVGU%ULd$AE+{B%)^k1C$f!*+|Z;6uMHcUV29Kj|k0AohS0S*Zey(H{9NH*$Ac zR2Z2{krxH1P+}uMOJ3kdwCGh@_^M@IKd=hT?n<+|m(P0%F^f^5SzV!2DOos#;x|#! z9(VdF=v7Qy|3-!!Dm20VMTpbhJ*2Nkx!uLM-Qo2h|81u82A33!2upcDZrLd2%BY*6|agAWSYAOu}q z5vZKIz~ub7id%?^I)0)OP!X)+U$OZ?<^M~;-x2(Ez!MR?9z1w3jF%W*sxkvjk;_3| zisf>JS4@^mATL|xQoYsbLAfYb7yFf~D-FB`BQ_5lI3SKH6;V_dtz1QmbBjuf53nR< z)d~A+%SEgnl2g)z;LyRa2(P18E~jG7K$NRUOB2O5oh?aRs-=l5MO-xOAVeu)2lpR5 zun(^+vZvjO$J1WmQBqte1Z~2@aO!>`{PTe6+OL}VB#76?Kze3&?b*nRrM!3&pD=r= z383=%9K@T#${UMyM6!a*MA-f9AP4Vj3&Scr@5dg9$c&DlQm~9dFkeL76-#!wZHOSb zyGMLPorab4V4#AiT<8s9d7*xRpnr&5XCmrYCI8a2GaZf9#m2Kd9?r_2NvjK#IUod2 zjfz>)s92a^3hvItK!vFVtXv_$qPIFFlNd>xWVM}Hy)kdmTF1;)SlJ_Y3ZnfYlv#4E zmbCK-^BytV;gG{Z4pK84Y|?B-1a12;-(-P3>~eig6V8?vOww@wZUxieQ?* z0hGd=>E{F)Rt4v*-;T8O*03m%M+GyiRI`!6Sj}} z97-f4W8sNAky*oh=HqV-0h5qQ1(S?-DyP?5m;1hRYQTG!#!f!_$#;-*pG-dg@noRL z>G9uP95?yMi^DEHH#no?0Qm8X;=cNzQcX4tyqEBDq_XVfz;9^YF#X^=J#ssGQ3@Dl^nLX1L%;(8 zqOkj7VTHL{4q0{f{A?5w1`nT%3N8H3fWmGLldBBYjQKu2t(MNZce%_G`sbxIFBTfe<{Vu48 zwR`BE-D33&Tmf$=>JgNgtjtzEfns&8%veG~l2Ew_kie#p6@1AKeTBL!)W4hZ{dZHR zh$TD3YZ!|1K`i1StXafiB?y`TgV4}Dp`oE@UnrI0g^QLz^ko$QBXXVy2D^+wa+x`6 z&dk}f#DF1KFxYI&a4m=zxwk^Gxo&+ByIe*FH*aG)VX?wF&kLf5F)Ax&_l7G%38R>? z1q#LsTqQQAPMtP|%bD}%Aj)Hi-Fx>yP{MifFJk?m^1oTUo`(Ii=BhEl`S!3n3LNep$k$ z6+9E{FX7D~LUEDG7xZ`)4T?Dih@%c4n{u_eIs1)ci2|582VP@hPC{gC*zet*yVD56 z53xtE#6cBls7km5HvCmB#l@vU!oniZOc4c;P{$#KzPv!iajixs!OGjH0u`h#q0i@G z9Srsm*#Jc}Ex3P(w1e3-~`Xr$j;>%9-Ar2A4LvFtkh}$Dck9bQ1#e7LpZ2cKi;5nXDf744X6kc zFTUnxxMF1sZppnK8Wk+=a~H3s>C0&HFSvJ*Rb1_-kQUVsXqLinh7fQp<>&MrX3VoTq+)4g;Hq7*=L0ui8+zkY`<;)E&eIBh=!jSyy@ zXN_cuBSnOO95M5}F7{yBan#=D{nR1jj*Ol_g$6(cM#c0<28$Y009|EW1CVwzx7Vn$ zk)Ok&3}P7ddD+N`x>4Bt+#E15vwi=PHvP1%`r*dmrc(alRNeS#XQn95^ezcb9Nwm1 zf5+!pa;7(%tu9j$mBr?VePKEf;NJHN?d?r_dXYAkG#B041UX!xi{FXEYP{;a?vjz)2vtFL(jeERf!$p zqaySw18O)1-st+^QNx&K>}$x~0#({JWOg!?p4?F$xrz?YFIh0cKKB)xjkNSEFfsE^ zff?AW?)G_o%s=;|^>5Ppw@o46!GWHA@hzp+y}wY%m)Y+&E_?Gn_lVnE0k>F3)v~>L zJ1PQNtZ&|49&i(V{BYX8U%*)!;%SbVAHUh;x*||P5ulP^TvW(~mx7{#!dWvIs3=kC z1S%+}LroEv6kiRfaL^Z2RODF^T!jya#edusa2!q@-HosNL+RoCIur%j92Vx>;4>V(mK{%*D6j0ecLV!vDlrva- zBO4?7fu=%aYL_#(i@KD-9X+%7VEbe2XW%+)Q|Qh+GE&u`!cD(q9~Bm<%psQA3aALZ zitt(iIS5S)PBG63q{)lv%Y}#UYlV>ws{j>?4P2*@G%5ZTpwB?yqUxA5`;O!%q57rl)@~Bmfki2xP6mTQw7x-^SE*R0DyCFkPal(-l=Hw`{2))lqs z*n66&h1^Faxl&L8oL>dqm)EDlI~)T4tz;?@to6+>XgsT5^ghja2`BK8@*-V zzYJrVr+wOuN2Ydpf(pLr=De5#Ed1BerTL2j5aUY$6ZV;J^K`Bg! zS&L7gKc!5fBQGw8?s6hoqIUQDEibf?kcpDG}OaOs@i0Dv7DGXGms5C0Rpu#*- zd_jdPUc{#nR|h&Mpdu-1ghplYBG5va-oqs9?73KgiemZ&N(@!3!tCFe zMrA{VMkOl~PjqZ76o8#O@OXfVuyga$sH~^}6;`Hx|HF4vegLQldobnCg#Z$LBNBl6Xl#9|pSg)xG2b*3O73I2`H5$~HE~@TUT>@9FgkWyCc$}^-VRETviqtn% z{kx+~X${ZfHPyb#t`Q>xAgY_aUOh;DqYpzEVA^LKn1J+q}y76E-uz?aSRBfTvn-j^5($Xu*1`-Qp7O3W9Exov5Q{WJ{#&M@o3MLB7CodN8Nm zYXKzrsEDXvYEaR6gNo!#&tHX(1)yTVFMm=(qCNWjx<;J@Y#{F;%RTbrYET3%-pEq8 z7!jSB>7C#N!V{G^Bv7f)sC18(K&83Y3f{}~D|z!y+lnnP{Q{*hP?^2K7gSVUnv4D#}G&PAdzF#nQCr z@uY___Lv_idR*SG&GJ??&-%4}>7IAe_kH8YIsmu(lAOzoZKNC{ZebE;bN#*fMtpy6 zm?Bpw;&^5l8;|;xNA?_-F}K{5N5+fh^MUsB3n^nO?#rHifXX86CM{`ybuGUMLKi1G zFRwEu>>_Kjz>Fwc~qL&*nEjBZynq%KpQ^z#VtRRJpf zYFGx{Lfby4y$h7sCrac=<`56r3vofJBe z4zEN=VrWq;{EA#R>;7F(0j2EP9V#gqJPhI3&&onp7At|g^t?pKMdF;@dlxKRjAksj z)@8iJ@7}!&-9o%(e&K>&7A#t5FqR23EwyGw@gYH(zitPEcs(z^Ut57au;vmPm>Dx?GnqA0T<&-jJX21>k^T@$IKU|kJ}Kx& zXm6lS5n~IL@M$m8+i)r1g+t3~yfTwc)kSp%y^!U+_!qQ(Q28G(zT|KnFJ7SHrK6|@ zRI1S!R0S%Y%V<`FzU4|l1x2itSFKuA6{v_!-;0X6a?z?l1)Y0=itos=2M;y`5&btT z=XJ|ffr?`G=4HW!?Eqd>;CX|3v}zPo4@RLZ^~Zp{GDurFW(Mn!L%Z`vI-8Nf+zi;8V`+%-h!VrHK<_O&05~}G{A|w(4W8TqHB%gtxf;~bC8udERxgSdhc`*&iXW)VUw+Ga&0#ul9 zy~uqerW&|Xc4i)(9OGJXWBKwMS#+$>=Zp5fPnlb6S%;Bn1Zzs9O*HaVrbvt^vaeh- zUqty2l>Ct1%mbi1>?WvI%x0s-Y#?K@_52cwolY6+04kR3qhyRJu_E%`R>z8)8K_+P z(t3HTK4Cu#XRLqAd5Vij`MkGbe2bEi_Zb83aty!KJhHZZ#BD;aVr8BxH_?de^>5un`@T0sZiQcp z6r@}FTjZ*g-j(bh0#qOsN`{KUbO~xuaX2ammE9av7I08ekOO`CWLK0xMNkU*sR~p? zZx9edRiH9s#tb#6h{0DH!md(KQ8!@Gy+W%56+tP&VFm@S>o;ytKm|pNP0RyimJBL+ z1^H@F@%k>~_H}Fy04jlgQ1OGx|3^?!m#d&rQJ@H$SU}gWU*8v0Q2$*}@ztm(+6FIB z@w!cR(FSbo{_VF@d_ct|`d<~eQqnY3p-~YR^#T>Own{xkzZ3D3lin3-RD6F$Mve%8 z)bv!5$>n-MDY)N?!8kpqsHRHO#ZJ9muHe9RlP7;yp;1X@pb{9k{F(+8?xUjTndv2G z`bqQPw;gj&Ydpn|y{koZoz}d1XkwIlnkm=X002Q?}{TZ%+0#RVbFkO-ND)Zkq^4_+hjyL7V z{jF2`ycIR*J(jPg*oe;6pdx%!04i~{5Xpd{2rSC*HsKXLGX!E5ea(|^X3Uw$gSN4_ z2+dw%xnj;GA(;%4uJ|rG{|=`Vm%}58lM>O(Ee^4)h|HtIPV7zeX9eDSZVC4#wOza%3swmSD_r#o?1&VuDS$H1TxmDJwT35?g{MtUxhcnO z&ZV+5&U5qK=Vl|^nJy>3G&hNisac45W!MKF70b%I5&f(*F`_W(1WUcbK?Nd%3aE^( zf-}8G+7M?YH^_A$2=-;BoCHdbp^F<*0v@#vx=FtVgA6GhDBNn#p2z8B3}3a@NYvY`+W@*&f{jRCuUjXjJTTUPFma9C43%#GTNq*hj!YrIvZr-NrZXMwWWG)EA7dPvJ{4PK7I! zB_$R4y*z`;S|4dfu$1@ILo!eSR{&I$G$Sfd34=%)mC#VZ6_=TViU)F3u2Jy`+vRx{ z!=CvI770*^jIKZw8B}J1QoJ-ORe?&C8Wq(qI2=W|M&)em`LhbB{5WeSE)6?0g^`pP z06bV(1*iy)QUChwKdAgK7sTmfMw7w8VmaGSF3LK14lj!K zz-tA6g`lE*wY=8cugXO%a1|{XuvPUVv9g^ub+O4st|TrB0K@WgYLitxl(?(ch`HT|?2Zr{%3n<+;}1qRAD zP@ayhiL@J(7;JF(LFbxkoaybAf{IIk3inZ| zaHdB}O(cBOs4~-Ix@gd6G-P7#yI-++#z^2RpQ~encOg$TY1ogvE(kGfvQ%isjF!`% zd{Y)ML4+s9z~~L;jsn+t`eH79F^@i*llat&Igj@OVd9qYTHgEpf`MZ$28^fVQWm2b zy~*LBrIaCSKhB%S+8)_ zlO>zG$gGXsIQBo7q|SJ%R+3WuHchn1U0F z49BNhj(wiAeo*Fymy3fslx=FAx49qXhnE#)mKCO2vW}(ZnJ8@uG%96*_fyF(lfJ;D zE5cL&as;S2Wl*tRJy21!4YGlj_%0x*iOq^nmeJ`(Hu~fh-`eNDMYH=C4Z9x>Dng?o zWuxbC0%&0X^Y+~k?bzm|xujP`+42Gve7|s;aCSEzT21?ZfIAAypfb0Yo99shw7>?8 z?P1W;74)K+JFs-lF#AsfvxYvbdA*KdNFDu9$lbbtI@V#gIfm8JkE~ld>V6tq4>5}i zK7WSv$SDhFD$76)D^@OBu^g8W_bo0bu8yK-Dk0-Sq4rs{5RN(V32{o?QQ=G< zxbjzcyvQ0A2~@m(c7uMn|< z$5CbKl9x>Lf4^mTNyC8 zMg)vvGO|)onKgUvtePs|`SzR1$}=-WP=-`Y(X81Jl$5086zTpFA&!4e1{G1DVKDKB zw^*LW%~@vudOosKh3^1fIRvqPw_#K_p^;X!5KjMc*%iBg%#5FJZuOTyK*Xjdv$gP&t00Ebw-~Z6P|DZ1$Fjf&sWd_ZC7}*_m|``kFbDms2r5dz z1Ew9~z8oT6Jg-~Kj&elxB~y`Z@dxz7bM#YB-QYVFP>~`Lzb0u{5DJXmxsOIakn>4< zIKJb@~qat#oV6hHLfigj%1xYvSaQ(b!G30Whgap-q3OZz8 zRdu2sXN5)uCRmWFKt)|wKn0+Mb7hT+s~U}pxKl5T!T9%ta!|PtD?nxWvQ^M>D4-%; zxb zr;FjKH7e?(mD-PC(@Vo3G%A=VuPO4KOkbi?L)}PT6dOE+{W%G8d_^}k`A$=XpxX5H9 zqYW^C9P?1HAh`#g%CmHbPri`(L@#Fgr87&J={t=;)GxSvu>9!TBam1yEjp$+Gpi^w ztC)*Yso2cOfuLe8)tIzey;kQDGRWumCh3-^;T{9aE4Y_r-PmMTy$;|9Rn#v@qk?6c z0F}b*Y}hr?*_0j6^p9=cM}#ATqj}*wUn0B6q0hgkOX+1PS^3#H1z9^YD>1ER|dx~(TU$_z0We}RX!MKWn z3QXO&MkN9};bvoabxgs)aZztibS7qG=H_MR@)VPh_Va6+Rx8N*e)c2p*=mEtM8>%|6;<&#`tR4_2w1#(~(!` z=qU4Xq~MG%v)DzJ6|$boJ2IQyUD&G>-q&zsB8EcVx&-YiSKj4K>n7{ky{y^BUR z)(?NIxc~hnuQ4eY(!enAZtLr}mkhfv;ms#$_fK?isrG0vA|P|`CKrzyXA_Seei!^I z0~HU~J8%Uv!II3bj*Ea-(u%@nKNNmAB8(+=7r71v4}ckfC~!hQaXeZC;#2<4RDg=p zn0$Eh{@ME0bG%vEayvgA5peTGi|Gli*p@ZZn?F^o)+g z$RX$obg^t@V7>%n9*H2+Gt&?y8NtWqFJ1ycz%g+YLDEF5{UQqe6&FLEfDs~a37Ry!*ce}@9d+0szI3LzWaOyy! z4`?K?UWc1XRBNVD!5TLd(Wu%Fg+?W+PD^MiXyZZJb=Z2~I9*DuPA(=>T&nwWI^`Bp zexakdq}(%uhgU&Tkqg}QqUic2x*AQ8IRrLoMa)OVtv3CVLB(ZG&7`{096E-9^KTIz2xxzT()5QpFNg230-W_D7cMh3A13x|ZVB6q^ z9cjxU+83E0c}hg_!h9MK{W&6L^J5*4-p4$V5ElWyMh72kPxChBZrsVN`(XBFv0RlP zQGW@ju$kAUQ0bAvBh$(bO`z~MU>OB>bNlj}M9LAzikl$z6}P%JG=SJPK1qlAIS-Gb zxbG=$HpMTd{7`pcq^{bZ$DG{f5p?vhN#AdtbD!Zs=Tnzhlk)7$mH@AjZIZO9G#yqXBem zjPAq?gkvtVSXq`8bZfbgdt{E~&=?9IN)ba!k50x=O>(3!b?y`;jPF&*qhgnEsKVS! zS=I|sVP4Kg6BS&_8eWfHlrubfrF!I>aG=i^{xGsJ(t%}~BU_PU(J1FF@mzFP7=THj zl7BjD$!PP>{bgIPp4|HD1utQsGNV_)sK>O!8<+H}U-~K&-Jtqq1L_!Guah_Ikqd9N zqP_FnVZRnd?!d@pQ27J%sRkA3OnkLZc-RlLh~nNFcom?cu*#uu$-i8fODRa8ByRNb z=2t+aQl*C%r6{1{H6#uyEO4-r5k;6+IedInyg;Qgx0#AbU@ejomEOES1+e4=DnJw& zR9FKg`=sQfGJ6(41=}+{K?RBz(O%4(D1K1!gUbI?jfz4hBKK-Fpd!|=I%0We~ zQBf2Qe+X36re9*@sN)Zoa`Z|-r7DezLOZ@*_5T6ORh3E83sgpp8s()?0jvD?prXtG zhJpFHLQoMc|DmAbfa#Yns4zPyo_oaMWWE_3R0;*C6fsZ%Ex=8skUP_#kU?c?> zFhB)<_K1N?zwV|@2Prs$j>j{Jl116XYb0Lb$`Vhf6DcW|(sL=V5X)=yW|jRt)W6O6 z?a95N2?LcoT2=!p!emQ;3dN^TY!XGrMYZXZ^?1*Wn~{LB86s%W8=n~iJ}-IeOA5b0 z$1X8Ga*56+h!|xj$4`MLJq;Xx;30rYd)gYV1{Dmc^xP#nc4p~QU4k2QK6+o<3$(~4G++UhrlWn-jxpZrAuSz;-?6oMLCNocMX{jlI0Ltj*#^T zSt7}TsM*nEiy`Y7vc@v8og?c7mI>Y-N45*BZo%t0w%-!N+S_nS6j_e3PAuW1-$A-9 zl(vAeZUIy06|ze{J{p}oCmPZ!7PhaGF`093#y`oMY^YWAx3$-nkOzWK5f(^)1* zW46f*3UM<)vYsnAGSd|?vOHph_3+s2V+%k>BL1>S6eDR-)Sz-zG%6kmPvxsipkg(c z^tv(wJgm-T535JLRiNTYDG2|id*hDF0gY(STua2dtmAuOaAo1)fW1tDrKn#rsMt$$ z(RtD@V@nqd1FMJ>^`4;8i^~fHjG5UhcT^+Ih$o8sH7tGQG3_gl=>|Mv81SHOaJ{VI zkDqzFBkf;qJp5Z$)IoUv%UTpETj-T6d@IwaI6TpU6~&=kfC|@cutkMfWJ5%TN5lvg z)?ZR~c?K#DUr_NiLr^pxYD!UqisH@hkn_`ffXd213i@1v7Td(x*wV3w0dZ1i{ho3T-s))i1V-ydWm8zm>ClvhfV!5iO z$ct6RjT?t|0k6vC)Z~WZMIgTmD&2VT($AoHo#-`1$}rUVF-0a#QT~ab5;upNetkX< zF5S#9X(3F%wjf6#*1krUoqM7?7y!WZ%VdFh6?|9l^nL#W?cksShX1<9QcPI5ElHxkp^++!_4%nn93C&6%pf0N%$E1^RnbwOEps$Y^&cc zvM!?wa8Ar!7-1WX5N1~}d)wHseNdxL>mOs|K@>8C=fZ@ndc54$V|WZY;Yqqpk|?=Xu;a@pH|T~%a9TW;g6wI ziEPy4=dOLBrGbH5YJQ%@E1Tx8YM#5QRracO8NYT;|Ml6F)m`(p zyqdMSf7Wk|$EIfQ`z(L& z*qrU}WbS+?cSk@*(979Dy>iw)j}DRJ1wH4kYJ;l)oJv_BPXQ^s=2+(4En#zHp4D>E=tG$*;Cy|JFj+xr>qYG>QskBWaI?Rof`vHYd%U9rig zPzqjNCC;qyQKba$Qj?63Qv*&Z=!)? zmdp2YlKZ}NzwpID7>soDzWKE48`?HOL@4Ic%ac{OMrCF%`xjkk!gFc8?#p`qf$W|S zWxrf6@8$a(Z#}7bw^iK7y{PyCl@*ZHsPymUxK({QAs3Ne-Qk&m1j81KcqPIW|J7Y3 zk~u=K&K5ZtS!kc&ZGsfQWF69`u*@kzKo^vP zqI>}`=s9%XUc4gNC4`Rar9bgPxa^|>P;qq`miNLaPzwAvpjQD=K>Y&Krl@<` zMAeb%ky52q6c;c9v?5rq!>zyM`vu?%=yS~Oa)C+Zs%TmQRN!HPSiLwAx!VCMJ2-Q zt`s~!D!11t7I_)E3zKcfGyR}Pf?&)*9R*Yz>ZnADk4gooYy=Cj8J@QRsagc6n8FyS zVCHdAkOQtLW(;$so+1nqSsIb65fzLXmGB4JqdoFMZfMk*3})PA_~%nh!zMzhviZ@@ zLGV%8e#j<+iqJ2y(99Z3^0M_s^ViX|z|(iP%Dm;N%eS|RZP0ybrF+MQB<UYIm zOi&4|Z4SK4xZ-~Mh9@l>pEhr7Vcz|!HS|?mC|+MP?H=ej{I2EjSksYlrXypyjI%O% z&$xS_b$4G=$O~m5FW5plo7Ob4u6e|^<{@KXU33UhE@2I2Z!BX@S! zY11y3rCg$m`?E$g6@?A-^PTy?$w|A_tm_1kY1!2O2E)mqzXs}m8vwA z>J`0!;02|MbP@uggn*zlfdHWhQoK?PC=f~@38XiY-Ry44{66#j?DwG<7BvnQ6?7iiGxy^fPb!ODV4Z{0yf|C$|Cgas81 z64H}OYee=6jWwT zF9#L(xUy<}ub^Th>@|&a2bJ6I%m^xD#*CFax!nhq5kXK9VX+7VmBAQPq$ilvQu-HE zBK?|lbbFr_739{3pgGj%xMMKsu&+R))yM9SUrQJQdu5gUX?6WRwKkX8kgIF5+GX8$*$JUoIDzbcv zoJ%h<*&ewd=hBHwH&3oJZ^+sUKD3S`A_-%q%?>DSalkWs|FB%HS=9dUmU}G!uWgpC zxQ&Dvd782fY3rzz7kreiU(%26%3Dhp+#s`){ppLFm(IUo;kCEEf8!tLKk{^W^v>-M z%bu6SrMn`sCGMB&&Rk(#*_m>h{h@W|d}!S_KOs{LZ@6g4I-Nt-k(F&qvlq2@bv}Ob znbPm8^zEPAzw+jH%L1CeFL(W#_Cyd=BxNmVf9l5HKY!h=vwnBO11oLrR+il8wUviI zy3Y2c2VN@oE0ZxWZ5`Sci%ht@q|g&?_cYPmLH+XN#d29D?Oi!8}iGWK3y8~nbMfgmhRcKe7bvR>F&*B zsg}~iJCz>Ut@OyZ%O~kMRzB@sdU!`Ebd>Jduyp&HrQ268-S&mjbssB@9#WpUYs8SA zf$~@N+`q)6#n*gR`lLyLMS6U--?PV*dr#<^{OJ5C6Xr~LRo1nXrx31CSso9w`0ta( z{bKn&JIuK6*wUN{3+B&tf9f%tnLpX|`Je7ynme(yxJ_5;T%NLEW%v8DJIu(Ze#YcSZkqSt z6>Z~wTRMMR-}`I(<*|_}QdDXG`7jA86Ti9AuF^{C9<%Iy+xBMD%XTTW&9%#TTrpuMI=i^dYZ*IBfFv! z2`V<~-L9s$8c|-6q$jAzv|W*)Qoh;?FT~#u<)QyiKmE?+cQs|XpfV#2DmTcZ_XL$0 zK~T9bR#CZ8w^?Q+(`8buqH?>F6jbQ-MNk>IY|)R69UF~c4PyOqRWDLx2>Uff#*~LQ zluuFE7S|^D@y8z@4tod(Lc|{~{2XrQMb=rjd(hkV`W^{_O5%2gkDj(ZiB(io&H?4& zE=pr_g&Eub8+)UA`-~TvZy*v>?EZBJ>VA{JX*W+?nLcs1gg>U`oPq}xS5fJhbma}D zb4Hf_=SrDSSQ@XTk$07Dxo_J24+ovEI%f9-m68i8A4r#>`5#(O4#~HFW%rH^cAv1# ze&y!!(xs0c{nOI6hn05vp#+I3A76jQN6M#ZA1$AP%j?b1(?`00`N5F&WD+W=Y?O{a zxAe0M-#+&Wc`V6MI?lv`id9z@{oCe~{%e?=BujJ-S*twIV%9?0wmCgxRJvT#ygWav zY}K4I%jFcAg86w@Mv#Y=ZFv7F4^aDn)J!9$n`shth0Q` zS~4-%eoJx03{O^OFAtbd#>y~sjkfX z^4F)W_05O2-m^4$=Bxz^$`dOu?r<}Pm7hqYu2pcIex7L8f^U}=f2m_;spHMkoF}CU zQJVc=>G_k~K#@aAx2;q9{bzcnGxpPh^<0p`%gAz~kU{x6(p@SrUDy17QWBA-cHPw9haWMRir$3$HV zWtp2?xU+L{r(ZgzJT9_r?y`l`WVOfT)BakT`iIi{f02?=XZKuK(Z+Px&^V; zMhiFjkAG|<+48cEdvBQe;O(UkW=l|MTe74aRKn-gY1OI1hwR)iK?OizNy5 zOpD_A7U>G3d*Th_M^3}rmT$FmNtJJ*U?)U&+S15m$~{LJx-TzH93ZF1F?DrFU1+lV zSuE*d=sZDXkOq}V(?2_?ghhGxwIkQv`l|JYmu?+5|L%w07%wA2V~%MjFJ=ZhwHB?%Dl&a`ReFgZQ2@=!TNHuBC@ zwv>C8lN;}`)x=#6dv&+Nq*q&ciTXv0Z~VqSvLK0^q?46gK~{?o$Ebwc*|AjSW9oS2 z_CL)T{RbIOF9X@H*kE{R?0vKEedL`7{yg)=R~9_?O6iH`WzXNU>K3|Hk^SQ?43)XL zWYS@&lSl}WlWg7{tCk;Ep8KeE%FkVf;okG}Urjmu)LDoAY{Yu*QU3JA!bku5&NI(S zO{udxY?Pa`@g4PNu!B#rN@7`l}t(e&XW&FZ?WZWogDYx>ti}#*HkSh(hug&S~z#+qIt7F_}$(MFW#%| z;@vxcyNjErct<@gce53{K43eR&fV?xy;hsF`#SIJy8e{S*L>`oTh71lkFP!U&?}EW zQfgaRe)J{Xvzaxfu9(n&Z%H&%q=ldOd!R-og{3KbJ4bev)l7Pg%<287&b~J|aaUVV z(X5hsCmo%YZZHTcEy>n75tcwMaw4m(8=WHTx#yl6mBf}rO#6IVQ6hXSeYnW@hs3WT zYqjfuZ@Hm#OS7#_OJ(5}>ev#!pR_e(jJE0RR$-2^Z&h33J+g~L;x4kmE%r^P8);81 zZL4&@NiQtDTI20GR^_>^mL^m-TvJucK{A%Z-NfB$ zmb);gt@jrCZSMJfTlu-nbx(6)oA&7{Z=wEmuDi*sg=wt^-G*|;D?hrP+RBT{w{^G} z(4~W3%G)3AmN~KQndL6eUD&f#a(PzwbJr+RzE9ol<$dU0$MU1}8se`nrM!aW>z5ze zZO?pns~HQr*Sv4jnWMUcP4A=EOIz(7-hBvMTK~4riCmW9>E%%k9nzCZj@-Xmnk|)< zKbK~WlO-cdFP~O=;;_;a2bCV*yL8u9rMrff?%bqw(~6~=KGTy-uI?RFMt`I<`lF@m zKVG`&v!yX#EZw_#>HcA*ao;H4$rIlz{pIk|%V(7DdFDOkV$SR*N{e4Db-Ys^CEqb! zsy&^yU`uyOS)TYr%ENEmY94Z=IrI2&Q)ZWrzcD&&B#Bj5WVYYZJ1>^rc&zlwt@F;> zt@Mjwr6c~Ubol2>KU}pm=J@4zoHynE(IuG_qjO=e3G^y%XD%(ha!+Z(9i^AXblmjg z(&)oVBldNt{oTo3zIy-CRR@$V+`DxCex>u|!mnnGlq&9m?nNYB6}M+@vCckP&rNw( zX9txUy{1d6dDa=PUB4rr29?|Y*xi((ix)u!UHuCx+6tGpb)-=%pR34HB>96%p79UX zlGgf+-F()pS!qE<{xm9uJ(qac2wVAfef`l!o21b}K9MXJZ~dpGDZOocWt&J8$rDuM z`_95uBywbAxkN1&RJwu@2+~VR3K258W1gGdSBfWYXi9W#wa(zkx?DjzgUNUm8CyTw zoy!e%42j&^EVpi;{(go#?|oVt`;9D1U%taR!3LxszbyazoZ;?Yx8Z%)<11sCLMRqnBtE?nN-CXjtK-T!r#+lH{A7TIR)0Os~^_Y<7w zo}hNel5^R%d469-He*I?kInFpJKJ5D?)SV{=w6gr(v_;J#|*s zQ$Jt!mlGGu^KxJ8oFH z{f1Q+-LP86&0kqCX3M4b@6-0+feRiyvi-^bS@`6c3!eJ*;(y#+zUR5`$e0;2qehUwZ4Q zm(M?D&RKgdI%b_EN3TBpxUZJ}c)`2lZjnODGGEGwhlI3a0g>-C*?SA#c)sKHXG;GV zKmC?pwv9P{@u(v@M;#{TZKDo%myhVY<_C*M{b2582hG3qy9+M){`3)-Pq^`VnTJEh zrO2l>GMU7ZL3%{Kj>DY#y-PKy#ETj6q^*aT0ksDxkWQV_@) zR5Dg&(TYmWppwxF&tFkd8H>h*wO6~_#L`4|P?4MH?zOG52g(jAwqvz2BliB5hl%0JF8 zb+?T5vOb77Mvp6V_Q{z zYMOB5H$eO%oS8UHMG?yuM$?$Rz*`X|4;md^`zY45$)>ty>)d%NlLUF1`T zK7=I3#YvfMi4Q8BJ|?-R)1BYuxu+SAh%Mdu+co{$!>l52p^naFKm$H@%E@7=QJq-%%2d+nB!u9MSN zldk>RJEMll<+rcfYRZ^zzB^{;cgO5H`S;(Mbjv}r9z5m!@&7$#{24Q!zEtjN+S4QE zy?VFYbI1Jm7S5j~9nZ>(*1IZ7hg-@}+Qkx4^rSVD#P?E9+uc2#uB4hnvzlvGp^Xnk z==JR(74jL=B&(F(eetP_fA;EmKltZ~Uw`Ao?O!@&*Line{p$ETmvyv-o!NT&zjfxR z;q(NRS#M2P@Wu;EUw?Yy?H5n^{r^t6@l<#E#gsAsBNyJg@znQklncL@IO4>KBTjzv z>Yu(o`kH5MzDdTVl&3Y9%}m;*jDM(SBI-|2kzw>yR0t~Q`E1nW^H9e)cV0WKc)0mk&+* zE=JO2!v8Y&spraFIxfO3j@}VzrG-JI`&Ns~!)CM_mA1UQQkJ*Vd5XJl((Z57W{=zM zPq9t1o4B4@+HfiFCI7g3Z(V)$bI^h8{?t>}=-&D5?$p+mymJ64$q?Lq|2pA|$2Gc`_=! zyr_J~bhiM@o2A9Cm3wE&8YZ$5OZyA%^i28GCJUSB8jg>b+T5vp*)0kwe=7g=uWrFd zwb_J|K0K&U-M>`J}3{$im#f|l!HgDdPJbJ z-|v(Dw4?N#At(eDB1&h?KM9dmZ~jKGZAPW-=KgJeuSq$o*aMCH9;s8J$c{Iy&NZWZ zvWn(}jLy*A<1Q9=^==b)^=d9xdTQ^owsl(bIuzWnyJ;R9O_31Bx*O%13fldJ#EnV% zldk8`^W1|N6Mx*uLD`;|+f&iXqrsv^g#<0@jMdfoDiKHjF z-0Q~=+;!Tzk5I^kj`oxucO4B55|nQ82bg-a&_GZbj5(*a?d&}F*^ls3kUh}gJwUCbuciw%>ah5X zHfZnLc0YQ?anQxVb4=NQUQb85s{1F{v)PON6O{W2uP+zU5}rHhv83n9`pfpNUcV{-a%uS#-dp4*<==EJx8g}TFS+W9uI-rE?=ISfud5?> zI9ZxDfTT`eCW)V1t9!_F!)|qyl1x1;r?zF~35eywLU*ClN0jbQM}NMb-PrxE?iZrn zrPFphbuH`lqkI^bEh}%*vgQ68%vMTS8jHJ)PkPAJ2A~5NK?TW8|2{t(jj4Oo{CPEO zt7`k@5}ye%Qh@45z{~&K(|XFaR!|9A(y2kkRu!Vp68;w#RrwHNU*7J5BvUCt~I zpq560N@N~|fuh4Ff$pFpYgcP^r8_qG91`Y}PE98MuP$0dGfYQ3-Yn_4zFqe1=2E^Ck?tP-WS5r!G zTIx^TKgw_vegEs)j=iP>j#jZy?FX>ldy=?9P(lAc75)DFWH*%aPiJ1kCs^foLB`F~ zmYu5Y9%SZ{QY;d2iw_YeWI2{nyc;601$6h+D`5s zof3?M?inc3>q*(Z>UoZKwV!d=yqeJ~R8nJUKql;n=M8Q5%ibuJ_jY-4mF_)OzFh8? z<@uJq?Q^uW=Z({=uU)wF2SE#` z@)*lu&{|fY1e`i8N7qM`=o=7$Bn5NDb1vlz;`KNk^&#>Ag4U-;U>-nctaN z@4R!~zh^C07Fo)E_P+OBulw5j`TO&);0wfm`1|uBlKA`IpU<#oiC@k`{;$s~IO6Yr zfByCNu>U#uzZmu(ga0?f{yp>mPqY2Uuz%0|zntxV4*P!_yq37k|GmI}A-BJu{$I}F zznsaxklVl6j{jWXf3wT~nDf8bj(^DQ|EWCx81{d@9mLJ}Po4ZPhy90y|Ha1r=ivWF z!vA99{xRo&A>n_sasN{Z|BH?L|0N7`@gG9?PY3v)7WY3jjeiXOFLd~C1^b7Z{iihcX9o(qBH2WnZ@w?Q@1{87J!9Bu{tUO7%@`+!t6QZ* zBG?#L{{pgnfb}7mc3hVC-S!wXyVbW;_8cd;U$?Ma)}`RLhObKITo!sVG=UR;{GBPtW?X7fnvqfEYKvqM6U1d!oSkx3ri7xf!HJpiazoa{pxeEf|5q#>68ZsWLxu^V zk$H&?_O*j}G827Vxq~2@av;_3v+7hAqGYRukx!HNFfW+*XNkc+;uJ? zZA_}wV6d{bnFI?C15oV#Spw@9rX*OHi>zeBlN1i$`kdx3&Sq-n@t^l;B12yc6JFAB zt3;Jc85b>1{R4(_tv8o$iaOhwDbTB`@@hyh)wEHgY zh86V(f38AT$=#F1)_WH8BJ>%q5);Op+mbHl?0$ZHx;wjORSh8;*&~~4t&hrA@OuPN z&EoTz!_`{t5eMp|CJA~ZhpRUU*$+Sc-j}49D9MqQ;5M-?xeO&(|AHAa1$57^Qjouh zhG=j|f!YJQU6i_Gd6NKClLCrFlKOkoG}o&_p-E1q>UHV#v`~AO+2vb4M`nC+AwUjX zHB9=;7wQ~{az?acV~4JZmDN?@z1Wh@7@Q_5lKjY|@XPXg^Yx4WIR&+)CE4Clg3pik zmP;&aD<6|x%-GLapR+zWnp0E+8{^tZNk-Jhov1S-wb|*|-e0d@0}-z*ZfITa|CULD zNgf@xL$p@k1WiCzhJK<3JV2ByuatgI;q|%u=DX}%vRkLZp_B9}sw~y+)p)^d{(`Yx zl8U6m+76AYjGTGj2x-XVc?f&3RmNP7Q~<%DFh@X=!|nS`RaqII!NTbKQlD#q+}3KA z3X`q1%-8Fe@k{HoXMp)VGl`8)H=bb=rh+aNH3X(9WueJpDWKFeP|YIx`HS&KO0X(9 zdStZB;`I(_qBea9iabYT6`pqMdoaqM4akNlo@T5SxU&<{s;6Vmqsbq1(3eIIJ&$Q* zd_UWf#7v6Gkk{ac-o!OFKOR0-4ZZ;Rv#axpj%H1lQ$~hZ7mxd2mDl9&WIIj_mn>$B z+cf(et~H#VA!GaW;D9x@SjMh1*}87u{M3A2J?>iT%WKjW#ScD{arz?`Qd%^bWJaB< z#wtf2J7UV0O^8p7&I>qK?uwN1>in^6nc>-(av-5LVV_{sJsCDysLXx^2}UiTHP8nS7T)M(Jt@3(9wRrY z1lxE+O7wPC=6jik1FVku8P8l0mtOEB{s21a+_u?8#Zs(S3579%4%wf@H3e~P>3 zab;Pbmh4L1R;RY_h8^qUN_pJQ-M+W+Oza+=^6CNhoH7CL>Ze;}#0)J0Dv|+%m^iC1 zO0ZBR-h!(}$v%eMK=<3)g&Xc6a%1b~t=TpC1q#YzNiZn$V@Y9d+%%dA*;^R&j)4NI zEAoBo=T~sM6|B?a?5Z~+Vi`3|&;kyNr<6WxHEqmuR7e{^XP*XKzQxfbvjpxsVTdSW z(m(`14QOjr*#!?aL7DW+uv{x+fk-5m5^Q8#f&vP}{vZ`x4m*>eff%-Y!TzBL_c9rR ziNwa*?WniAN{7HlzQp%sYrZ0)jej;RqTDsJxuN{Z3a)JTi*{|`&C%llnt_rtNq&Q7 zUSkzB!tPm49 z;zZu)g2$Jn$BtQKk+WPe*Fl$@XF{GY@|IA7sR?DZVPlPSTC3-4JLWGI;dttgZBA~f z^Sc(hEG%PV=X%6~;VR~+^O$nJJnhe(-kIrKeHp+}K7I`T*o=3jCl~i8hWhMD%M89w zc67|O+_+2j?cU}G+6Q-J_8)ykGQ8@ZTC%LYI1%`AjbUY+cW&fLCBePO$5-NV4Ekf? z%Zi4ed3L|$l4fZ#S$nrO*lHtz;2 z+-+Sj0j z#^JpY(#P9<3?XopYv(~AdUmIqv}2*O!aSsEB3@4m{evgZf4f{|j*<`_oiB3f#!H13 z-c@nO*%1NPQfuj5V{KW5fX~KHn;;&rrr+LA3#y!!<>%y1kG4i-DCMXx1$o#MR<{@| zJfXCd<}2|zF0P6hj1f=2)ms1lhKxPMbefjGwnNGM<IfP;v={p6~YTfO_}!PSMu zxbaODsZtfx<1&wspKbePRpBpXOS`ts5Ra zRFu#}U2QeM%a2~iJzitdttVbI@aVS)ZaRV!!(_gRL_#k>OV8*VfMmpW zsh~k&&A`w&PD!ujCA~Vg zKjqE4X)d&ySNOcEWH&A^{)pZ@nf?jP`d}~tKQ6WB zJ-+AOHP6%JT3?Z~(DG&UdFm>cDMr`AfC9sxT9v3U7lOGC&b9A53{ejS6alL?^iJ{4g8@x+c^Ig{GVVsT@&1 zi)UL1om1w9@-dm;)~X935WnbB@)|*5@2KXy@fp6YlwWwZ*PP?)?w`KiOkMdZ!A{9h zmYiU{-C1xhR><)qkXyS5mFb;8kGTNh%?l?#YuhyZ-^Z*wy(x~Q<99uIU6sB?CcJ`Y zt&SV_O^~v8Y3P$PJAJx5RC3^#WMGs~T+Wi*wY}}YAU~S6Cgp~&kYhQ^a~Hv}Ek0*t z9y`yq<=$TXEj33z;CZ=5>Mu89K8J1He(3_rhmQp$m_(k?9OU)qSJVc1Y1cCPX1ah`)Mj!V5`+wYXX>mw% zu2$xnDrWC=chqI+LO=jbfc-s0&zp4dC$`lQzc+p>{a*CSGNhsxd$#joq+1refAq_+ zOI_O0G+*h3iHAwf*-L7R_^_>*u)(Uz`t8}8?-6ou!qcqrN%-=PzS-J>=eXlG$(@J3 z!?hc?2tTys(w7WU7ROd$tEz_~$4OqV61KRHoLyA;L_3}ZoTu5_^(f`z&+cRX;~s4= zzULvP@cVFaO)4n*&0~}1_ewViQ0hUB5^Va^z3JkBRBY_-?cKt@nO^75hmC$^OQwkG z@2|*W6hh!Of2mW4pbD$rUV0&pQxm$bjo9A{I^Ao;#^S~2IQ*8;>`0_1!tyZ%F&L6K zB$;T;*eM>qD^JhVHw1Z%ncf%dWcJN zK!J6gYo)uP$}o=6##uClQs02e=`o@wXZL0ED|2u;-V%6ut$p8@&pi86C~#=lxZ1$j z7Glh!-05M~Ql4~w!q^<{WOLf7aX*Gve9ZVqpHXQU_bZe7)vU4m3@;%z?(bX6^8&gq zukqGvII~4ZtD(=_ksco%^z)Lkmpz4RgDa{P%j#Os0DESvn7@vqqq z)zpz7x0or8O5*zkGpV`t^GQf$xHFrYcSYm%*cIpIhsSNJ;oCu}q*})Gd>aR%tv~v6 z^=AC#wGH7B>8 zba)dDJmJW^B|>^3GY%YsaB3@6LzEQszDp76Qd36Lm`R8HDTc8>m71VXSA_>qXfKo( zxBc$6D~b%`5N_2@oksG_EuQs+c?l+)-cO>WMVxSa!m3cn(7%^#}K!cj@EylIiH1t@N`pogJJ;e;8!Lzy*;>3L9L5-T@|P8@c# zE=S2WEnpXzpSGARE;(R#*b))pH-jNbRXl(4I=R~0C zuYki>V(#&>2$cSi|5>d(e9mo>wr(FRZY(0>n`ze1_x@j zFPP1}mEt03SW6VgmYgQ>bFe#!-(&K{j#j6Qmet!e_+**yKl!{rmnJ-i4rp=zL=4KW zUr8j1KYELF<>6qriG(TuPVuG~O5S08~U2RPlAkMyLN z-P^~(@RQvdg>vOEZ$$2+xctywI|RC9X9H`cfd1#isM~QbCm&i5_ZR(HR$^ zhqwG1?p!_dMluYoiFU_YubAPhmE#?PQ6-I=rp>-RPBjbJBia48OLMF8GUR=1sOwFl zjtcbz(bRpv-b*WcVc&#r-ybHA68Ej}meSc0i1&^oxmi>3CQ-1SG>nha*wNm%yO=+1A97n*@0Gy!l(KGO!y&+FB;qdjzJx7{C?G`@(VEb?L9$vS*T zl^E+*uTRVnup^}9$);-wA~tm22h0>f*-4vc4O=XD}^}6Bgt+Q+?&JvB^e~(>L#|F}Dl)bw&qy$h+m1>wGGDqsSrHaGOhUGwCuuakX})d#bo*Hi zEI5!1^Bbvg)seShWg(+sDNAU5@TblhOPZeRy)opLRi*pGogTYe?!OvW3EKDS@5_JOWt*Dt+q2a9ceGd7 zS1(EsEC1>@1gpLZ5`=%)l0`*$D6}(C*QVm#LlTJmAeNqKa+}^V3w&UU$DwKCSI( zVFz_4(|Z!RtwU`gaN-f1WaaF+w(%}_kYd2WwtZiA>%KIBFZrs(=5SZwu2|ZBWb(NG zR)=+SbbAYM5lw$+^F9`oXAFpHGU!U9KmNU z<<`$Kz8_7STYn0lPuSS8opzb+n9H|r2;s!dR9v4%?k z6zCGPjG%*Ai28Y{!_lHcz<_nN__cuj?LkN|kyfxvNO}S2g!9T~hPfxAr0Nk5uI1Pifi~1j zr5is-oL;xDQr27g(V~HMvuS+`MfJLs!_Jas4!`d=n_GOo-QG_$>UlT+!y$ZfB)WQ# z{c$z#obF!b z5ydHQ-oANP1C*c^407C5^EMxweK*wc4oRk<7OK>P407W#@kVGbtyC;MK{=$&fFLEj zG9iwe-;`iN&To(q-OD*i9Al^l8;UP3QGAly%PMM%91py03P!D})Ix8~rztLq|i_G3vA>CFY6`jYx?G%qWjz zd|jdEAsw$qk!5h4u}qT$L5k-YE!80)nB|*$Tnf2Qd2niQ^cKK_SnQIo&ea@%ybB~1 z#=Le2O9LxnkhuOBA-z8xXL=L2T~uV4rLHoVcXken# zb8`=g{L{*LMuwSWa~WuV0qJ#S<6EX!KA@vSnIJ*#Bu( z+{C>#+0IUac_)?d=B$ml+@DqeTh*9n5-BDQAM8>b&Pxf6T0UdDlqJrKdjv;+v{K5F zatoTFfcDA@+ac&&*@=lVDF$>t)CWF*kWk}v2JFwL+uW*HkPazj@zQAAlEOXWxqb&> z{{)t0&=+iLh+O?R72UFw1iTL?g8(3GfXeTTZRy@y!|yBo?`Z}I1uIYX6=W6U7mjKN zf5&qf>=FX@%zW^(Rq@h<_njN5nH}Y@nXiM#9j9 zo5;Kx!hUEdQUvEeE0)I6OJ#|mBPmJ;mMfWJIJtB~N12bB8}K~9Tz}CU@7Cstyvl0K z*=iH>G3+>e*0C$;#wyME^k-t$8oO@`LzEnzLU*X4Z$Cn7GLz{<9_e50EKnpts7W!L@$P{>AM9jWa|w6qk{*N0g8{x?LU~ z^o!LsFtQ8_K99*R(zdWg&-1JU96O~Me*g~7@;`EoOIW4y`24Xi;T!{ z8g|eP`a-bs0cQq5stLT`GZPiYif(~BqrQoIup;zx9wE9FqHW9EP4UAQ7g)TmShN+p zrj>mCoV|I_@s3AI4nA3ob$rcv65P(D1{gWSiHYOHs}S8kv9S+n`GjzcJn3&E%sa94 z@A+ry4_C{uqQG^E;6(2INu&{nPq^k}s`YHnG-rF+^aiRXfTV^u;Xc<~N6M@~e#y%u zmwF#i!MmOq+6lR!3`0MP_FY~a^PY+lB17v6;<{EIMvDOgC&4=ILMxx+->TCJQO0XM_$9(x`fxVdM zELc7J`^n)~{Kg7<~g4U8*J7ftP7>$y4mJ61G7jS5B(k=GxjK`Mec( zM9t^d_iBo#CJ3#m3d7~bkyDh)XMxV$u9&p8oNCBDdCBki6|%@%iRBW~aBN_*3pTUA z=!ihSOgGMIQGyxx0E}pM6%Ro6x!DM%nansXH~?UN$5Q@Xy56oR@6)n0JnhyzDA0R- zb31Kw)t&3I+~{cU(a-HR^jq^tI2z9a4 zd?^tyc`%W5p12l$%^`)D0M{q(pI)AaILb4Bs7Z%FQf%{DAdq3dUY#}o5#1V z!JE@_0Xnt8POol(M6!A|z9(;ful61o@}q2%W6t!}-k*%D#;qlfUeSlU>E}ue7+C|L zN%+eb0RM;y6O3s0rq}StY}MJ+6OW#e;i=iorga7~yL`5)>WZX-HZ~JNz6cB@MKy{( zYmjyhbThvO0>yd|-EokT5Uyo}nS?C?q*=;J-@0EY@n#$_QUKU=6M5A^^!TTn7(wNrcg3&gm^b& z>ouGx=Tll=)o}2m$N_;qLabpoXLkWE=oE-ih8ZNzi2x7 zXz6~#ZKsO9Fp`1Unf=FIzr!QeBB*rN?(!yt42kG#io|lJ1hw%nrGTwq|y(^1iyqfTVn;$FO-tA{sG?R3g*-Sf$7ufDc_!#C^c_a|&^8)-czo%l>#Bh%Y z-OD|8QYGztz8hGs=A@bXmyOP2lrAAB3wA|txNrTSLDcjJQE~^K^&>j8ix*W~cQSeKK+mD{o1*53Ks<2!Y@oI?H z8<9i)jR0WUU3^1A0UbXWr~(njsTl5w$i0@P{P^dyyrm; zwM+3Zko(hss+L7RTv$~$Rao_lfm{-ZHpv^0Mo#o2*VqQ^OwPc&{CeN?d_N#Md_`D7@=Vjrae+kUn zpWJWmGlT{?^*Qvinx`n}`Ifg&UL5}hybtclo zjyt(k5rgF_bfl0U#+PKe6*H$Y00exV*0@bjjPJM0F`E`-G&fxZsyIv{!pi|3xI`EJT zQ=@f}?{b=vPHwdbu4NJwl5!X@`cx1IaBYk&euRb8-EWm)dE8B`V@u2OK z5OBT{+HvLn%(le=OZB(Wws$Ktbqr&Y_V86;@~+Ac;td1TmdK(6M8}++8;q9Aip#tX zSE^1YXoFq~9?%7Ka}Bzf!{R;~H4_?Aya%Z8V^47x{>4L<&H%;|x5O-l899?Mpz3V&Wu)IGJ7dkV^g1>%O(F2kL;gF6hy-6(QjDC% z71z(fZ3E{1tDoN-){WlT+0FcdrH^@{E8+x4=vaOx!k6<9B(g4Dx7^cHrS@t8F(fK! zT;>%r(^02_M&;ue<#3_GIFGrqKV59GPuU11UR{X~qPS3yz!v;Q(}uJS*6&tS)s=r5 z$VUMN70`eDy><7=eADC->*jB=HtjQKX%<`ho_+it7r1XK6L(EB_HKB{7Nlx{%;f2I zoz{hNzzkReY&X|bg9F^s>v7e(8p&=C-afZZ={en^t{pB+Vd~9qqhp63w%#%qY1k;T zcSsL-4r1R*WoJcv`PjSmXrp*_vA>qb52N8+OpK`Vo=`j-Db(urwnXT}B}blneNF+V z=BMRoTL3}2x8>qf*8JyW@3IQeBNt_=ehjCA=Fi?k+Tki0Z64e*fS3UUol}n0#ql$V z2E$WnD+oMPP={wEke&FrN;mgtr&cIvy#$~30Fj5>N`FKfSL7Y!PP(r5Cg>Y?oilsN zrkaUI$TWnO@3+IJ<&uh(xs1ShDtl;BjDV=ZNzd-|v0~aWoHpkYS2^0Pt;Opk5ou5Q2)* zu^bQv78u-i8(tM|KlBZO zuRm&c*}-zD>YH+<8zFQKeVZu2@wKYLb8WWu!qm`M-5Yi5cciPZ>GUCO#&#(`0Nw2! z7645Co2^X9F9Sgv3`Mpd8Vy`{zzaA)I=Jd(vUd@atO_V6-M!;4lwbf?K( ziCH>?E|>B!*Z>DpeZq5}mnIT|aj{2?*rsOgv~JqGKL|o3vU8zp}wa z9LcGtV?;aX>>y}${#vHak1AfN9Mw|!&{Ey0lK6JX#4NMfFt1$DcBN;~A}B!2Zod0H zwQ29on6f&V9X;P+YB=}W(b;~lDQI5)W|nY(J&<=nbNslg z!q1Yu^-qT!q#$&ihO}J~EM022tI6-GupW$x; zz%^Aqo0kKL?BGVlvQU1(-Vp-s^FepX>PeE{rs8rz6@fLBtZ=#E7ei|2iZ!Zk|< z-v1l|*W3lYsHB)7kaC6G4?yJN(nVWB;8tP*56(j%ilP`hL!b+FutRpFJ5t{h@qV27 zHsUju2D+JcPnTH34eMuEVm%HTbO(bBV-!!kL0Od?;J6dqZX~CacUPz<_#VK3x9zL3 z;0iGF-Vi`g1qD^E60F8M3<6=f7X%PnaHut~2&RIt$K2cwc!&Yb>J15;671m^C~;Zj zf1s4II6+%F12nD7b@u%gVs)q640O;etY8zQhz3ztxDmZ;f|oYG3ts-X^tg7#vnrv1 zMDNqFbs2Yg!)kU=&P+}PNC;{LMk z_wzkFKEEga$JsZf`iY$bH;@OnRUcP`U3=9oW&?94>6^6m|JtZ{BDtz@%`$FGwCc4J zx8tFHhF^Zi(s<=?$9@CVMdY)`qN$=caYI4#EjGgXCt{8f0al~cd2viglUF~&%Eyd{ zcC#M8CB+=evD~l#);b5+D6#=rBoI>)IUyYG4hhB$AM8$(uZmdMiw&;f==qu6yUb(y z_7d{LQS6|+3IeS8c9cYEy6zUX_Zjg{Ivxcpjq}9@`8XqDT`cbCYn6w)@4wMxpws%c z^t&#~ygQyNr4}RssvJHxayao>7y!}E38o`ikf5Doa_YLpWry)KMCRjkbFg3XEr|C1 z^2B18P^LAvb?ee3DwS<;?UkXhe(Nh z$6Uu0oeUNGM^#syx5x@*BxYXic638kUjb}S#9I$V3NaP~pfpH1WTisj{*yYC&@Z4- zII-LYi@s6WG12>+WO+5qBU}Wh#;s5itdoptQ%TK?=hw>yt@v#gWc@NAxo0H+K%`7L zqM)8Dp=gKH4Z))3yiq=<8#iZkoWi5^@HW#~phl7IrC0KBLv(v1C#v!0Hv!wdXT(GD zOP+JnUAs-QVPpF&j6?k^*4ut%YNWXFo;?S-wUt>$hpc@CKYz1T*a!VJ+1=E-&wf9B zx<>o@co_@M-Rmsj^zbFwiMT&Mtb9+jEH1xNI!J^CclVkDVUTaeW?tZ||4_0Jh!^y#s5-)dF zf9;ir7H2oGn~8nDH64zWX?7#QIZU>927^{GYQ4~t>=ut-Es_kOAbBA5L!b#38?Mh! z{BWCTp(GfO0plDFI$NRUV7RlF4j}thqfpTs&a2=>Jc@X5e4@Zer)LX9g9bns=M-cl zg_wndo-UQ#u5O!MnmP{b&$HQ&1nJJa{o^G%S?9fbMP*Iq@}B)(_~yDv&WLzQdx7p- zgZJh{*BHz{Z$*SLNLzIDDQ&r$FI*%qc-p$?cQ++Ss>3<4owcZ@H?RIj!^|)$aEt6+ z@pl|2Z}jRp5Mjf}p%O=S_ZzTW+`cP!Izr$d$!HXQ|1zL}PQ1AV_y)stI9Le$Iu-N- zr`s>od-ISG__Rc?HZD7U87q1vho|#v@P)pqvtXnh+YBi!*TSvR%S#o`2ZQU?Jm!Tf z^)jP}1$JK_pt_$#IE5VHma-T>dI1bOs?M~N+ey~)*LdyuPy=Q^>i^xhMd+_x|q)H42IC3(wUN8~7T~J>n zh-=xl9Z`#F47sy%-E^qrv)f3x*yzLa5P2=dObI|C-F7)0x&$&Q`?h@20Fc(}YN-1~ zzv)|!Yf_~9TTOT~(tDwc2@If>$oN_!=YoW6990593}Be&tjNvf=}2rZBP+6U$chp` z0WxT;=%Zn|2L?J<{~!WU~61@0R{=Virm<8M{Mvd>u zTpp2F^2il2p;)pghGT@AfbK0;+qE3c~Uk_*?0TPzEmN_&Y0kqzQVHQ6nVJ&#)k!g7cT(FK+ar zPO`hOx`WIu;ZRs`8+VKx7r*V%?~U`Z%UCWCOk+I#fTXGXC#uN5-t1QVoGY6O;skzj zD+G0asJSHTGg)r~ZsZ`&tL5qo|I@?qge|KR)xoBPM1sCbl&8W@u6Dr4J^~ASk^PcW z7NQvnN$dK>)s=q78g*$+M%L#AyXVmR&c+A^+g-~K*Y_=e_u*kW{y97ZVRsa-RoL#d zy|FBJ*Y?A{dGF}RW=5H5)N*=@%e1M^;OcY`3z&xhDA8W>$wXY=lX~IZBz=}y!F>wA zz0$A{lrm!nv>-`$5>T$JJK3!gvW5Y19$Zyn2q;L?zSa+mQ0F0<{F@I4Yyt0dUuYBX zR_fjplYN`OX`+2Lp!1FTYm|7k8ZJ4vYJm*CTo>_wAUM)?FEWpF8piuDoHKQZqz{g= z^Wqko@5UvaIAb;y7RnzB7m4Tv4E3oAcN`2R_Yf93`3cpX8L1Wf;)J75wM*c2%I*%b zZspW%hF-pTGJ`Ur?G~L+!amxk)~`AOo+P00gPt6CzqR7YMohr6HD^&p(8j=Cq3h(h zp|l=~Bdlbvm6uS{X+mHvcnGQl+MxJBTaPu>RIS?P25I0z?%Zk=!d;gM$O05TJwx_o zh!Bq4XCKwQpuze%_74-&fEIccF!1w>gEJZTGwX<~lQ?Y!X*)#E(G!3K_FG3J0p=e9 zI4C6oP9ISsSwe#G1pzND7i7J_UFyHl%a{CJJASOC+a|M=&p?U+v`tyUws5^fibxIU z?tV)r%Ir1_HfoMU(w~kydQ%EO!>FN}7ASL5Ilx^rDcLgw>`Zp#CJ2!OIx*PvEzrF0 z=>>E|{+x@*4=fqI@mQ|3YaP(bPQ^-q=VhFHE9lU)e%uLKrXh@Xe*lR6`#ki>%%y7( z2yxAQH(GiNF;6GP4+(otCc4*telNr^3gL!cWeL2QFV~4$JFrz{?wJ6$yGvv#y=(aD%f`pr}sw zE3?+BT^opmIa?`fiQ*xRef|onh_fNmQ zMA~b+`(oCSF{e+7{+ALu>8E9acrOSjepBC~1hfzohOUoRFMtB2w>M-~EV!*rpeSDn z<}|Uxf{eGJe|LI|1Id%F=Gu+by*siCZo`>~Y?b5+d(W>=iivz;-XP6EqjN<+XIHxv;)g5s zp2Am5C)<;cs(D0TPZA7I;V1;?nlU1`d861WS_l1-OF+8Chrw@I?XnhE8_+49Ig2Nx zPY`r0;5t=P00yvC4Pes&dpuyR)=rKEX00>>I>$*1bkZMq=wObvB=00lg-2_*iMW<-nyZpk`)@m0GOWC}Bb)VF zFF#dJ!#8lnWbvz2!WFHSe5#}-MngXH>DPC8Xg~fq5e~9Obn-24L3rm6wVk}&uDX(; z(XT!udD*vh)=%9x_|hxpg?+^-z%HTGLfUjvwMY!U8q{kg8s}iG=wSA6mUSiGBeI`A zu~zvN@@m2GtdylM%pf0dbs~7CXuU~H@tll#1OfhmMb^^^Q8R)n{y8)9{{$}bI9ua}9NVMmk|(D52U|4c zd^T;^hpiae1N(Z^@!-u{uit1dKTb+=GA&Q%s-K2zt$fh?-r6hgQ!n2*nBs+hj@EWP zDeG3-g~WU#whSFXn&J9A)&4!UH~;P|fXZ5~E^gr+zDe@XIJSb54B6zDgE_%HzIDrY zQ1UVx8NI}kg3!Em?B&YEaf9<}2(#*4SG~L)ua4L027}vd)S2z>zjo{aUpC$zDCp2Pv z=l0aC5G+IENAx=4c^8quAWg!R<8NAj%hX4b`k?wK_r|;|oSSjeBuu~eI@9mH^RZ6#CMZMJ29 zfXJKPNf-0}aQ(}H^U}SPspgoVp`IU5+8w8+sy=+5t5jcYZ{IFXlgc3`b?D1$SiFb0 zjuNZ0jP=?D!fj}#e_NkV;1WBz5cz`N0@oV59jD8!HEKI7jegY+4N6&$QcWsn3-XDJ z=x=jO9eky(^TS$M76HEfQ{FCybNp)oE+$KHU8RMhnP0u9{lWP(76t#0^)ogK@`-1N z^Q3_~IBG{HI3if6JRnhIEK0W1Er2pTQ|=GeT>YsheiO7^J|e`ffKH&o@q)G~BGi|pF#uygW>1|p9nGx?asijswF;!4GIdVo!|bea?7yYPzEhf0@`8#&hGN*JSG7@ zC1-r=UI?o5SX1mzaQGRpH&zpx=6>#Vt6c$H;dR^OfUs355vfS^BKWwT=o10B)7k#o zN!i4N-r)Nw8=3DN1itF@zl#efsM&gstR90pjpqa-^LAC8+o6a%^Ury(;#V({ zv)>_8`k+R6mg4*!!`nI^kXZ?c^Ati?DK1_KggyAygZ>7b`r_@N5*xM$E; z7u5KnEuUxkpq?h}%E<5fjk>QrjT$=N;>Z~xcXY01zA33adiD)&bYT-J&kLP|TIy^F zmos03Ue84_Kyp$1H{o$(w+qy`VZ5((_@$&2z_H~?QRov)F|NPLMd1q6(9A4+VWC-8(8IXXAWSvdr!|`D@(NITM?RE5bc{hWjN3)42*B?C||D%~C#Y zuv66>Q6Bo~W^_i3g0H=*annp|%Chpd&&oI!HedSB|j$7JPi23+3f&0l&n zBsWQ)QhU*{)#KrPWOqY?)0^@yzn!uw^1kaw-uJkRy%%+It&>p8W&V(1_XO*B zS6EZSYfgUXmyKv+_A|E2lQ5FT2>5T_LYH6U8lT^;J$%`ASo0D7D*^rH z?5xaDLQ%+IkHRxaKkk^mSw6UgoAHWgP^%+jA1vrQj)1Y6(g^ohe&neuj?P2_P zJ*ao;@@Fnwi-t)k0!g0RZ7*Yt%;&s$1?nMP-OjLl8~I^i?s&x|$n<`ba#nsU6$9k9 zPOC^S^-K6qh5iznxtm#80HRjT${u05h>6aN*}P%eR(zNxGyc?_o+u6b2d&b^(THnw z$L1wmvy1+;`xa`tj2mRO6sq_m9-1Wj-OH*s@jHR~_~Pgc7Ve0GMm4BsTMdVzLq9GK zvXy1R>^m%4ngo5yd7EW-N0D-?kwIH-RYV6JTw-GPZkeG`nE4JlFrvpL9(|+qm|o%g zJ1=p+&ia48`zetJs$!f@|7$wTJ{J}Q*~8Sk-S;SZV$|J@(@^kWs-?#6BAtk+4(xT;XgTH9%itEPRp>JY3{65)~^ziQ=?1Yt49FG$;JJbNB*Ij z)jPOgd2*F!_&HZDm<1O!25NA?yOj6Sy-!n0d1zo{Qc$|J==F?X4%x?Q$l-A8 zQOTASj>bDlBzvV~l@Rf}k3QeuKRq7(!Es)%=lvYl^SZ8E!U!t(^<9#Z$cCCk;lj!} zFM@aDibkaxRVU!E7>b7yNOZl(xoe@?6kQ7iELSML*G4s-7mm*5-e6~Sg*pkl>y_bg zrOL(-Vx)QV`M+S`msbxm{iF6ou3JM=hI{lNU;OKm$z9pmMjE2IJ6u{xBsj+Eod%!j zx4^Q48ntE>W;79|=grf=*r0Q&V2DrcwQLdI{R1ieTV0YLwZqz!@k~cs@Jsx2alM>9 z*d9Ud@4&B}6Q9z1C~#H~dxW^_g@8t$OsUJ(R*CP>u-K;5owJkdj zu*rZFm5Cgg;t=s5j`AgZ@9)R51=tO~;XUn^!smRXDEY$;FnXNeI$EOI1>gmypSelb z4o3HmAH>XRjBgpq$J4C=yvb9Q(z$bjJ~Wxx=Y3S>vsdC}YY)fM01wg>_w!a7xS_Da zgx8le^Qo&gOnbJfgZ+prRA#iuF#A_q^y%pLn`=`0Il?rEr77>>_hV%fHPXE8YqB;n zjloBSPJ6#dw7g_h5L11M-r`6nw3?oD7jYx!a>t6<%)Ua$O; zm#%oZ&m8;Z^=kVO`JIoeVRtw4`e$V}4P#W=&7RIFX=teY z6xFJ>!aWo%wbwY2%O0k=pI`jOQbkdsR*?EDeo}+{LRK90PE)2?+c_~wAu<%I*IkRD zY@(y&QQ>A+6dF*?RCMdk8Y?+<&BX?ZUp>-L)irgoh1%&Em-MC3o*en8vt890IW&i- z>F=iog~?@GH50F6Hb~I&>qqXi zhC}KPxHi;gd3DxY;0{l3w7Q6~Y}a>k>pN;^%lgNC?&+G=+a15MX643mXi(~J^IeDN z{Wp=;EFKTj+J<2zYz&~NnbYHP*#(vpgbV)PE1&U_B0u4|#_+CET?C(nhADFD?)@km z&?QLxp4=oy-V@*6~rV>%iaAY<(+n@v+J+LI=1T_ z6`dF0!U%tnsf)j0A3dHx|D&DAb9;VI@(cUe=RTK3;@|hb6ni6X71-Zx*7|UM6D+W% zTRDSU5xyJO@dHNeLa_ObbNTs>f7d^qIOTYt_Cq=$eFBdC{G!aTKJqql$o|G?AXwp^ zYj|55uvqa7ZIKpoOwDg5PU>-%pXI}HoA4)uL2>1?mp7YzU(^)jXTG~g_q1r8I5s(v z$xA;nUr~E9s6!$FoLC7o>a?fma>gyLyq~;h0~S6I^Q8NgT*!(czZdUN@2talNCaU~ zQ50O*Q^Kj?fPO@&cX2Fb;%Y`s_-cS`#dCW{Z>XdBh|~F z)EMbq0yif~`J#-Suo(~B9h$N+Y7H<&g?>Ss2gP={rtN~7$CpB%T)B;`O$$*wZNAuZeOhx#4o$BMTKb~Pu`;lT% z_3F0Q8;)4mssbK)b5ORT=py8L)@zjY2Rt3GZ7EDZVj z8E)@P>np{Tp|djC7F|82N&-UVOaPRmvwjE9SSQm%a{<1y+*Cz9 zg*z-9MZk1L;vFC@IHv26QDm=m2EI*`OTxtlXAqiH(ChA#XV+_6xr#qB|FLu_(N@F2}Of*6XC5fWALgIxnsI+%arEp=1S^a8%EU4xgDAwb&tIsd_oo|gLk zALh-Rb1!0u%i#f9Fu)Oghn$OV3_Wk3(rYaxGj7Ufmo+n&wwJ&7#CEc(u6A@ZgMsO5 zfrI}WmFlh>l;WW>M}00i)Bncq@|WhkuYbOZo-`m6x=?+_R z8IiSfLAip3M`EdmDX~QC5ExS$Vu^2V~Jqq zVsR+Q>`)Bd^l^Ynl9hFhv>CQ9^NOJ&0;T%SG4lTJEbDp-kPI*qBMCS#X({GA58tiD zNOUCNoXCjZ{bhbr85)qR9d_3;pMG;3ZpUX)wJx68 z`jiR>);uAT(fo{g9v-9b1o(Cgf_5yo{=-*Mhh%EU0&Rjp$o*=*$XBVLL~eZcxscP~wC#VQH>QiX{T(&y zdVm-T)aIf%;(|$E0xeU-ijBEXBcHp@xWJT=99e*y;}{s`scyaX>%ER4bo%pUF$W0N z!531UK#awyyb-@vNs#Pb$tLoWdf?ZkQGxs*1Mi3GhogAKtqkm+i>b%<%6CGX=G4v? z^}J9_z-o(k$I*g`XKDLH6J(k_Q{In4DDjgsI9hgW^< zX1L))PxAvwQX)==r2QTxdsokqPaqpr527*#xFdowYC9_(;k!R`V;*SDFt%Bw=flSf zVLvlGaSl3jr8(wSx{Q@QpLn-ieUB^9V?i2~9S57aF!lHs84)0fsujoT<5Q}h^!-=l zh_C-x-Av%e>p6I48q%Ly%sqA&Lhp-nb=g61UR+{uwcjDLR(?OJht*vv$j*0)<2Y-^x>6kJkN-VWk- zt_L{sq%l$)Zm6w%9Zux_+@EqI#o$Tuf;uIK;aSs@5Si0=O4A=Ll*YE4F4pL}S3i8{ z_h|G5w#Qk$tWpo}bjiILrmLsqt2?0d=)8`GT|7Pi_fU5;yaKC8D=Cxd+ZrG}^(u)3t$I~%Kcy}>tiEI++-9sxj#!$@7IWo8H)p&3gZS|9ZZa|!OkxJl@=|49WsqBC+AhEj{e z9e?j*2~|nkQ!rf_gyKX1dQ{ojc;2B7`Lf5u2A^`o5;Z%X*ps>WWJ6#Kc!lxHnMvt) zxc?*U=eQg+ij@SJr2A;IXm+|mGZpRRnwtFb1e^jn)!{z?tau2(@7Us8FkKGCg>Kqi zt+yM=kyFi6Sc$EVdT*%#?#SnS@GD3E9g%wDNpG*KQHj@cx5P~wM?MG#cl)&B?`iCi zKFJ(2&JCW+&XT$MGhg2mY~Q}{g&t*IT`yVZ_Ex}b-L%geN)u7O%=za`*mqUn=#*=* zyHpj?3JSLG@{h{!zu%9=#sA>#-Zt!qbE85{WFd2Qr&gv|ebcqGs{`^))O5qsO5V-FVO^1vB1``j;^8Ig%Bf+L^gXXRC_4Kk~yht(?e+ zbOdVtaflpx_EYQbdK^0!DJM~UNeg1OIVnz|#>UuWetJim3AP1UhyFD$;*pvu;28B$ zXZRlvO6Lt3K67i;>|#{6e^vFElIpVs_m6OJ_(uH3Xl)jNharhXB9%yS5N1|=bWX@a94|b2{OnCIBiE=(|hc$90YjSYE$sf zs#(umI*=1Z?oTeD`N5{zcQ9S%1r?xuL!d4%C-Rb-?%LJw9gHO!M^f)pQ4iPRnq!GO z!bntPEG(lTiH9_I0sJ&bIC)fMn&nGYTt&*pDy@=aoj&fpa8nn#`JU3yOPqZSt=S|h zwN^8STezxZJ%De`E~lT6Db+B7+-3Z7J*3@z!_NPQu9#Aqt?A-}Cjb8*#f-nOb)MG^ ztXzlO;r&?q#bs@XcvP7w$*hUGsgaCEt+!*agJn$%g%7wfEXJpsslqoWB9@U6wx*SF zL^Q+j8`ht>dpjO|!?8Hk z{75sAr!?xXLh6&RN5}+&h0IvkCvQ3k3BKe^SGek9!dkou*9E!KCvA>@ek}|IWlTwD zu?ZVXv)s6+8#Wr9rEHNAnGJc;hh?x3=bb`+(p!SuFp-G%V8Rw(d(Hzt%zcYDe)ywor$+-iWxKa^FEW0xcGno?d0R%xHxaSFVDOlZwQ(n9dJ`f z`M;=cRblBNhAE@Faxsz4DCrD0Kg^T5boABsW=DbR2Qfsh+$B%rx4}+Q|eli(pK3y0s;Q z$W5e(L;;nb%zf$rqID)7)-yxVPK&3X7~xixfUtLu){L_$<_JkV-Fi&I zCrd8bi_I(yxB|}sK|+VYQ()eDcqHuaAE%n(S2Rh1>SC9tOG8lG=d$l=j*COI_?zRm zH&*-hdomD3_$>!~8*G)1%DtF~hnX8!rc*_qDu3eQB=y`pXULiRq0GY)_M49wDS3hm z-dk>^Vo@{CtO&*>MW*sX|Cnx7MzO3?c`!Vt+9^*aoE=x}{@*Jq1^^mBRa&S4KCDeu z3B;&YMM6D5banB)KFLrYf6vm{yz;$39b0XQ+wPL0`|>nFG)m+1wqSbV+HoHViiF4O}Dr_#v2BCr}s6FER?y zIF7R`ccPQN%WzNpwh(_kI$RB`JVo+%X!O~_KJcU2&{!CdHK|Fe?v}*jb>}(0EZ&V? zu=Gpx-?^A_vg=2SC5QC5MZx>7KMJT3<>kE(XuGkFX_1;RY4%T=e6MoHNyjys99vv2 zXp1$?$)pL(AaAH`!Y}>HxxMy~*4X_CM(3RKxBo<8CEQ+a0LNH>`+<*cJDoOgA(NbX zWe|z)=!kL@)HCxVu-M0 z*Y}z=uP_v**H^t?DJ*^CiHU`s?`xs{w?Bk}uo{ksy<&rlp}2=Zr^zZn+QG0?yMU%E zM;;6HyFwFS$Ul1G{8=jqtdm-(p_`L25eYSlrlmYG)h{IXc4k>9c({)$vDA*~!u^>%?+ja{)F_wfAPF06vtEf|#M|5s_E{mqt!7~rvAJI=hu-GS z9s_pb#fc?x#8xF9-Ur?AiqP-<#)xIu=Tq0mn!IJZs>W_@-_cyQe93s~fBDCQ97$b|)} zQh80)59{~JcR9(*IQ^@;C)xN(SbjU@Qccih@ba;Fq~aNRK`;2;C?^3oip8#hkefh0 z02J#GS_07$S0MmbmIOsckA%t$lZeNKHJ*Ey(!Ic|#*@vIfkqjC1p@fRr}DXHekOuN znLF4>4_TuvEjLcpzLWlrT2^o1h4bEK!AKOX5AMq)oZ=0%HliRh>R6a}vy|VcK+WGX=b-mNq4b>?}aKLw)ip zXA!H?gDvjltJ=SXFQN>?>CS zgY)-CH{?-k2?h|e1GJV1a;26Qq&3DUyf#LOK*XE8Q zDuqnQl=(w~lQ#GtAD7(wl~m%oe+fZP0dSZ$~ zXPv9)^|d~zz!J2GeK`<$pDv*y}gg)QXB6Wo}tq;Dwh`)!0*i6Re3)Xg@TQ~0)LcfiiIi=Skh zhEn8F6Gat-;N#NH@sQ%>8mJ27$R-4u#fA%B${f#20=w@fCCzMLIpAHSS_6kyfoB(k zWm&O_qt869C;wg^VmVta@BLKJ6{1OmKRY7!yM|@EW;9m_&fHkj|1i9)=61_&^N+bY z{k-eX`fO-7T7EuSR(IE!`u51XS@-oRt@m~_?TOL%tNK!9V9_Upx`tf3_W_a)H^JE)DC&sd~)j<_BVuk3?j~;cDX& zg|5|Iz_fxV;)_REvtzx(tjmW7#_ z=~i;wGj9@0&z&tY-FG#-Y^CuY{VR&=pP;tbVh@hXcB~Sia6J`$S8d9+=sQJJa6Ik+ z5oazyTBa`}l*xpRr6+*8QSL@2JWn7Nn2P5|&U)=6;2u(|O!wQjbKN2|VI7yk^=*f5 zKj+`=&7>JumHV}eCxYdGNQ#%3x_ztr;QrQG(GHe_ICfC@R|9$sIBqV45jE; zu5Ft)6ulLWSuIgCNO8O5RbA!MO>zw5H@_G9byHbvQv^eAXHv}}!7br=nZSgOP`s5J z$&8WQuh{ua(zAX@_=eJJKRAA&5}cDHpVOkK-qRl+bc(RC=1gN)(!QXpUcF=EBV`Dt zsbv{HBvDt6#O^EE^1@V|HSi<&dxAw2xQ~)9u{!4@hGBp-xxqu zt}tx0!2Y>P#`)i^X+cb)!N6f@N;{tzB-Tf#jazrSC1zy)SPpcw{01oG5pEJT6^}l- zmA}23_(KG^+1pJ|&y{hbRWVbgmIrvtC9`^E()`;x633C-YUZYi=2xCoxqRdV32lmN zKMLM)ZORo;f5lR5OJ5THv;VU8djsXA&JD)8?1i=AEs*B9ZzdJt`UgJ8QOn8=%n?}1 z_3;)2N=GD+7yN1e8<}_cnH7$$xvakF!`-d?_7Q@_UH7V|bu|B^pa}wXbz=lLpu;zO z7ui-P1eH$v8vA&c>TX%hy~AfbyQF91(EX*}|0H(k^z(zo4=}ii=(On9nI?ZwHEYv8 zSo_=Pdz<9DIqz=^aso*IL>TGcjy=ghj4d8_H`lKm%R)p{DEdxe>ToX=tY5W!0wLz!I{DZi;9BE8)e6{yM;7qMpp7LTdSp zlB$G1dir?J;j$?Y=!pz32^R+&JHRRcuOr66Xc~r4vc+Ud0qAu=!GPF>wx2$80aS5g z)mxMGpkuZ7DWe7q4(ygOBC*qfbzNBJ@v4TNj=uvDk})R}&j?$xowE@b!B-2I|M+8R zr`)0Wc2^Wxf8W5JC-^~$u22%2wh(!7@g^qtQuVcvlTXeVs+ar&Hj@8DTuWOS=mTQ! zH~b_iULsEA`^0U$l~XjlfuHnTIATJTAy`%1sX$0b|MCOsow6xAt0kuF2wl9Mf4aKQ z!M%$1dl^1^5qnp>GPG1v;Fdo+2NO7AX+PKduGol+!z~|OuFRxbhO+9bO(!U=0k%Q(JK0Lc zDm3Wy1Lh8?GY$BP+HTMu7Z?sT*3OwKP7yf&r$~Xup1(e*OE1CF|1201G0;)cuye#>=ptz+- zil&@lbk@wT0iFgP5AYG>fLwev-u-H^uHYW4MJT{ZH8DhDE}n}WpQ?mn1uaRcPwdoV zUVrre-eHNT8V{?&2L9HH>VPi9x!aQe>xO}8L({yM@CefP5vsR(@h|PB#G4Xy?|sDX zrCcC1^U$dEeVwhO!;bZMUwf`Cyi@et$?HgYWReCrPZfdepB^_WjV^9}$TT$lAeg`Rvc`>+NN&Q@<>1#!YQ>9*R9%GFp`IZ+<&j zAUcJKrZ{};#&f% zB?Nq^oXCDJ0CWf@HNpvV{ujW>5VVe9hnOq|4%M>!*)!-_oa+C*QK@|rNyy6K0V7B} z&BRAr!f@gUmadR&ct`E@twogb)$)>b-^0$P)CItHWz_Qf3DAG5@VC@h>YZBpiaJOM zh5kOfi$ghHX7Y#KS+=6x3wI zHph&cxd!$O)B0#GiJCI4fKc`hNZ!TLfn|mpxa*;dPq1oh?m;ER(xykNTaH+EJ2=Ac zf#R@2N5pg9a-G?`(X8^eX`!!If2|G-TFlf3ghv7513B#n(RIlbCeu@`}2n?hfih=$>k~sK3;|2 z{l6y{rj8N(pBL4%y0EZ_-F5R>`CzR17Z9`FoLAB|U^8Ux_z?{YOA%netuv*0iJ zG3*k_L=eNLs4(2dUm#FI72?lPDP5^HX0muJN4Mt$?Mmu*q#gTPT)y<^2`wQ;Nx$`mi^kF-^~XA| z3)6v@WLpgO>!UyjC5K|v=GGzNjDm%D;CiMn356O{f=VS?`uJ*>V=Kw&>lDaE(w)L!2{I{6u`<%Ai-*YV%t!tmzXDg2 zmpoFabGy$`8IgB^#=hJ6jV>N3lV4Ns7w-&+u+&e9vS=3LazuAwbYZw>b6v*;v zp0**rF}us>Y^QrR-v8Y>{t*ij&h5Ud$8U>4^RT63`968{g*rXyVWY{ZnbZmA@JQ;6 z@7CF~7y541@nBds$pkA4Jx;i=hqn{*?jerac|flr&d7ju(xA1M)B1xu)YstLW<*}S zXdlbs_`mWIxmh%g#1>YzTbj&uD(tCnlNv=j(tBnP|2&do`j+MKxe&Uh=yv6r2ZcuV zYec{u(xi;T2Aoi;z|O~a`jrT^tkiq5&U))X{={R-OZ;tJE2}L)f)4yW?D0U)y3mc| z(6u;^7};io|MINB{tFS8HAycvEpm=Oa0JZGDR@Xw6ct9bzU6-y%~HY5N8&iY`9zMq zMvZLY(gc;B4C;*Xbt_cy;7Lt2ia z8x0%=r^&(}t!@=jFG$-WgZ}b{qLj_J)e1!ZBp}F7WsXIh5y?6j1njhIgJ(&nA6(F8 zc}bgjw;(hcOFNVp^doXVNN?m(pGlyeTzPMd!xuB2<|(r$uSJQyZS0SQnWrK@3Ejp= zP#lH$;Fhpj3w6MQ*GW}lSbO1iKecqzMqRXGGB=uocds*bZs$WrgYNuEDy6ubzto!zyOaP^Qk3t?^q&-NvG&E?$3Kw zvvKM4=O9j^-uVb;{dCh6!R22XPvt%RpiT&nw6k8UdJSf#sml0NLi0{|4z&!xhfa9p z5owypjV~w3jMhZjff#wzVv+H}Sj65ROXOlGX-8;3D}AHy|Ey3>V$t25QZ=DFmvlZ} zqPEtK@S`4utuo?+qnk~ZFtb6*t>nbT>zd^@~fVaDJ?d*Bqr#0&$JW@uT z#Ro=aZe-fMefdSn%J?3U+r2?-DSRI3d*$SEgBs35N^aZVCNEaXpI)p6ID|B>klU+C zvRgg)=dmX;n{VT@^G^9J+C%N!sppa6N>c_9yT;-vUy@(0A3&~qYQdaiSM_LC zR~}WVaoRj+G|<9|AtRVh>xhEX<3X=Ogk^-WU|6*9|FK^q_s?Rd+?Cb)c$s-fz>wn6 zB7mID&7MdDrP#6B+2g`IaMxrEgcyfA;fKKuAh%4RcGFh35K7In4vd7)_8samCp?z{ zL?{Gg7)1jGyp!g+{Ue7JmgV@18hcERd;A-!Va`yZINDI*o zqMZ{3{60S%>STU@U$6Q}BZyI_&;_qoHv`lQ3iuRzmL|*F0u=;k)wKgTp@xPgU+G1l%;h zEQb=w{r$hbNdT%=r=Bd9qF3R3eU2;f4%N~>w)1PODiez}QYO5Ci)qJ%1z-K_4rm}} ze!}(Jer$kF^_G;ON3A4z&5go@Z>v3quZ|l51%rO-jiF?G%1ib@QA0)8$+pI+U731m zG6*V@8aHhgxCR5hH=m{59f8f*lzQeL@bd{kxEl(qsOg)G0Dx~&!rt3;!pEI$qOX%s z=SU#b=*+%6>CIS)c-Z+n*{;x^jpbZ4%B!qeJCeExm}_54l6c`65&NuF>z)^n%6|^| zHM=@HI&-vqve6!!^=m=2{c%XYEz&2gX>R0~U^-^zT5w%8;O*&z#HJT7n-s%orx0r1 zUC;0QViWs1-nM#fmbu$M5$w-e)S3RP5*iGwdg#(XpzXdUw168Krt6%%L=01oT8N{U zQWAMrdA>j11Gh@biy#3``4v&V85#L*VwAo3k%m^arz7In-t?*QDuJUJOyOS`)KUu5 zHBp-~S-q{7VyksqX+T@h^yqH$I=xmEH&8`&%PWDsxOhij#I9Y}Xt^z{{N`miC!ASZ zBo@R#k?udl zkF(2@b)&vu(6;Qjm8lIKwS2|=v?s`Ri z&fKfBJ71}*mhmWb_fhP6=brXfVaF@Z5m_cNn%T-n+xB5&t;p#boLLHDYY1 zt>VI~I2yRViB7}@1oowF$*9`rLeF{N;{u_0Kz-M#VvC1;8?BC}yfj-J(^nPMkSfRZH+$ASbaPOWK7)nL7;kHhx{{at&AnFRto z94BA{!?{TGhA%|`eu2GA_Dr^7KHyZ!DxoY92T^b&ia<8atP@yBSH&Y2k${$~HvcdU zjk+m|fxOlF+|zGjcNIGlh&#bX0;CLbpcFl5ciTHMM`OX@{ zFZ}WNk3-h(eUU~Od1z5FuH=OY@J1><$XS^$V#0llO*dCj=mY5_wVcoC2!M|D8}pe4 znQ^NV+J~MW+m`PAM%l~XX*4|VH{_RGqc6?m(zj2SD?y;@*f)-Jy-0`ITR zG|Y@6+P0p-2Ln}j2bu7Ju|OBk;0EY6u}Ctv1?H0&8Syy79&+8Wa6CLjPE^I~Er0@> zGAQW)?KBd%)d&##xnCwDGIWY6{30q7&v2yV9B!PSq@@jl=$Tbm7fB)>;T@eZVymsE>YFVkuJx2sGVm2tw&NV z34lZlt4PAQL#g+%eDyi!LTW=3SMt6p<|F3i`lv(pzPJCi6BnVd^-_mCsY$5Yv*^)W zW*B^DmRIeXejT{8SfP{ECT`OEW4n?tf%PD|D!BsQ(JFIi4?dal)m#$8;5D!?Uq`6! z>rH9A`f==ILiL18jqaBZiErmbD0?ysy4BxXULkt4!k{T|LWcyKTOPwWGYbRa_c{RF5LKOSv$75NTa`kAmPCooWfH5!d_moz@No~{=k z2h$R3iGzWrUgkkb3fRQP6BEu|Mxt)!35lXuB_aW5IX&o;K5KeJAlTtW)I^JP5y0&< ziVl$a=?rFkTh>0E4t|mN*4lDFPUUPZ+VSFj(-e2bkJZ(YiYHmKjtBH8hKt%1rG{Mk zD!L^=*S499!#H+G$GB?YX!ACXSkD5O{+dxXvrj~fbR7zrWNY&A<{L~%vOt|57bsjjB zVrEKTgf7)$9tn5 z5XKYyuzwPeMYg7i?&} zb-oi`Dt}*;!BcW8VpXh6aat86^_XVa@!j-%f}8b1$A-In(DAtS`CqQ!|YV z1+$WS0_2ZmCgHExxp`oHA0e^qc6}^xJ9~;5R_&%EdEWeY1(u29lQj z;Ca4caf(-*S<_-wY?<-*d|-E#Q-{gH`#FpLj{(JOwGkfZU#7`RVCw;z3V`6G00Tq!!b}rfxq}ZMVDKx+gTR+^8+~UH>y&A}npchCQM0E_st{ zR6)6VpTx zsH?2n3uHzNRAo+zB^t2NfmJYckKY-QZ)|uO@vV|8(YYU4%Rb@G9$v49Os2LAzg{`t zgLx8|-YxRG)tnz+Ozk6{*`296c`-fW*1D1KlZSv)d|3EMUqY{qTL$83QA=f0!N57w z`Q@E)UZi*0Ah>fLyUX0=qFe45l94|e(Qe6ZmT|?dW7R{#LX)BO2FR$A54;h5hHE2f z^w){)9tV?G7 zn0Y=}J8c^Zyghwm$reB)ic$w|OO6(pXP6j3pK)<#%YiIKbs-w-mVf^=9q0$+iEt?X zf}N?80Nj_vj4u}b$WJ<@_zZM~i%@Z(Hst`G^%zK*&Tg@QU}Fh=vv-^KfU2BRV`*gE z204(qB-UzI`p-dCzt8KUzgNS|d0b@qJaVUa*$X zd_PyltlN2%MMwSE#;0e;7px8qp0op%Vx=wE9(gmfKr6r#n^PnBbiieEb9#9A@Z7aO zW@X0~(UegM!CAJd2EW$S@?6~(c3n9Zrt!Jg*>`1I752x_8tM~`CIq?0adN@6>%_x4 zoy?^AS+ocBx9-;zU)0<`ho1H68RS%_SL{@ifADrIeG#rBCb5DS&MYg1$aWN=%g z{#!{YpzGbqvuS>a?=^!6iLypOI=`(~XY)@kL-2IvoX-KMZf#OcqhDhFpw8k6TzK~g zjq$2;#6hU&%J}afyY6Y2*lx*cg$uyg!6ijPpI^Dlq?{SCtT#uxe|vV8DEsi>y@B- z_g@m`gEQ>eJo;R6hYBu-;HgRnH`^pEq?bIe1eJ~f-}YukBJF`*r9XI1tK5Jo;xA|b z3GNlP^Sr(U^;cF#3T=}8-l%*EU5$4DKdf z9zoN+GMNss3!iel!>eWdNd5V=#xIKt_0TXt&cmmY8Cuv>ogMwV`-r2CkHsoH3(8>I z<7&Unqz}#_u6`XN|EEuD7N1>ro7Fp%6OT}W*Rnfm&a&xLTo)f<3kevK7tF0HA1G+t ziQ2nu$Ikt5LUng@dc==J$*}$=!~PvV{N5>kO!C32OC0Bv+p38!z`3}&tva&b534(F zdQEhod zynCVhAhB0#Z8Xs&Y_2L+DNmpp%Mtz8h+{3QQ$dB0u{jr;r?f3$5}9F?$!K zSS9}8HI2Pf0rcCRfGw8V+CG?oeS2DZF0|bV+q6cEYMhs^n_#r9s6yJBVq`MH zRYmn!2K`DSW<1iZcWWsLP-;Lifn+OE`+uH?VJUZ<^R8i7`jR^cRcs;A;qMjzf5pHK z5I+)w5hw5c0M+%8C@O+1FU0(X02O$38Mu&LuCi$A3uHaK`q%^j+f_@!{|1IDNR%Bh z$`vXgsuQ4ZVVwrhgv)bIKyWvKXw3)00oNMr!QraF^G-hv&#HSEmjom}zVOBRvn)KM z{s%QE5`J|LDFBxyiUSULWLFutBA|_Vl|Rpj#fj!4KI|Ak;69!E%0R(bPG5+E7hIw< zc3Rn8pVB94e~;0Qti0f3l?1EMamFov$4$7`a=K!PPBYw_EVR??2{+QrULd)%i%eb% zEcs|*JM}s*Et!v*&9Ky5tM(wxbk`g&crMH#-F4Q@t7^FY{nJVfRc%l{3^!7*HP_7s z{J452k;~JE-!8kLY^iG9X8NtsrhImr=!;fh%GidC|IkdQMi2l40p2Pmu~gcS2w4BJ zsBnwAGThuNZcez&Dl8sWL35FLl|_IUa+FMIly3R2#lHKLv{bAcxW0%V&zgH+ z)}x8R=i`BqHy~$n56^j=NghcO(J)uMDT(qMT*Fcde?Q=18Wcp zAEd`e8e52XyMB0k1d!$~Xm>3qBmw;{Q?&mr-w@e+?(+%1Q7=f~Gmiea$_b!<&7CTF zl=SMFgwzG7T>v=h{3EYC3MLMmxx0`UwCq7fIK=Q_RrDaVWlC_3dCBpci5WLpF&VQ6 z{%dVXBBwP4Go#91M35XGouqmEbP6x<{#IvqAs}|RbMmFddE|FsY%sRLx2o${*1>l= zd-|nI^r+;|qN4@A?bZz;TUULurnci|2z)o0)TRtNDjAqGO;TlA7%k%Y>MG!0!mQlH zqQmyzv|v0<5Rf+!c?rPTh2Q^2Rjl|0$ixtE^NF^s8HMRpRNmpJ(SajB{JeoIkiW+e z!knBU*h`|oBELxZv}W{m7H!)Q`DZRt3fa~4ggbr@ ziaR>{;7?2H#=Z4|VLdI$6G0P9etk(ImB>er%Eka*v+1To4|2hHq9qKM4LIwedA}p6 zn-+2(Vu?wRnVBKPdGWaf>YP0X-+9Rvsx6@M1M3JyEkMi=Z8hfkI(-nhbsRq_ic&oj zB#8s>9NuX#<4;z%iT1FVeQ$(r2KH2kU?P_DuJ(N3<- ztw8WRL}pM26#Tzxyt6NvIM5c*@A2?(BJg_ob$X3vL`wq|ynz4c7e%@Ji~9T3{!Owx zUPe80^CN0BZEq#Gi_hrQ3-61mc`X%;y9L4vu|m^4wRI(w!!PrRvTx#ME4q_T80)Vl zrOqgqHU3rZ{zfwb_d@4SgB<(dY_MNWuynVYb*!6?TTOo7t6zX0l?G>xg?Z;yUcyK$yF}~n z_LBP@M@eT`EBbiRC^h6ia>Pd$--VJx<^jjIy4*GM+g;PAEdbSR<|?J<&#?uLVrFI1 zl^0b`4jV}B%l75JjIK9}z=WoQG7<5N>kA(|u-(4weF%4zQ-&BfiCGECU5|(xIq(9u zAj?@}BLR5u7eG}@5LV+Jc~7Ji2=2UXd;f~4H_2m{GhVP7vt*q2jEfFfE1N(QLO{cD z0`>MJs@EYfADjnej%r@zlUdYF)3`Li$CF*pumiJh<|Grj-?RepMrz1eMH@8P2ry*< z>1rMgtmPEJN_|U2U78N#gQ2^md0uJ$Qp{ekaYfS={j*<>?Thi=lex3;3tEt11_XqQ z;&nI`uwm4ue4^{ZYsg>|>+4w>6awIN_qPwhsV*|6}jFgQ9%8 zH5EYw6h**DlFSeV$w`C(8REc@BxeyM2gwS8Fd%uz$$^0(3rJ3q1ra1l5Kw|-kep$A z(BHk^t=+A?-&XB^Yf)I@&~LxpeY#Jd^E}U)00|}A+V!?1+v){srtFlM2_AA(9ExjA zQ>K;q;mi}or_aTP@^+9JBR(NcMweYjrZ0tgZ>}rT>PanYrhK(-d=T5|85#JT3Qe7S*B2*XWhGk!Y;48qP0ql1c zdIMlpwi2=FZ&UwbL7w7Ci8QpTMM0UkGSqgbuSqZ)!=>VvNOXCxe?^R_jdo(SUpz6P zhgS1O5ngOLo=9~B+3g!BfU~XQfG_;|J!MC7jglc5-pKlXRX=YEk7Kj`diUKwqE9#5 z*p}GU>g^o%?~;a$A8Yj7=}$C?Y$pw~j&WU+>zv6gJu$^AMVX>Bs7eYaaa9aY@9w;E z#4xR$h<m%*&QhLXhM;`!NRsF~P<#qdYGa3J*eLL2e&$Zk7n z`dHHrke8FO>Gt=f5F6~Co=x~9Jc~#4(E+3ycNSPG*e_7ivZstU9J_O(@8@klT01|qHU^E{eyF03tNjS7 zF5bi25!EIpQRF0zUCfcAxGT#N_MBgzH{n^-^==1L-)n}rb2co7TD(=8(Wql;_Xf*+ zHD_u$&XOSN4_TM~z8D*b`skwjLebncFu?Jx)Y0v!@Iyy^{dI32kia77AdlDzcFx1y z0eJ|(mz-1SAe9bgxAc!@eaR^kgf->h#c>liDV;0%H4v&@!Ou5`J}{k)7d95Z$(bAl z(z6&jUWhRu`&(Z5aw)C?6RNeC1SbmWyz09rT}%-J*MK38LObQd0K5%)Cxp6C1Zb=n zSc7}Z*-q>5c#d^=^uC&K->bkYmnoWtV6AsPK4=}i$f(`3cp@6Kkf%P&-Q1|GI)o5# zJX&>YI6PkS4KKJd;L*Pxt{h3|tq|Ga_ziLhndfViNMA9#&@SJ}7j}w7{}9u_<5lar zV=?oeyV5W!EaQuKmfy(QQ6&LOZsQ$pvBQ3?2Zl zByad61Y<-~tWEr+!yCSk?>%&yH_e-8Q`bpyv>vOi*#c)IRQro^c>B&N$f<)gD zxc#%jCT^qbT`^PVlY;?loU);dVpxxQlze{*z{(BowRJlJ+kpQ@m#(|#WVgia>qj&|kd1J9%liv+40Y&jcw zxoUyG)aJ{=Hp=_FPu;ZrufoGL9N%sfq$)kzIr7f?1&KH@80i_$Pn9{_jyowR?Y}#; z%lC-W->Wf4xiG0V8Gmc?hM%umm41leh!K0JlW+2xlIf=)&ud%=t^+;A4y!>&$&S6h z3eBaaU=sV8%K-##)xeb}{4N`$tXU>NhvIC>9t4B@6>tQ|2tr_hb})tRv;BH{n@JKb z&%haAj*vm%-A2WHTiYZ@y_W=93{k>lD4XcJCw0a|pDzY=0*dxO3+KGRxV3OQX=gc7 z(S74IbDZ*f$a&B0{E`yKoQ!zt_vX_eW$C@A+zBkU9Tn7Xq;#OWcj4-vmu zF+s5Mc=BG%;3k@gH4le$xn zZm?7355s(SMIt7L~xUX^B#e=KCDg}I0o|LOy98bBh^65oMwQN32NJv(GG0iI$Q#KuoU zK%kC3=Ptm=Pc+W|ybjLuqz8tPUso(ikwEY48~+t>-yO$?cL<0AT$3y-CG9E5fuDh} z)L7s|mOv=WeDjMSU{}4aB9;LPgSJ5O?;6PgT1|V`>xbZZAZ`|brk@#Yx`($G(g^}} zu>~OO4XEPv=%z@+v#Bm?3|!=@+v>w9-v9Q0q44AGxGnH2F~9v|N~twVZSkfBKV;@q zMvQ5{Ue{`83j;B0cTRKBc2IGFMJe(1bAD^#OuAHbpEa7dzxbU=aJ?QMq}*{yHf)W( z2r?ou&lgtjeb}iuQxQ{{SnMcw{d2&D)RjLgfdfKUIQc*dVgKdUSZTKiyA(o(@w)RP zvo>3Bs|4s}Bd|ZB#DQ z3P4BOT|2MDjVK^obNhxnf-i20w(tVAJtY!04TVPC<)gaG)`~y7(-9y**Xs~d0zzub ztDgj_?%};p+aScK5g1WzLeF+pD){-Kf{Bf*P(&&S3$>_u!AIJAW3hlI;;cjvi>Df)8xakEJ@$^uF1wi6Z0d(Bk zl>0NV$)Gein-&6jFJF4^tvO=&p~~6K4G4h-e6WP1j3*Vi2uTAic8);*;AA5JQ|lxO za5?_>agd&G)A5@WP6kwwfgo8mT+)5K$9 zh;JC1J+K1y7866FkdQD?C(VKlupvqUDvPURKR)OwDNQj#E;qnDyQa1lPu8*m`2 zt1{wwyz@)^o0&JKN<;U@?SlaY-5nIbE>1mEq#1|}8nG$;V#GEK%Ds=r>l>_!O{s{# z2t4cMJFN3wrIL7VeIZYVnjE}QlVdhabah@4tdey0S!5?ZV4lB3(@leD1Zj!*21`Bm zQpqQ^bWu!vqv1}B72BWvoQ!h+Cu2MCZ#_X_MoSIjrdjgjjgdO6_Ix-kkUEur0yBr=bR#=t5J;U}9)RuLTeU=()iIEv zF4Ky7JFcWi8J^KfcaC^?zVR~! zR#$-!Zo2JX-${oA5A3>80q7whEv>kG)4y4Wib^nAVvDBI5kC;^JP2c<24xo6KB`1; z9BBWvdV`x`keZFUl=)5(;iUkyHt3O$@E-TO$D#3%z(*)xNdCeDUzlw0El1-41DZL` zH(v@NF0DTs`71Ne+ecwP=rLZ$e3#}!XQp>vO?#_Zdfl(Y0U&|9l|l{h?Fxl1s%yeL zZq`mrBiC#}K@dRXBpACF2IRCGfLxnh{l10fp;P^Z`mO%76N)jt@O?Sr9-gxYh;u?1 zB>{oY74{$ah4ujr^jjF<4N(eIizGe-9{N(c8zfyCG%l15a64d#` zx&VM-j;?>vKs=>W0+K18bP?~5K)kAN4M755*o%KW{Wci3(yc6(at6Y2-WV6Q|MBts zg^Naq;AZ()g&I#%68L-=txU*I~EN)P42} z(=nRWDnbRudBata>oBwUY)n6paY@4g=!p?flA4MJK@Vha-T!JAg$07C`7hw|^TPM< zH7;&Dp!+~J0COk@(3T%;=A!T7yH1?4+{HV44|*BsUS1P}dOQ{+4=%6PAKH>pYQq2w z1YuK-f{y^uFo=Whj95(&y|RSjq7@NA_WLrkVgWbzkzyww4Kvq{k8Tn(CNMHtzsvA4#8Pu%Zo?d%LO51|Q4 zHy|xN-KJfO_iL8d_^DLYV-iC^LI@0%vA+f4j%TyP=e{u`N9|9AJ)gApBa)4`vhlp2 zsquG1(Ly()dy(L(dv#0?IxApKi7fd79I{_0U0neT#j6F%;(o&YaG{ zp&aJ^R|L|rGq6f#8D0q8H}>^R9yl9ah99i73yLvSD6TF6z#cV(ngnbC-KVsose2m* zXu!eP_fO6mVGMGf@}oVTmy>iAn*(nyK@)}de#XjZAfDI}HBHCF0*|zf=tmi{Q(s=d z6Ifl-l+)A^)!kn=k+Y;URNK;6osFtYmfhPeN342x8i!qp8AS0@IbL(>4X+KuaR_BIZ64-xwBvB|EZR{BWd z#S$PqI;d>H1mQ>asv|q^fBW#0Q7r<*&?%Rt(-}io*l|r+N&NJf zpJZ)t{(TK~i$~d}>d~}d#-@T-yu=x&4 z_4l9apu&RF<4}qAb&#_Bps{X_R5MZKvV43hW+j>Q==%?Ct&t~@sZ64o8v|bh{9I$g z#b=0nDR8@{JE+@~?fb44{hIJWX~7vjq7F|}^*+4O;;6%~U*oW{I4X}tf2@`|AF%5`nutXb)5-C)Hay|e}7E!+svR$0efH_&5 zeQa^@hF#QUQ+Of$GqU5mG+slnZsKT~Q)ev<1+K4|QR?Bu6BluXYXDoCiKF~ng(=x} z@ms48TSptixZL^@w*}pfpuzoOAwL54Y0_R$7!uuhtob9&@u=}8hruYOi&U*{&{wgCeu^TW<EDbAsGY+x-o{dqrov!>93ESoYaFV3~+!BCz zHI0PcD5kx>SIcYC5=2h5q4Q}$n&sxx_ozoj(lI4mBZ?^^lfwRe#U~Y zV})qPb0{PB2DQ1EAwCZjpR>XdW-0YAv1SHV%xATzlCWk%__vXM_(1>x=bIP|BLwV1 z+^wlMfXoz3+M@|H{c9*@JcML?GZPOA9E6u|{^S6X!KtL?4Ue>5#1Xb}XivvQYGO+S z5L}SVzylfrG-y9amMy>&g&?b7I<#J7C|ZCfSM-_QkHBrE;)GnEMlNNLQ-j)4GsbKI zm1w8CK`nRQ_ccCc#-FqlQoVSC^?1=SGv1+UdJa}zbT}E45;G}0%6WWHLqEz;@O#*& z?fi(gO>yfJ;^6S{;Zyc1avLa_)4}|B?qx?H{CYUjAWfE*ocAMs+VOheji6fZY96u3 zGn5cK(%~WET(}|zRN4@t;;@6Cw`3K*A{B_y0|Y^xVAv1NSC2uYy3f)AfOUhg__?(r z7WZeDOMANY5RCclRyD*UXF_v6gd-yfDiRgx=%jK#i(u`y-!Y45##% zvgKkf&ru>>KACAeVXplVFYr9^Qrjj>j~@DAM!@_6D82BAO-qpD9o4T-M|?~@wo2x? zYAHRU5jq``sy0kt%cwnBzX^3&;<>eMvYe$4I3N#WdCyUxK&_EBPnJOzetNjKLKUqj~?wHs^myFrgxG?+)o|Mfwnme_N1;}M3 zdgZ_GsOJF%;(V5i+fNKScDf7rP1N6Xc{G*B!!2r9Uqv+)cLtL3y$KU%Tvfr(p#lQ) z!-ZjTF^b*LSnc;SGq>7pVf+f&srJJZl{%eEVKtP!D5*}FfNdk3Ilse3coDg^ATd7( z&=`ugVh|TmJdj;h2J;9&PPo!@K}a#`(n)~KE;RrII>23}LYL&x$KpiW4(8CS&9(}f z2t9I3jA><`V83bFg@zVMK}}^>8}#t^0_cX+hdH`g zoi}wDr|TDMd@qYjLx>9L-wF;Mz5M*O0YskkY4G&P8K z{lsfn$+cqZGvh{Izb$Sl^r><-<+b!r;KJEl01Ej}GX;@CaCgZbN=L>bKZR`|r$k6j z8Dr|g1|iaC*Y=T+Lhzox$Cu#$1d!7#c(BnY7C=1AT^k1I zMiU+lr5U$Dq7DS&K*$DXR9A>xXWxLU=R$-c=}?Vj<)aib()dZ z1u^EuSc3zm#w`>eEcxFj_ewP7e>3TU_*jza#u~z<$gFI#d-42Xtf}OzlAPYHlzyvr z2?k!&o=a=@>p|ghW0jdtMqE3eA{mOya#%VB9qd?3l*8)k#iY-st?-lE#AiT)`TYa1 z=`;Ci;%)qch@bcNjDqaQkL?~-tgR20( z4AW{6i01>rPm{YsnO$dhh0NG$6SBF3FgJ>QLuTzoKP9`;cWNUSD|;Xt5>@h6W_-sd zVb%ab;7rH4!{WwPa^1bL`8%Mg2s!Rf2bxd)uAs?frFD3Tm^%IvpuOe+TQLn2&ca|nz1oHn0PTUZ%dz--J8+r+wFJUgzZEiS~Hj^b-SxboNF=h(|#$n&1kAwjl7zd=|Hp)+h zZO05;SDyo!YN$!R7U(={6o9RW_iMz3%3c z`m#T`42GLCu83~eWS~fK@_acjjL&S5iF0q?lu79w*`o=dl7)7*G;f8?g)AR&DIj(n zjhv%ihwA7cZ%vbSri9g|#b8wP#hb4VaU-TQvnnW%2-o8whm-xe&s`rK8H_w{(6YS| z8d42�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 WqYmt^gZ{F>Ul#b^Vgb`XhW-zk``|hN literal 174012 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%0Dc;Nx+bmgW-1nMlICocu>t>1XS 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 U5`9a>CcK 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≈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 k@Bh1}UH2zaRO30D|atH~;_u diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png index 0526ab1b6215370b5d57b962a43051b26948c0c1..3d3b2b379aed7793ece27dffb5a68c427ea7f276 100644 GIT binary patch literal 44320 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 literal 105046 zcmdqIbyQT{|38XBmmmYuNC`tJDM(1eATk3(cY~DDjgORofW*+@00ToeNFzvhmw=#j zNjKaxem>9duJ!%nx9+`b-TTKqYY}tKoPGA$@!qd^?=a|VMPdRP0xT>nVr3;cbu6s= z&#|!Z*75EFXKJ=11A#yIZ|Q6mR%@ge4-w41Aqlk?xJa3?2w7ba;%+98jpz-1n9xQ?rwrHi?F8yx54%Mw~LmS+8X16r#CJMY(?*O&@|IhJ=&*->7 zoA~QDyxynhcD5v66_T=P=rHeR5_kuT9`myOojCUr@IiO0F#VGh!TsQF+$vT>j6PPJ z3Tk>+P!%})D9vjvy>=d=Eb^xzI?nKDKRV#*VdmNQF8V#st{1Bz`iDDC0X}hbecY8q zJCwi;SgRJ2fBqruPZQO^d1tMn*RaLZ&Vz9MXuscdGP_m3RpQgO|2t0YB7m>)Puot- zoa256Rb~+A#o7CcD&4QY^S`{j#hHAE)RF~3gc}?os6{{c8-;A3s$A%qf!B#j|iKnyg^OI?F+en~sf8|lKfB#+; z2SNStJDopSXaUCZQRJdY7+b=y!N+z|sVnQsVchG6X|ss)Y~^H|t|O-FtY7OCF`Ulr z#3I|3Lz2$3@ll-86&IU@g(l5J|JaV@5ew?Y#S3^{1-I65=%HUQVPZm9g(LRYstB?;`roVCtNQ{ReSMpM#8> zG$}uET~DaZ{^W7;il8sZ(WF^h;gSSbl%4>M&z5LVaqSGZ+Mqt?^-b0s#3zd4M`j&V z?V$>x*zR}@T4j} zxji;54W`uV-a`#V`h)O^RJr#D(D$Wzj-8b5d(h+Fd3(Kh>md3?eIxki zvuYXa*rw|pC&oq+P!J{WdDEWX9@MdSs^!Kl2ng@3o=Oyi`bsf2`<>UGp71N8v!Z)) z@3j1qJ1VSM1=^Y)rLL9)2A{LQPle~~HM|U}8?Y@c zJs6?rG?zh9q&@k)+Y_hT1jo1i&$^NXM|EVH^hdaT!{UZlk2oNkL@1I#MK*G<^C#}F zKRpyU`K+OJ;t=b+cJQ;QaERo>p{A?v zz{^P-i&)gN=%qNx+qj=R$4VSL<))`(_ zUmbd@<|?sJ6h@hsKh|D#lr$dn&Rkv(-ChkPaG5Xqo$h#{Cfk}X=l4ho?`j|qRz+HL zLC#~UBJ&=Lep5z1J7QZ!W7;Vds6->0XiVQc7{LmVN6B@!(kE+`e=OapU2I-sYRa6Z zryic2|1E7yNQP6)to9lV^HoYp=iM^G8?%Bt#n6njPz}gDzWBu>=eKfyXUZW8G9M7E zXgO}-uf#k7VUVPU@Z*FcrQVOA`B*~)Y1j!m-u-dy{A5Ur1?03>SrAhY7 zN_QN}$lpASl+gK;`q|)oJWq0>qyYaK00EDfd(&4m!+lBkO?ecQ6pC*q68%%jhQfte zUKHk5)Od>ZFM7>-3$n;>%G?{6sqC(H-t3cm2!lNAUF44#8AsxWK|%G!7wqs2CYcTr z+WGV%$EA0)RMfM&Xp88F?o-!=dy;OO@P>f{DY=%E%yP+FxkHPhcb<8l`BEt@;6eJ& zW2|b3YH3)ad$){lz41SxrTe%FOrHK7)9WM9T(#D?>eTk>fIhJGjL__ktVlIeLyI9g z-awXsl(w;iRWIu$A3k~HpM>01mHTgQ!iGe1mxFBuSV(;NLDsr~4Z;4$FMK3pxx^i_ zO&-Zk1y6G})5KaMxEq1tv%0K{m|5z5X7IwiAO!hA(J~4Ib`H@|=PO=V z5AYuHS$HUM2-^Rme6^$;mHR!&(EiQr_37g6NVLD}x*G@|gdfXOGu>9AGE|Tzx_H(p z0$EXOnd-y?0l^Asa&xCC#u7Gdr)_og#A<2B*0O@eLU8`o?`E|1#V9AxlL;g})sSyxGZWg5jj1zdxge z>^39l6}`j-r{tYwuE$$$FIz|r(TzX=B3!)MS-jc7f0Fi~DtuBKhI>>$)l^c|5EPjv zqo>u;{P>6vSOavDUdTUwWwi=!?OFboC69kRDOs&{Wgkhx0NVSwE}AU41wFEbEdF&zA{uF>Kq`gd56nA7!NUm4?-OgD}LQ7Y4CPzT=c&TmQ=Eh zxjyYBGsq;(YXO$*vMg{YFMNB6Pc5o^VJu-Uw>`nWtMy? z5;T1mN(5p<#xe{GEOANnDw+biL6IjdTcT~J@ni2BUe)W+C9kq)`* z?3X>Oz3#*Kx=1rZBgGmgi`N!;H`-RwRd`Qsn=p z>YNzxmZ0}0pG1$12P}h)J`&pKzSx$^#s2ZW9WA%iVG$*!kI6EqSww5)n=ZUH@k`oBIL?4Vn z--leaf6J#FbG^`SD&1hF4}$nL9rhM%N~7f(NJWmgY(NnyDh{F(8CF-5n!VzczMKH; z?su*{J4kH#pvQ!(!~;=6%5bxrwfP4p!VdsBW)5~s!Bh<4P#BJY zYV406A%s4*O0zQo=rhjqd7-Ij-5=Z#JD`2)X-c^&PxX97R%4(MR zT_h2tBu#hqtvaV_@tfmU=;*J&v9I_K`}2dL(&$f0Xb#J_7pP#Vo*$*ZPl)MU`k$Hp zGLM!J_O1N+flx03hV#3(nH;3I#%iBWeU%;@&c1?+WH#GNp1yELJ&F$#7P=d_9+%d{^U3TR>iuE$r4}P;IoXsUz&HIrM>D4%a835Z5V$e`1+ZX+^oIv^T!~_OO?_fo|V@dWxb}tUfLqR-5*U| zzzwk{p2)`^t76lW%fMw;0?yhQNsRT@6aCIEf!LWaw>BU%-xbR(jb?&}G&Weh)2VFO z$W6Ucb8eX>P}R$C@)xtjA=C#q_TF!*P8z4D-bip6k`S@azwe83@FoD&L66`zHJ{WX zV7hITfAK~4=SMb-yq{usE(|(#k+H9r)(L0CDN7f$Y@8? zY=XOV{+jIfR5kXE5;K(U%gN^X(B)Yv|h@hb)9%3cYxY_#q zlsKPtfRXQhhbsCgo(&Ha)ceO{>XnD+t4Vz)3Dq~z{(5c`nmZ+Zvg+Pe+;98xt_T)G z7xMVaUn!pTYB!r%>srLf<4}PEqd)!)mz|m=-44#vrlIL#gvD@<2rc6TtE#A%Xby|M zCv;m6QebK}!XKZ9eKvV?V%uB9Z@ z+3>@K;*wWrB3RbnDzW2-zZ0wOeXlW%Qg3{La>1(Orlx;ma1GYQu(&TTW!(9I&J{EU z=&%hd{HPEg0f7~hf51C(fdeB=-+k;#DyvEF0k|>_Is?#Dg;E!yVg)l1e*gqm%XK2F zd#KG|`r3wWnpLF+f;G-r!usADsBB`vKPDrVDW(U&T2L$*G^~LpqW{}dbkV2N-DV>+ ze|B=)B1IO~pDc5EJIL@#%b3;Ow>*uF?x=~GT2ZNlj!wB(Ga<>Xa;oq^$`pHU8>=wt zw?C3k#MP1f+|dvbzfusnWzl6GOeLP9dJ5<*9>$1qd7D=$QCaSpyCt?qdbA6jPjb~V zOF+vYSS}kALK#ZPHyh(NJ?OSlSxHKz$)ZYzA^!@yDS|C!26v47Ev)MS2HNy(?u+4- zu-MNagO63x!%rSxaH(@%lLO1(aH%W5-wExGU6-wt5CftPhIUQh(yQMGFl0VjUpWS$ zZ6rGXEKTKZXDn6T!ymHQivaulU1-avU$p`#DZloioM0V1E7~SfDj>Wl1g@IgrwZ>U zXOzih;+?s0uF2_R2r?016RL>})FLBzOc*D|e*du0LK6}pVLq4Cil_WpOc8-fx z{tw|e6MU6pQ2ekzrufxjWzQyT91=AGdG#iu_YwOOWzNeop;K&1ou5AgNrk7v74d^x z&Ig=GblL#2cudpe`STxK26_g)I~p08+AS9og7N@>6it1qh~j54et6xk&bl%BHYDw- zSoiSSm`KW-6KTB;crO!6v9N7dEZu$4CyH?iruYcTwATmR!4u|3er2F)%W3(`6n{IZ z>b5np!g$EtCrS7ARSoh(*}zAS?5ZCI`zy2c&5ZhUNCr76VO_f}_DcAo$iY`)Jr$!7EN)In z3-2(de@qY;fyD1WAP?M@OzC1k-3Ll?d5UnnY{b|Tmx)9J0!&J@h|y ze2H0*fCB@4Fvovy{`c~KZ~pi4e{cS`?0;|mx9tCM^S_t>N7?`Tiv3?K`+r}t|Ce4m zY2fU7FW`1Lf=`_k(^&7JUcrd|^bDdenwXC!BxK;oTpo1`cdGCo(wH+zt}-i(fC9Ki zjq6yqLh1M?ha9ZP;FZ%NO)CE9B^V-gm^?I%!&O{GUX2TU{XH~UAUM3=SYhi4!QO*0 z!>_+%Rj!D|&VB}F?>GUtda7|%sODRh?HF1XQ+iM@D>=B-d!jIe8J^TGk?#qB$Hn6k zj~Qa`E&WCB?_`SNN5irhg%qPM?$)jOugfwXc>LKcEX^Io6z~8Xr-XiRA(ZDls{Ew- z#|zGh>M5htP0aXtiGKCy!1F%R%qSj^Zao0(E5>93G{@&>df)B`uwx}$cAN1W#sBJK1G)8 zGmP}%SDw^N|b$Dv-Etw8BJyVtQ_>*Yg|YSkokaGK4gNk`!e*sEq)mw zicl%cbp3kJe{0dQ06ZUf?5Z>_{Sm;qJa4a0wc*o1Xq?>hqUCc>E2z!urN7|x);c(= zX2ZO1Q15+Dc;^Iqr*D=v8o4Dp{evzq=0=+2#YYy7%fi#0Pqp2cY5o%3&me;e{5HGy z=oYWPH=}bu=>Eyk5&04K$%8^(pOX4Qi=%gbH)kiJh-a8$@eniW=xsoh_7^CAx*pJU zbFo$X!?_O-o)2%2)NYS38jj0FB7psYx}6jhmJqC()VfXXwcPsi&{lNP*!cpK(HKxd zT`kx)ueYtG8b%ao=>hdKTI$C0mn8;7NN89!-5i(RCI*~Ssh;RqA~a8axD3^s6v{oA zcO6p=@6&Uro{+rWP%C7BuX;2erWcgBP3o?dwUabl?l$P>$mp4@u7@mv0zk4h7BTx3DWcVAz&ZBPwcZh7EUlY^TZFNCu!10q zGV%)3j-F!IPE{sx<74%DDy23(;l`&@wGG;h)hQ_ zH$KRLNw?6=3epv@=d;M=_u@5}>~BoML-a7iEsFZ?+C3YncC5GZ7g3nCsk}!j4rR`% z(EE6MYJ7Wa9OyQexG^&btkm;nVE~(`-|6xlOI>hn!+%0FqU8_FA4)^lks~>Y;BfbC z1A)lNlQA`^rXKdxaklVV<%Q_(W3ryMA}FkK=1pO-UN}#5^6r$y1Mv3@Or!;b5Sb4B zxNq6~mc8+mJY-N-o%~1stF4s!_9M(>RB+_N-z__=^r2+K5*w=4_hMR(P<)!p7Zoz{!M0L2Q_UyUi&v{6 zEDyla;Xh%RX9xA35g@v=oj3FIyvGd4;P#SF$F->Vp}vPVfL)%~mFJ!eSBqi-&_R_Anfz3iauD^c%|<0ch+s1mq$E%Yol~xmB1v)nfg)d1 z3?`V0%ByYyWE#|ksw!`I<7i7LgSQQsOlXO<5O=f}Ulo@4Mf4&MFf^P3F7@YNB5;>b zLCZyB^Ru@=5VuAsSVu#i>v_NWyUYQv1ctYT3QZYk9MV;coYXG*H@uqb7W?6QI8Zr> zBv~F)3ywr^s#LJ*Gf)To)i&DfF2m=Y*z)lZ@9$>C3-mJTX8hwSfKa7IUB#@pd>-Va zKk0drIi7!Qdb_)Kv+L1vInPv6k%o_WlsBGDKF>+L@9%?JDuAmP%gATJj7athy#Ni- z1(@XtHMc8#0p~H%zTchrR`O}=^3z(MNS*goC2*g;0SqLi+SAvxb_s~yy#!_I|4zPr z5P0MMr_zAyE%HZ*UtA9FC~$=L8V@A_KPta$3eZW-bQ*R0Bz zjCV?ggap{^bl&5%!A3U;j`pnb4fW;JJ^i=_7?KeLX~(VOPsa4`Fef9Q)~Wd}KLi5D zP6?^H)IzN1=E~}pAGq+RNC;J z-@7CS|J78mko>fGAk~*nqQR>XiZ2S#G2aqbGxYq9C%C!F#Nd|{uSF8MDumKFQ9&#M z@cu8tdjS5$h2b(q!RbP)@eaZjB-d6Kz84#LiXFp0J?581;x)KtMT%|~5}{$k;8i0P zo06KJEU(p>g<7rwM;02Rzk@W^`#9!Q zw;awTOmi`-Ao}g98B#u6>1^+)KBUpTG+FsAuj{rISMtvyJ|r;r=tn(aucZe7U6?UC z5YTd_GHfqsbIiWdzSwJYsVb!-To8_2F8W^Te_Yt9@)2h*xcOP2Q2Y~!S{evEE8o*w zMbTt7!Hv}A)G99h<`y|)cM(M7Xt;Wpko&<#Q_(=mY;SX?QOb(fpWyNF051lh)sJ?& zX9wA5dq*|U(-!2aMhE&g>xxouN=v#o-T|i7QBfKoW7Xu{;4|_9n9RmFY&4^5h&T4( zO*J1PRUb_8-`NsI242uARLefi^%a8|33NA&G@i`b`CO{uyo-2*N1KDR+_?A@1GQE3 zLPI9kR8}-qfhegya5q!Tt$;mJi2|X8*ib=xMu+o?r;M5>ct@( z(%=H!145p5`$}$#w!QyW!UJh$gWcrL6H{{hnE0nil$H#fLe*Fs8QUHGZTXB3-E^}=+iV8>dZt0x3`6To^654u5R1Oush*mR^1}xetB-1bH_V0t zZR-^h+LDUu&40okrI$TfpPv^-3q|LUx&GxTFVUY~qcN7^j&;l}t5`%X!5MLak+<#b z7Qg}){NIX7^k0x>A1@qzT1C6QbZL+^K?tgenFMsvdtg!u_BI5E$0<-`V+SYXpnDADA$dNxQKpL1G79Pxh#3jPCdipPi~(m*9x&bn^1pzsQ(mtZBp5Xf=Z!adc> zK3Gt|RZHCd_)3lb78aU|oUObvMrk)M(Vc6Rec=*_>nqj#7j4}$~osN&8a43 zsEsij)vnhS$BLdee*KFP`^@S|y*@&a_hQApuv>&-8nksKBCO z)EA!E+3k}xooZu@dcSj;1U-r-iQUd;6 zbsiy?sq9LbR1Z#STjux9g86(O&$6f2l>;zBC?%t?r2ftC3n)WbhD=A^I*;y942w;S zJmtPVctnfhXZ&h3#Npja-X=R-kX{D+kBdo_svggdjQk9W<;hF$RuUOD1Lxm`F#QRl z%R98bY)4yTnK!P#no~F_cDOd4e}UrT-u9^ZFMGtOagu|LEdGs}Q8MyYg`*0Hqri8+ zV-yv%^ir&q6)IO56~~moci+PVRn=GFM;hp0Um})aH~q zJZQj2*tGfsjzu3siyu9L<6(+i;`T;jfyOA84BIT%z#*mP>wI`@Kz*K@wTiHijNle$ zL@chHMs)L82Vd^goC{g1eX2H%WlNv6GV$2$Q zKoWszm2N$QZ@9COypOG;vf5Y;4BcJ$klvtjZAV-6TeSZ*kP>9{>{}q{2Kc8r6&gk! zGY_b9GcMIW@JRy~)s~fBuF@ZsG8KT)1U_2nk~}H5aYzf-=qONHa+vi5c=#m%yupd- z?4JRg-C~?~X?vK+MMbdlz+6NPb^9Cw9FWKjR<7Ks8hagB^lC4!Hz)4x1v$7PwaHwux)@tTg-o?2AUPb` zV^onqeSX#@#Q-wWO})O$!oc?75}?3y0%NXC&h9goRE!zch!((a%#E1q4x1X)g8(DB zdi0}w^1Tn8C!+iRt`CYx=7&%96g%>I5van6uYC3ESwP;-^3j-@_J<;m#$WXL^mPEm zwMtYaTI|k>1q0&uQjJQCw*&zp?>snfbFE~+Ee??T_Z)8L>1X%KH?hwCRL;8 z??Wn;A)A)jr*3qbo+?}dEMrMg6elA=b z9w9`$4QPUf$&H6S$*}QCb4Y=vdo*ns!a9S!SGwnNY4((&5{aioy#0-(d!Qxg3+gvVPb zGCf-^1ip|%y!w|Zc1th3a38G zhvrY0qJAAwfZh9xp!nTI27oTJn(nbM?yd3i^$@logyX;($oYw+WIGxITYOyvgf5=Y zg4B!!x+zWHKY@(#GfeB!q4AX`BX>ECZw?a20_{3mw7#|gBi;U&ql~=! z+eZbZ)<1=i#2^|&LSdGFA`rE5vm4_YY7-UUSWU=@kl%%?=(D2!YooKTSy-%^ zfVPtM1^+0PH>H2aeDp#+_Lnp(oRvt75e(^`V}{?=Qs^5)@WbyN~4o-Z9mGO7%nvH!sttC^PtIp@|@MftbrQ=)l;0_zBybTbUmI3r5@H% zi-_T|t_@WCO17aLxm~zfzk?t^AU)x^s;QH_aNm5K(%T{^r{USpyhD{HZM>Eua6ZVn>>Qm~(CY zPMNkb1Hyl<&cW$?y&@(dFn-bfF%A(>*s2AR-ylZd3zWKiN$BUsX60z8dYd~}jXXp% z*IA0&a#VLyFoCsU=1eu$iS&9?S6hPbDF{4e93atzF%w$v6Cy^47IWK2(`8%q z>`yO*H?Z8TC5ByIzaD~Sz5{_})1Wby`KSb%aOj9GE8JIaZ4hZpj}G-GmM|!r6P}qx zKW_%g!Lsr}^20ujX5al4bhnEAdp1Ah?`jC}?1;W$_xs!fI8hi=1^yX%f2_<|g%S7v^)mjoI{xc~{Ognad-*>V`d>E6|FAm#pV=t? z!|M3gqWK?X|KF?FzxDnfW&iiB{!b64U!f5YWh3=(Zcba)tT4l=l}@=q^V5IaqxZ0n zah+u6Gmby~g&%^oT2SG;r#`@U0L$-@6V8xPRHBzv7kKc-g7ybM@Bo!|5E;r)4~T66 zx0j2?7%f`Q76l~1oiqTH^bzL9*6n^ESNS=hk*W&MScKBMKmXHuZ|M22Pqa>r`#?$& z!w<9q`cs5KvZ%`{XI@&x^~oGx%hiG4U*9chZrZtzCtCbKDJ(&hJ=W$VBl`egfoXtr zr;+y>W&76`;|Q^d6q~8m3C|+6(;l0NftW1*|s`Lf6UY`Ce023&E5Xd8uI1zHEWFBpFp>m_bQNYw|#1Mt0fK-&9Q&EKzw z3<6Co(qrDrUJ4aG>=)nyf5yk~8!Rk8vgF}jjbKHT6vj!wXx3~$2GeZ@)0!*TvjI54TzqB(DN3yr`40Me2D)(v-P=hV!lQm zGyH~8z^dOycMfpdfg>bX_vZrF14+9bx1iqJAwa-Snf%*aSg-Ija$k7?^6{DxlKew6 zkZT~E@}H7&mH~X4Iwt{^WP7$tfhpyEPpkJ(Wg{eiFR(wxWV-#>g28qxm1_UKV=L>@h$3S;ZE!D;up9ZGMokL3FY5;+5V~YT zMOWfAikNjq|5n+gqI~T2ivf_#3XtMiRrma&3>eO889q>Lh^Idm`|W2^Sq9)avbCQv z-iK3rw5Q|scTc!$FjrAo+wXWb2~}oS!dBKj&3wSMdT;s;5Xy%Pk;Xuyv_{=|?-+~J3>1*@natV*oAOR8d65U zjTb7S8R+&Xshcy5ktj^?bY{3nQlbDb!ZI0wZ3?$d^&?V4%L)39$OPaCA#CvG)26)v zLl?M(x&@b+MN#v`CNM{go_gFI(*seSvlY=k6SBE4o?cIHsfLzd}e}MSE{^^u4sqUuG`1%cR$}6ezgzrFF zwWHMl#zx&P?<%Pr6g$}h-s1c)^Y#Z<07JN&;70{ap|sPI@|9VgEdubRs0N%SGb#Waq^$dJ-dE-L9=VH z`q*9im@uuikXM2aaXC>}6)5#yF229oChNPw5)401lf4YN2W$hdn)v}lI#d{kf45c^ zTZKQn?3D1b$oB^h9|x0(6rQHru8UNStgiQEIOg~G^X0eq`mV0fWqWPU~eK)0S2l@Bf&7)Rup}Nao6(t~Op;!tm`O zOnn>ai4q>uxfd8w)hj|zlhJ}6Y4Pa%Wy*Nn(Y7728Zi0LasXeZBMtm;`iKuOXPAHi z)FsvHr9N{2Ku00vN`U^==+hz!X(xkg^-(h=JbrW4bY&?0z1l4&x@QGcPG1$ynM6>o zqLDW!o73KPw-&xjB+(<%+Pk67pVbc;q-+K86*;V^RLrg{UN(gZPpb8Kx^KwA;#6Kt zgtuFKt@Sj(y_4OS_LfS{_HD7BQsH7>*lzZ1t^UANN9%C*IQt$j4lFuM+y-gTH*uKai`4R@?~Ba7qy&u)=gV(UhEh&c5;@;9avhP!q#Dx%x_;H04J4HWq(&-* zIsDqN4qrO>T3P{Sy#IwBKkH-cf}$ z^5kX+g;2sp5nZ~s3eksgYLfX~Gc>-aI?Td5LmW>wV$fT=;BDug3NW)u)Sow@AX*ik zO(2;H)}zTP^mu*-@OD+A>n>_t69Myckfg9Hhrd2vBL6}&z3qd4W!`W*IYZV{BebOM z3MO7dRK>i}l{ybc&B=vmFO^Gk##4wGv)I!&8H3ds{m+(Yw#Vi3@5DO>sq5CvRnY4x zooJ${kz;vk+Af-^o6ofOT}$1%Jmwr~x4o`^P0Ad7sy%%Q@?t4UKMO0`m_f9+GfLmq zP!zQRt}(m0tP@)`E<$?#YdBA~j2}#}q@a1q9^b+E?;a_Wl_~4nF#sSXIS7v+goSB= zOodJvwk_He9>=hH0Uxd751;(!f%Pk!`W)6-W<$00P(`LAdsmsV(mP9ih6KntIX0g0 zoA{MfbRbcxE9{-9-&tA3m(Z1evv3gd)i9n4yg~Sd6&^&cIhP*@4TG|+C)HfOm+2tL z^++<(n;C$|0%_9rbt?&5c4q}fQvU$GB49TbG~iu>-X>)6RLOa3VQRgXKP+H#EX<9} zTFn!SH|TNXll21R@K;?z?IOU+^?~Rk#FnkULesC4+B!)yalTbQiw#L`)Xon7;Ecz1 z1*ni_mv6dX@;UqE7nz-`YP}d*xCRpXrfXo2_3Nnw-m2*00?e+={MRo!O>H#+xL8+| zxeQx)^uYu|!p9KZJ$gKb&0;ad;N1Oq@qM7QS0q5SmUxgnb+dTOzaSq8zHEC>4r7HBlLC03=7)5{7*a=Vfbp z@WwpETczh`>YI7Z=?7my-9ROvwpp20VK6Zgi7?b{bKMZbunjBAPy(_==k zUsNY@XLaAa=O$Al(+vkgstcIN!7PLqDN513tocXvbMT=&i4vN^C?f{%m1&mZZ=SmT zG$((I!-0gD++`Rn(KS=HNukPTGL5)ByqR$9Q4UVz(Uixt1CZQW_3vVDmRZ2!S0stmDk{~-~P%$I#frJ*Dle=Jv( z5PR(F*8soGO7PQ)It-G&BLTK$O_~whkYRiS)AF=~jq*&gwnYe3p^qdm^z5R|POr}W zR79*7|1mY4W>mhkP)OBgl=wln61p3&tYV{baAmC;H?=N%Qup z*=U!BY^7V_>%c`_7WTBJU0BR?nf{ly!jW8Nsr5fc8+d;5UsdyO!3_~YI6^JPpkcr5 z4OqEDn-$p0V)3j?lnXw(%BmeErPM=&_*~{N+Lqb_vJH7F=7m>nhS(`#&%uTI6Wy8= z5am1YM3=iEXXROjZ?um|*^pUFv@a@(YOCDNLzVI-o3y;HzQ*?Eu2t>AdT>~(2@zFr z6g&O!IJeX6n|R1x%=*$XrFAF?PwI99Dj40?R9D)W-(^s}=`lW04CE{|*B8l*vqFYC zfQ>uqBll=3t90)L zx0_eY7}IpjEHHSdG`~jeMmYS`{$1or^r<_Wd_))Boxh?2*gi*5<)?8)p|NzD)k)Z; z+Ed?f8H@G>xbkp_io$(YPKmI0A6%@If8!A%UCeS_$l!srC){-0wlm%g`^W0Hr^J;g zt;%nN!|i>fwMu4NXaZ3jc0!&EK_O`a?|HFVmH1|G3LfmuKy6iDTQh#X_;X06NCzV& z>>&U}kkvM1uW@aRDbzm(tsuxd!fr-r|y}-b#RD z&1qVss*Ydv_rFBH+=O|(qjdQK-7m@N@ghB%=29zPh}hhC^%|`hm5Xb`94hgfz`r}3 zcS`wbw4YAEZuY1*DsKgN20WMUkwz&(dg42tCtnj*jnWDTEg$%eD^FzUT)c`>5VT0 z%)oSZN~rm)ZZ7b({p*#$@T1=fBt6|9M|MHc;lV`s>1?sYUV_9;Krph2(+ghZl<(OrkCKs(lD?J8i{o`&+;J>O zM&V8!Xf2Jp8vo|3-cxabbH7&h*#SWKh*Idqoyvm1dtmS16C$~Ril(XR;@0lW5M!jV zL~2!${?<;m^qoby2HiBj>Id^t>#@g?h7}6eqm4|IH8~Hx{6GV1JU*Ye zTNtTEW(LjiV_DY9fCfujYFcVYaXs7uh7| z*Yr>=pOjTQ5fvdihunppM!Ls55HP$by^o;k@gr=B+`Krc-p-p!JKqiHYI3}Y$yZ=I zLQR!&A$loKJ#KQF^K!w=FPnn?;%6{qsMD5KN5IDRaZD4}(v-kkC3Cup`)yYK{OYI@ zNEG2BQeNVfyE7od*qgd#8HwzWHa&o71lIV#v%tj`KjBlXEAD`fXh+B6)6)#O9_N+{ zaa%`*DomM0dDGX8+E;9h?4yD7JTpP%S!0{jSm+3C_h=xTBsG_6+*-aOB+#Zc8`zmv zCVh>bQ-5#5Ny9Uz-5Z>)Cp@!$JKlms2Nqchqzv=Pl=8B z1CRDLiu)%}k2x_Vmh0np(mXQq3{O%sv&VWelX+sJOAm=P-5kx^z3u0m*2{vzX9hOr z`6LDxq-lcvbzMerQ#Uez1eOjn`-<1mv5NA@u!X7ucntP?`B|nW7AqBCGDX+XAS35< zq>bC^X;f%nbySZR4Ob*{t&EKsK&gsUGBsK3JJu?1rcu9bj*nqe6p6dpCU4%_%_v^aztQ5ENAEJ>OX z#re!>wzI{ZHkDWg8%R0E)AgrJOIV#jH`dE|e4$I+oI09>_YZCGna2?bwmyu^PO5CS zaIt&db%7`9dtG;wwR0{r@$m~W>7{GXw~7&L1*dC{!7f&5L0pgTG93vGF~q+OC+-n| zD&KQKoch@qhCSzWGLgnJ7x)vYvw}0hG%^vR#X~jd?(elfKYPn+()&A63O}~ykU_x( ztoVABD&F5`gvPAe^@@!JWu^j+7d~0}kF10vNdKFNa0JysvI{slA7$O|s^Q2Hu-D(u z6Wkab3N2?(5n3+hNuEOUFy+)L2KB|9YqZyR&W;0f zUxNklsolF=-4<>>C3C{^WPgOC@b3w{^jLW|VBl;@eA=^bnlHt9;IJ_^b6#;w0bT%Q zhZUVZhl2_zk4LXxS4MT}FWvHoN|H44fpF(?^378D+zq>w1xsScn20?{kdaeN|K|-OSd3&^8O6Bi?O$MLczChh&PIYI{lge0f~$1Hz3y6IgEQf z*jcO#@GLsSu#7LsZV_~ZO+U$i;#`*|-Il@)$lc3KM0DEM9zBiu)H4f?L>eQiS_*hG zH`!$pS_SwXPUcnk9gpWRFzjU!9qKbvGj>>_!xRSCLFSm2{sc$IQ|gxTSrsWxWw=Q6 zk8DXB)~uZOlmvbDd!ZnMii%=%Qzn3HoCbsE`j#Jz>cnunPr#=4IfBCv?#oGRmx+U6 z6)73(<=dxC3o>s~6?h5&x8E|2UV8iUTC>Y~VkWzt+GmkPwioS*32jQolh^PAPSm$k z-NM(ZeK-hNSB-2%HfDO=SNHBZJl9>Rh3U`^9tp51Fv0EVLATHI3csKDrVS7w`Z|sR zw+-hfGZ;X9YpDac&;ztiCo&_(xwm@_;owe-y(q$~sF5T=7xm)$fX30?weqJ4gPYI%UR`BCxCND2% zzQyl0eu4vbrovYN5&R{wxf(~M`dr}rZq4Yw`7eOHqw#l_2Qtjj#GiSaB9AQwD4o>G&%ux#0KEPE?kskPJP38xEQ`kP!^`ExK@$?Zz+1gN&a(sB5LDW;P^-1 zk~4+296UsO{s+0_oHTs1jkNtpHrs}!F}sNBc~wVUml@BOIR;=U5y z_2F;N*1U-~{RI78Sg44L(i`lbFb^3NVkxBFd0`Qv3?6lzq;X&UZ zq*CRtV{Ec)L!*e@CLeYh8l2ol&jvuGR!Pm-uj>V-+Ixj|(3<#F`%dK%xq)JpGIAdb zIa9j(D~PxuGUTX0c4j?v%uKw4nayKChSvH4p;$u*hJ4p3`E_;$CSY6dVa2j4JefLY zWh9GO@`S_DXJPWX{+=>RnJH75nk_y>egTscbBBCB31etmI*s+`VUrA1yU7Sks7vM}Z66 zTC@wdlSx>zklNr0yp+YTq|oKsS^E){k8ql?U-?xQB3+XjgT9L^V;g3{9?W7c@wi@6 z&qRH}9%arUx}k2-s5va8EBb8TuWNMagpimatm}{cXD`Wnw0UqaIV1R zGc&%%tGf1bC-?Gnp~v_KZ`ENu9QMj#^G;a#Uo@UkXFOmRQ_r8d%=A`gPibS5g*|kt z0ZB1Zc|A9%LpmKylq5U3RFs=Q7WW(fK6)DBAtKVwHdeomhyXtdy97bRGKafg3(EjOL zrq;3~|6m-L0;4cu7`=p12Bg8L`NIriWKZ_yJA>pKFb&|Oqe<6LVu@Y7tF`B&nl(jQ zBuGhHPl*#{FAHZLvG$U6%^Hyo{^bA(VBsyn8t93leKc^Al%udUO+p8TR|P-Rd+!&bMR4TKC)O$Tg7y7& za=_IySvY)tw!4S`+t;H&U*PYzci_M}H3(*Jz`%fgWPx$eb;K&+_Z2>c_qQyqv$J zZ&UWfR2t1Nqm*>+hnnmX7(Zo$k0l)=LUnWRefv|rQyqm*5<7f_u7uh&>aGmWzlpNb z7{$t@gzsuj7JSAe#oo#%yriuSLeE->>Ebjjq6;K z?maT0=aojq`9JJp^Zrzsa+cS#70IJ7PvNrBt&92BQLzclW{&fNC+rpajOwArPW7$N z_&`M{gkj#4o^Pj&^&S-Ub>>rr*R-#hndlEHKm$bY+Mh9hM(4Q_+_HI_UqKwOXFPM}~jaC%P1!mlA zE)}?BS;4k%LbWa)m(-F$VeigxqIW=xsf|U_j%ahp0Aw64IqH)zR;TyjZZlJV`83fu zlO_f1$%o*G+JsYk)yxKQws6Kgv|eG?tW#>ut4CRBhX52*KEKsT3@Xj9nn2lJz{<%0 zB=2P#yU}qAeI+_&M2q$OQ`3Sgg`|Av7+1^KEager}>jqFrp}>7t#g zK)Oll+MFJr0HI}|HZ>H82-Mo+Gv-??79D5m><5v2Kp<aPRm*efi)NBLu8T z9~9>_Scv@XMyKp9WjDO77Tb(K0&0#aL!>-8wfAfx3AaXvwwTvW46h=(&Mh$ytv2!4Po0lgCnF0d2bi^XLOpJs&_k@RU7Z?b8Kdj@EQ-W~=S9AI5$q8N3_@^(JsT8XgFe;7Ep9c4^>6qbUr* zd09#J+xdv=N5>HzovC5IzN;{q}~vkI&Qp^iUg9fo#ZExe`;RLqoa1#aQ?s> zChL#dHSD>zT5t|_)6SUF##F9FSq-@G;6|&`ihOplP=54 z9Y_#Scs8EA9^HMHWt64fuOHgHIX2Mt(Y2bfh6Mo!9(AywP_{s<(UfpY?=OSZXZkT*r3G=QDzT!j$-cp-R6=zID6KkPIVULkix&QFJ?-xzKW7pO^hM+N;R z=u9O9Up(N{(gj&Sb#w<8etJ!3I_B}w_!R}o;*>pCpnd1SbV zWT+`slVH0XQ;Tu@c-QxFP~c;pzuO&fL&o^1Ch`JohnUhtIg==6uZ+3=q@8>>O_nLo z%6;l^*_HPVTJ?*32jwF}!1k|rAXYm{dbWqHJGyIeuwZz-Vbe>l zidgDo!;WWs3tAM-B-0q{9$%fT?0V2#u98bOeQnOWD+3Yio8!{^CM5VIw z`@!l4Xa;25-#VB@UX=()GK)Qkg};5%$mbYd-}Dtu#MZ|{ zdv$D*A+Vnbdik>uGf{OSm*m5mT?I=U<23iOaCBpSlx>n-0qviS-SRt-I##abM#izw#Ndk_{^I9DfcIvsmS4yirF>J?{8 zlYYyTk3}L>TO+_*m1rB=#-{qU1=8*_V_AAN)lqsc@UYt#NVvs9#6c}tTCDni;j&sq zw%dAS<($XHO-hGh@6S#hrtwltaG@udr4aD3+sUBsMWMnzuByNf|j(> z(BdEBI5PDrrw1uH`h{+ZaSkC4P2Tc4d3_kaj*;~v?dkA7+PC(ndUG7E`JF8T|JFjI zT+43XWru==Bfc&&oOnaNRkALtIu&^coCi%(5wZ` zH<~ivfF!)&gw5@WC@{+^oQ>SybA?gAMTAF&L{X>)oZXd7AAbY`& z50-Q<@K1I5GiAp+=)Aqwma#2M*;_JGz7&w1kqgq8hQVI~smXkPQPB}>M-*(O`>`-> zMUGYb#xx36E=HE;DTn&C7ugQIp|HFzCY(Kn6?J0D_ah5(a)gZrwnG=@LX!{7ycv$o zC3Fo4z89MR;#JF1W1x9<>a@0hI$QUE>7Izr5|X zAHsCD@Z#qEU|#%z+A#Zq7?BixanPFIth{&d-Vd6$e|ORjpu;UkZEHs?IKU{x33EkZ z5i&wHi(VV3llFvh2^NSe(b;n1WAUn2)lO^Bd;-*1BWeS2((v!+_|WD@!vPuwOcu#9 zq3iirX6aLJ4^(k^?ru5LN8iA||8l3yFBp_*ynPeitRg;!#rxAmB%oTBGGjuf0bo#p zr+scdG8R6=g9>Pvh~NuI|E#5w+m-qwjQxFkGFil$63@jXHs20Q?&1Va@dmiywvock zQXw{zVg5$8Nf#eg`{$yUUdDYfGDF^!1Qe?sb!W z)MKQTcQ<(g{DFkE7WYIino|{5k0kyTmL2M~h)(@xWJ>hpeNKDVuvBuu14sTzBv&!K zi2a{fRUKdd^c{W(Y~p`>AHcExZ@~5c`IWo*58$bHNbvuD&j0Nzo4olXy3WTRiBdf8 zxL^PSj)~)_YWusar!~ooa{Us9G7BaG)VKoz*8lVE7m$|sfWYYH0-6QTD6m-VW{a#3 zo9UBoo>m$Bz*9!DImM#o#U9-W>|*{Xh7JLX$jHC4a#A-yl;}Uv6_zYU>!luEi30Gx z8!ClH&3nxm*JTu)*xjtNO4S7Z1IW1lvKD6CtPL4muYp+D4o=;j+84nL3?vu^&pLDn zfRHpkeAlXPUy-o;pUr#I17L9UdoUxH=FmF>rVPwApHV=hSA?>&46q}uM7 zp3weiK+fwn%+*v>aJPYyAyE=C8P8V=wAQ_pN+z2^f(9);0OWtWS%-1W|DajnLZ~it{rz2v zZPVsC(An_oOgM>ON{PGuZy`Ax%o*EH+K&{YrBOLwT%e>nt_Z@gF&Yq8vH{e4`NI~< z1SVG2Qd`Uh`f#x)%?9m%o!>W0vvi43u5?0-+k=kxwCOh_^dyqU;7c>tW5R2M#&Y8{ z3;AU#CEb`-|3d3mbm6o^2%6y(F60ZHIrEi#h_c_;4loJK|MGxSGnKo7~KJ1~>;eBGsSdPG=|IPo4JL{WkJ zGFoz9K?ZC<`@jMXkk2{=@ZyUtel1DF9SZdaC;M?U!NH%q?AIBB-LV4BSleT)K zlGkCacC~mm2dn6}t~ljG5()O95Ar6Ajt=&w(kbMFf; zSdV{x^0qcI*eJiTTVuez9)JvUcel{4`50Zb{{bYp0)aSXYcsotKqSNf2od+&Uo%z^ z75@N~RVw)fwur^MrRI?aN)zAwUW)qpx^nwUhWV6WSr!COg#j>L>s#dR9& z_}Hc-ItzC&lDmFn_}8;%z6UjD`%Y9Exj%Kz#HZ(CW`mz((RJ^_Hc6$ z{h@}*?rW0mNuWMs=Y|ks>m$JURg8aFjby#cj^D|EfrBI~q+#K3vJ3P3@F(I65O!js zb;Up!KYVd5TKc9JT=~uPl6qSGp#jH$Jg0%0!Bx2@7lukfR}+KhZH1_Xloi$a8Ah4! zE}qP=6ZRHbB*>5}fYqT?r<=ieQ2A&PXV(^5#PZbZZXgfJ89F@ugF{B3{cv*o4G6** z5AHmOYf+_#vX<4;&>BFw+V2Bbl9<qvF5Y zpl!9N1pDDjPC4^owouDY1yH~`vy6J@GVP!)1DE1*`Sfb6>%5I5xE;SLk#S|78d(++ zZ=(0g6>;8-mvJw(`q^I*t;C)TzTnQwQGN^=nxe84qHA&&;3`?6-SzSUqyZt7o2i}G z8-QD0$2Ezm`ie$jsusP}QxLl2yI ziZk41iJz0V9y@l(VN3y%0=C9RZXeAFoCy}|T3vy-a1`C7AgBoq$aMSn4%8)1A+3DB zTYsh~FWxbt2Vk719i4y|;k0T~5S{suK7f}@9eZ$xFkW~yB4?(|EDbnlFM(vojOEqU z%0H7e$(Bc`dka1QyI6L(=;4wmS*8drg=Ym`Y@}A@8ruL9NAbnliynSJQe|do4CoJV zI6rOP%2EEfWRvL(QEZ*Fd2khE24qCP0is{JpmXzQR15mASAm!TvM6Fk&s4%Oe?j{r zfZ(TQ#0rwrR-9;W=s2p%H8Ir}49LzJPkc|IAlLks>>`a|0klr%eYW0rp2M`V8iS+- zby4)orK%MG#=QgSr)5u{quYgm3{1;S1<|x}S(Jc0VrUw;e;BRPS$=AoJ9(wt!~%)X zW8W_%{5^N)RpwI2d6uoYrDJ+FZyC=n;|rwu6@B!Yrsg7wGD7wp<%a=7!mM7uF91S} zHyD^a1(9xQ))*|w54J_GI($JH@<}pcD?T+qF*GTT%d&Lb7~+>dDnw7dLb!rm5h?gs z<}1^_L%MO5Y>AB0hdLoq+%M;gC#jaXTlYx3P$?hFk8k#Q3(Yw3yC(OAa8@~VPxnS$ zW^CEVy*w{&Ka~U@;Y3^3AHPxRA z>H32peq)-EmybA{{S)Io@4?F(G@pX@yVLm%3^}n#LFfeK+g!kiS+I11Arj#HcLs+kz#FgXn=Z@=7R#_0Lt?Gini_|q=o+a%kvK3#`Dlu|SgFIRb2N;Pp|zvR5s@6Q{%#wvvu+M`KQd!Lvfi!^5c< zKyQ{nk_EPe^XN#xQy}Bv&)pSoNa93y*o+*R<#hi7jwMojXh#8S5sEP%*t8dC8_RbF zR~C7=Y!!0i7C8|I89#Z>3?+Ih&jMQGrq)Kxr%uj$C?+TCyzu&=%|{C|lIQVC&xc0E z*7_Rst(Vtm88onvgm@ilJd6k1E9ZU(kq1rYd4an3x||(iS89=vJq_l3i_1n&d&Ej! z4h17f#d}RYf0cs^+5jCo^D)q&td~ao@Fcj2GIWKhA+y$kg!9}7_d^GTu>8O7CZPhV z4W{|)(n)G7tjqtgA!-ab8cvxZTEK|lC*A4<^R~VOrm4AAiAXrKk5$4|7okRZH!bBl z13{NM_gwokBcFkKLcml<4Z`{jxdXZ>D@-E{aixJ(?U=ukeEVgYY)NCp6L7b1UJYW z|I);ld~#JTBOewLG|Fwm$n`rMw>~|K7nJGx>T)mC^Be7t=KMe}dp~->hwzShN2^v1 znhOZqf_Rn$8w4cQZk3_3+?4-FB0-}HN{SD6Ja6?bY$`U=6y#*A%SDdzykF-dxwgcB zn-S#XSLl_bmxAmvajr2$J&x|`bk5Y&l2~GG%-=!?Of5xWpVl(S{p#p`zlM9KeA1(x zbligOQs7>@bawaUj!ygLGpYR5rWlofL!1{iKYw~M8~<6$yL@L4(%k|Sx`r+!F@eqIQRXC=#L)szRPXyDJ_9Zk zv(QZr4Z7eqc5OM?`(3h!s;d!gp*7D0!ZjRRnY1gm(kj^B$pm&se*zQ}PUD6!ps}qj zuz~3()65l=dh)2?4lBk!N;-uTa-1kX^*qJu0&azd^g<703}>Wyw=Z^pb)p}d%;oh+ z@NYdqh|&$XdIR_gQDqyB> zR!CYXsz?h&X!MVJ_v6{a$e5D87w{ZQY+$-GTTs(fbBvo6Qpn@HQ&(z>j49aUv?#Gw z^ktJ_zC;vU*5ar=T=H7+H(d70Po&=186{^=RWsjd#ETmQ3wjod@wcX1hV@Jy zkG%CvLe2&!ekW2i^Y`$!`AOVpemxvs^=Fq1#x4q?B)4k&c$7NJ10-`3K3i{ryXBnT zw=+Wac4~2@mEdXjO6yVs2uQghK}XT1MXyAQCI72D zXcIGQyaIN%5N3$*0Yw8(ej+C2b`K?|=jBbK!IQm2cl;g$kTS-~av28UmpZn9=iacl zkc3>n^{ypma&n!NcM26BZQc4bz00Ke6hOH1dJ;7>eYNbcZW&Iw@gmu@HG;m#dUo!~ z+u0}rOK-NX$Xk#!?)>^c3MhnuusCT=!U|KKzXPKzcEc?zeJyOQjCQEOA}mDy;N<*@ z*Y{()UxdodP-x=*=$<(xpo-ZR2K2GmOnBtROu%u*4=huO1QdLVvIBckPm##m(u79s z$#_M>?0_tzmO*YS&%&C+tvD8x;HcZ47Oa1$+V>Vd{cx?{#u`yd`Hi0k4ouG>AO?O8 zrG2;)Y>JCqt@COZtUGK{)__Mq-etQOpcV`K=tRZn4iMap1SD0#F}aG$=WprefOMr^n*+bb@7 zEKZzCe8#}g`2(Dv6cPSWX`_+`iaXwyYdDMe zt$jO8*#*LR2VT*Ty3;>2b!jJ37J6ODAeN`0dDMnxdIbZDZ z&fyj})M76pG$Iea)yBf6dbH!CWbU1sSX3#4l*qv0OsMx=vqtCa@KeDR1_^4#pog!C-XD)n0Ew$1y! zlJ(TQ-}m+lnhpq?J?y2)KMnx_qrKl@K``&+u?S$6C1#;;l{~=uu@8HiBo+N^MM+t6 zN&c3EY8Eg|$i-@HbJB;>JlE9+TF^Ag^L5JEgfh%t&*W;%QALBn1W*C1$@ol+piW>{ zC|F1=i4OjYn@x46I%Xs zO6J88;8?Lq!bu-^m-H-oAH`Ck@swhF)u^Gl%TKKv+MCjn-=%XU?u7Pj+mHr_4gn{u zmE*nNWT?8u?~Sqjw)B5m6qswDZb(g!0% zf6Cv^@_t0OqpCC#roNMWVBgYNh${D`Fv-v4uLMY|nMN0wY66%;;|q8Rv7XW>6-W^z z|5g3>2DVo6=L#(u{|Cv(FB+j@}`mnTqifhc9JnYvP; zA|TAvCoPBT5O61ej1xS+*sV4H23+_p({oYOxxSPrdo8eJ=XKE$0+#b7rn;PQZ%3o{ zi%_$>Mox`r>W#T{+vQ%6xm#HDkWU{@lfja#N={wxR%05bO-wG|eU{ z$*C8elwTj)+ zOI^kR@lV+{nFd-z%sC{pv}nfEbe~mf@Quq0wQ2&kyW&BI)Ak%XX!+xd<6(Q-LQN7! znk1n8B1pOHwG_I`BHd37`Kmpkcr59{(Yx8SMPPyN6mB)1(_(ZZjf`6uF7kB#3hsb!0N~X{p*)yp6PRsxai0cJRZkk-d0GQE01 zb}}f!XQlfo>vMove{0l6Lg=yZFRVrW9{S4*u0};Q>MqKK5-(wx3Nhfzl6crv(5xR4 z!~p5b_LH32lX)`v+Qc3rATMC}d^N9j(=z#s{nwM8EwL6DYushSfJ(X1_tJEvGPA1s z^U@m6dI5v>d1ztc(l|3XM8Dzv$389Bv_i_nHdSpd$m9N{_5ctgN_5 zOmpf_XI(8I#4sqDtA#VR+hzO`O0uBxN3Xu#oH?LuOOpyb%M)uGzSJNqZz#eg)7v*j z4xUWazz>RiOdzR-8xJ;yZ@&P5bgp+=3>m3Tuj08<5N`bKB3?HrZTh*iXnJMf{AyVC zOjTrC8BS$LRK3xNTfZ#V7zHH66c;yh_^C0CB7HEMz&I6r2VSkV|!d zHE;4c`9&;DgjtQVDJLtaV*{8Be3MV|yj5mD=&wHndCT!AG@fu4yscL_C$pq0z)9V3 zYj^Ju+4V=FzvPCX&=v*Gd7`5x#}yJgGlh;4__;xD41z4o6+Q=icBnIJ7<1a&@~!VV zpqHR(!Y}ZYoyxF2FUa*9|Kx{^6($B$P#DCpZ{g;v$xBG>O1vP?IaTFrt-lUiQ2){a zdYz0?zL>&H^|zFjAC)(h0MW!Kd@W?Q%%H2ZJ->*xG1G_=$T?{!S?h}GDPAl|!>#(K ziHykJRc)Ox)J9w<0)iDwpZ@YE9}6IG*=I=W53*025pS~V^G!hxLMLTbHqSDilQcql z0tP473$Kkrz*i`)6Ap=1=4%}@Eg>^@n#+yCMqPb#97iE7H*=*}V9oqK&OXJ1NE+bM zyXM1ih>^ph0llxC;?O9$E;eB__9gTE(P#78aJX}L;ovX6&RAEu$w!g*R7G8nIEf@q z%GR+ohj>~E{0@*qCrM~kKzhUuj3JvFGxJGxf*Yi1riV_8TO+tQav!d}^W;*;@Q)^+ z6YBO^{wk5bIEa)9f`ny&Q~LG28p7?e7Q|T7E7W=FBA4nWPM5YS}N95~Ffu!LoGF6Ak`_ za;#bgB+(8Xv_`Lzgi3WV?WG5#Rvv0?0p(c|st%vF-5j+jZX;8M*f-jk3N3jk!Qq6l zwr5R(*+Z^~Zg9h#2Y)-)tme=qxmrDt>CWtywp_H&;HU2-V{sEKu6#6SL6GD#c1bS! zpq)k6z?z(62bhR*j8~tILV7c{O6|el%4KM@5=*8pgI<U_ndf+5$FYA(8`J1GscAj&^8v}dE3s-jjZ4$g`91DsR&UrqcSl~-u_Xq-U;TiHrPTMNRUq;rNhBO;rqU%%DI{i<{b?1Y43By z(8@y30hyT8`rFslyLz|x1r3cqgzjOatuvh)2ctD1wzfNaW|!xQ!e=z>DLhf64^oRY zICB2D!5uqQ2I3`?;kYMo9;M6$ZXCt85 zMwrWL|4V$Yi_D}9NJ8iN`a59d?WtfuSkEf~!s2maCz0Jb9LjfJ)gvLyibnJADV+Z% zX|wjOuRGOiIEe{4tHhAQ41yp4Qcps!KjOadMuP)*cr#T&1Z%OI+1)ORhq#MaS5_=$pG@ z13px0j+#ZULgLEN=mH%*uT@yLF|Gb~6hU555ELy`6lvjjWtnWKcH_-JpCq2CfCyGt zXvB=pevWNhO;x!$NZ%3!wFiHoU$ZQm*Wx_zx8K=`w0k9Kl_WXrxV~>sM?Kj=IrTlq z3udMp^I^uPS--CBGMl)87MZcK9h5}f zR0SubxnEW%#Vi7s^}#HNt$RDWE%jlfnK${yop+}N&}tfy;d7Wk5Oib8oz%XH$);%5 zHp}Uzmky`%N%5p{tNCA@7?DBp=XX$PmXqOFzqX4CE6T3M$=UjOB$My#qT9q z3ss*#Y^H)Bar6z-C10DDajmI7@_5tFJi+^tNs_AU#*DQg4&3IcMh0_zZt*a>E(n^# z&{DP$U%b$FlSU352x(x|e=ynhqxX=KF=V|Wwp7o#g~R(NsG>ky{FS2~yQ*CEHA$~) zRTP2bteTwhWqstc!AoPYwRlbT4uXp*1XbyryGG<^eMfi!zhSghsWi-2P`6(`)ezg@ z?OfHu!82#F+@cjd^e8T-VDR5fYKPAurPJ!aB}`$u2O1$9Yww|fE`fAqY?xgJnHs<5 zP>YhX(LleJEcy_v99$d~h+|Z|`WYG}^svHk(O%fu!Dwe+iV=5gohH>>)PGUMjQR{g zp=Ok$Ia=p6lS6!=4g27$WdA|~?HIS9+Y1c9`K?lK`_9kg(D?@OL`il2#f}0J4&qG; zo&6!@$x)j)Y1oRMxMul)1iJ7!q?D=ma{EKIdkri{?k!i7Ml_&n+`h>rR_t_RnaR^4 zM6R{@MO6BHSh=cz%s+v+qh40r8eF?pHWt(>xq6qs6N~-|@3B## zdRi`U-m0qJiLrIfN}oAoTF7F>CRW?Om~b6Y={_bScA^_L62spckY>iX04AO|;+@ZL zsln3B^eN|!!W}*A+`SWr(a5m8#PAQY9#ScX@SyV1rh-HTH`%Ca=}OG|IzUvsMB`(d|zxtD@(tw}c;fhOjz`st(F<0m(-37ce5&Ga}C8!zW$_z8D0t?5IUeDj*wjleL1?W?Z4U= zNZnx;r^Z4wMVcz9u$n^zgWA+S zBbs5$#$I~>eW?}}=KEw^o7!8X%ink*N4WZ?ku|%W$e26Y*418l;m}=BaHC2J_;mRF zuQ{knKQM8pj|@1^sO+8@!m?v=t;Koc36)H%C{lgcxNWqq8^tQ}(>o$Y8CYMZT?TfY z5P*8|)vpAnH)((sn3Tt^^_CW7?t~wCviE`MQr8_C-G!qxRk$x*-}nleJ*d8q=h$I4 z^W<`B3lTd-iM-MMJ1D}C!*x?!k7c@FrcmVMdus8UF_OZ$^Q|5YWEJlxu&jOYd&as~ z8Tk0>DR>DbHGKXDx&G^@H^4<>^Pl7@S?oWLXn!(W8++)HlGhE`;>yuqRA;=vvw6cg zF1oDoi9nFXU~kj>xv+@GAmhiMI!QV;RQgw@Iz=7V^)sIj-5R&tRsEdW?AkmL{jN2l zngN9#XPwUYdxglwviX{p-nfVAySBdc`7Sl{0@fF!(mR9eX^f?L)&uF4IPs zPxFd&L~Q&a&tK^=SI-Q*)^kEwa@n5~^@(N!&LO{4sJDn1IBgd7Yg*+1G zvv;WxLY|2GrrFYV+|tZlN}kf_vtHFKJwDPmo#T~uU7LMAI(z>1c3#{BF)xS`pKZ-3 zOEXtqY88Go(_qJN!i~qT%In5T_b+vBS|kkHd$C)UTo206&GwpjmBo1tQ)YN4#M30( zO}T=>WL1CdV5>Ouhn%3tgYd^~=@38&s|Thv5a{uUeBu0ld5;Q(boEM|AN}_AIP{rF z+a(_rwsAp{ZKvz%swD$Ra|R)KJnkmFVZc_ZPQnh?w=54@b>`ps5`QBk97O-mfNV>` zXib`VEEYxtYVOy&&DU43-fJ9`#Kxv(A)b%o`eGu%FNZrGu^!AQdv)=L5EbB8Zn%ku%K!%yHrbXCr4K^*A$ zq1fsu(6O^pb^j}!R`w}JOY8=<+VcX7M??LrdXUwehTNoHkQ=(SZQ3k$ED9)vH|SXq z*412#l8BPm@jM%XY1Ixb8-(#oyd2ME`(H8gMYG zwpob3ZS$L!U+qG08=6YEOtSJe@?J*v^2ML=jfMNtGtmeT*79_8_U0va4$x3RzbSRox`e0nV7Cdw z%sH-#LjK!u{mhC{Y33G1!Iw>(s}>$|)*~s~1O7`5|8^A?b2fveHlyP_B1$=?h)>DC z3n$cKLIw6yl%~^DrQl?w!*}G=)xR{$l*Ds&Pe&VOKa#E7s+OHni(#=_1OKK_UD4Jk z?XhrpVAID^UHt`i9T{Xnz#>Z8dOlK1{#??(!}NWLVFrx*iYPX0Gf%6`R3lyf zSy5v%Z|0cDelU1BM@Y3^sQ4))!v{c@nnc2y{|c#D1}1rvrV?M}%+8Yb?HSGA?&tX0 z!x-QKgyx5aa*jfay3}OV#qI-ktR5-`HH}}u5jyU+0VafW!<3JZIg=f6yh1Dhhib*20VW#7i zk>dX##QyB?7}7-6Z`p>|1f^-KU2VnJ8MYesX;4?N1$C5Tdj7!5#TF9Ge>A?(%C&pI zbL3L1j{#Jdt9p?$Jo%Qm$TU&(WRCc$w}&>n7=0{8a;VndnjG`9GTXPe&^ofPZly!Lb$6~!Eiq*YFFi*%+axWw83&3Y)HcZY z$3wZ4#?e$hYEi~hkT&;hJ}APuGw6CIoH94Rxki7HoBgIPzfR+d__|)_Z7ge?7kStt z5LfHZX_t?kbQ*Qn8EvZl6+4d`1M)v(W3ybL=Kspla_I`jaHTIpo-jRthAnJ<@9<#C z!Y6GzhS0b>OZKxMV?BOnFrur=jhbAzaG}+Jz9#YsmghT{9x}=R&Rd^@fBc(4?id1) znpz)OG-t(R9?5Z&-_^Zs7B2A=_QCIXx&8q2U~I41BS;3h>yi1#jForvFbVLK$)!hZ zm;LK{S21$7;R7@=UOf0D=Sq9oN%%Bl<0>E;6+i2#5qaCu+x z4!*v9tTcKmxNypW0^E#&OgNt?7=%U8acMMx2YlwY(i>d_JU@$S!E9JqT8}&uhS?n@ z;6(`_H9V{qr!QqHpQzsUO8~alGXMkS7y_0GG z1#sjMm60D;*T>@}v%Zw_C7uHC5I~74M$2EQ4ZOhBsL?$tw%hH(cnYI6_OcO)8cz=R zdUp-b+9viH_BKOb76&?4jTA?Nq69`9=-fN)~A%x?&hXNmsWkt_Tk?}Q_?WM zfxi$lNJPYl22rRJ4 z^8n*j9Xu>t8JU+zd~(;S40MV}GN%N6mIZ9YW`p;Y(68?xSO$Qu0YqgT)npW$@b%B9 z4F$^cK(VJg;OyB4ZtaYmYC@+du|6?l;(%5Fp!91VkX{7<Df_?LdPeZ;+?`g}=1*-%`|DV6T{|vI~An-4nZ%+rI>$^6AF( zRHmA8UopTRD-Aq;bLUomKn}3tcaNUjwHMtz`tQsCmq-5}Kl1BIlQ=A>e4Z`#%7n^M})nbc+Dx z?pK`$&|WnbDPWA5tB9p=I@%c|sK0yyXwP?ntDanw(x(s#cGx$-4!xb{pzJkj1BD8b zXl9*YVR0%X8ZhNsTN@k?p8r1m033Y9A3`alttHv5=U>m0-^cncQ<4f(;pA}SDiA)s zhowLClM(L84O4lUm3)8!8Z^^7?AKE^6gfGRpKBU+ptylfPYUK1GNb0CSdK44^^D-7 z)R{+0?CICP9!gOiKez<;OFeJqi54O2@KZhAmLx4h8)#5((k2P`G&`8zN-22M5c{{=U*NZGoPCeO6}w&-mwh zvlbf9K(L~Zk7L-5SVW+}`x0F6N^|%d|6aYBBO5$scB!nnFf{^r`xYRa&17rft1+{s zJ={j*&4I}%LH_nOFe>Gan1j%cHz(tpV`j#~AP$0Lt*9g#S&hi6%rOG2PlA;zFH-D+ z^tSk2OnhM42G%eFcIKOgKvSMjV2@M5A%+Or4y{oTcIJUHIV7wC24Mns#CC#;fMKrL z*#f@q5O#pA{Ke3gmyJll37;oxi5I(lv}FLK#LO*B8+^;owP25T28nt6n_zZq?Wo@8 zz)_XAjXV2QM_%9BIz1SC6+zNJop<-g6bSR!)N&VDDml$HBH!^jf!T4P$lpm?z-Jw! zLmiloX!(C}_TEuVrEU8tGZs{e(xiwof)Nv%6p@Y$O+pW$Hw6&^0jWxNP?6362}N2& z0-+Z{iU=xA0*D}ppn}pn(xk(=H#6`1edqjsf1NdJIg4ax?`J>duGe+nX$587vF_M< zBKOE2X6;a!`8+B_XS>8^v~-n@6W^?bMo(*?;S`(pDqLIi=z}HvO8endBJAPkofyT* zsoR!z5S^$S_DQ`0SD_rvl;A8lh4KZ>^&BW<6A_rMY@x#9Zn6CKDoUByb3*sU7GpHI z(KS^9E;n>gdV7m5gOmo>2~FDSdJL>{Br^tABYgrmbrOFR*^5k_w`1*@C$(OBVvkv8bB>a0YY=e2aE zc6JG4gmFS92tJy2O?uWnDCe#iXf$P{$N3@^r;T2G9uJQkzhE{C8qfmNP(G!s`;m^d zo7Dc@H^LdOb*_3oJ@6UJE)&413TO&W7rdb+tGJ!a~0 zKYgxsvaK{q#j+Lo#*!sM@s~i}sP`^D0weGoozoMo)3pHlz6Q;;OfJBkHdO4*q*8BF z(Y@%f)h(F1_u)ml;GVeHOXKWLDv@8LGQTVwf_d^Zax9NA#V32|aTX34>)3{^K{0L_ z0j{46e1_TIww&y4juBY#Vl#5T(_BeZrBkFO_OlxrjrT4Hq&ND(45bl6a@+IQ`lu3{ z0Sw$~uJ@0@J*~ZxsO=#=#ab^@aTW@w9M%JP8XD48_eJ7(CU(1oLy2srlY5x$-%lMe zO(fjhW-l?E`fjJr9eZ?uUu|7sD6 z&bytu?7}Tc(aNKUm6GB{sILR9uQ!A+40?eWt!p}h;VW1+(MF$*>(8veUh0%+RV|^q>efl%B z%^U=%^By!3ZjkleU4#b7Y4QuceJ>@Um2HQSGgS6AUqOn@F3cmHRA0=41>`*Vad|Ku ziiHO(ENmc6{@$bmr1_V!{FhaKfK2aR?F37GuPZ$!VAhsrq5r8p6r9~|V zKz#~oV7QFG3@aEC$My5T18O4_FKfS+*3wOb4A5EL8k#6FNe>Phq~OBANFisD%CtL^ z(5OR18|=|ooI?L|Xj`C;Zr1uhLlen25brUyQz15TXv&s9Bj8tm(=m*X(=%hk#yD2} zaO6j+INl8U6!<5L5EqP9#%d^$wV~ViY?-E~V!R=UC2>kt!P3_}Fgu~4+E`la7@;I| z;REnG(rWyOj%YF=&IygCs9?jsBvcSmaX7uOQ07XWc?TW%)Zg7u2c5CmlQS&YT)Glr z1?w&NWW|FsyG*Rjo%$y&@z&;q8TJ^)8HdH>NMyk-bu_@{k?FP2)d_Jf9xxcYzI3D; zXAx*?@YAyKcurBuD7zJ((in6x&MzcCOvMeqkR~@eyU_%U#&>@p;DpFB5~v)wRXCdb zc`BcPLz)e@$k`&dxXFXZhLKs=h_$O^;Os_H-@cL;ox0<83mbkRRvu_kkzir{ocff` z+5J2!d@am?Kd=tkDA>>h+<@8L(CUN+V$O-2P^$hQv1g#*FfrB8{UnEpe%K+lm&|ya zDm+&rDG(~cNTl*%&XCQpJ-SFUqS%Z(c{WWj)o<gWO zm0+?czq1OyKx;hHV@W#ML8vk9Y_$gGSwtwAIhJqlQcW_O0XCHJCI~DoG#JL}$1uEb zKR#OG@aD?tW0>A5+}Gd`5^i>_qDpDZGQaz6G?J1l7T*Wef?e0T-y(o9n9!if=OWD4%OY}ANeQGBbRgLd=lxYRdVKX|8s;bj5V!(Y{z zpBA=6Klpuzz{EZewn*C3s&$XM52(EFHw`g4jRVU~Bxb+U&j$J8@w6Y<4X_2mJRU)+ z+POQL>Re$MEMABAwgt5CBiA?Iu+G@qo_KbTxe{rRwY*@UN>0KdHuX3MUPeOAE;$(6 zUdPsYt`8@<(7)eqBtn0+X>1}wpe2Xr=8HE&o1ZG(rBP%;XTZD%G-`;3O-iF==M#)w zN9@lrkODR<{`jluuoYcgoVD1EuxLNnjo_P1iyff3_K*};=kF@5>{c0!b$gWfmy%sV z#UqWdklmZ|XmXZA9WM>i!9A^8fH5^af9N#qQ@r24tb(!h*&U{NUH*WY^~v8Nd8}+D zH?=HOX$tV+S6ij$qE7|CX0c_)t<}dkn+yk0|di&5zJ)fU%Xg~fkPdGz!pD7qynkm*Zyb??m{0eOOQ*7E29n{BGxul47;Z_|jslrN7U__J$y#7%6?(Qutxh3wvMNfW)%{NRfiq~-5}f!SFetBm`Af*z zHd{f2(^(_^_PZvPE|yx$Y$H=&1ZVF7&92=4+k*duER%k?@-))-*#;Yyr(-3Ciqo(X zK{4J_0iq44;ENtW3PvU%39=X+dV&1oQdZfo0^5Hm5KPTce8&=En}7?Ds{(Gb%ik-+>Ul|57f1&7nC29+3sMgIUp#&x+S zC(YZ;!FM>6XQQ~~jNfesvEj*c2@kNM-m z_iV&FK!gjbB0WKD780}av`F3h@UID#8`C!pw-ya-536}8*d;*5@kRup&*fET#uflkb zZ*{{A1)daoaRh|k6+P2DQW^9lfO8Z24k=0foe*Eia|!5#ztvyq$Ubv?)Cy5<<}!a zCViVPj5E^XNw&kz?)xWP&`>XIK=l&2aTp^$kL$g8^9P)<@04fJ*hg@DqbEfd!K(|V zmK`EawirH%l&ZPil4=^g3c?Ygl6TDE9xMqzXEndmcPw^qVT zalOLcrRLPeS*s?8x_Jl;bEoot24BJ-&Johq%L2qQ$dHqGtY&s&kJUzTtS%V%-q%~e z#9$nj;9R_0x11(fe99n-&7PN0-?A0N$Q$HZxogsttlw5Z1XKlC*8h7x8zqesGh+H5 zf3${<1cS9VsPFUh5N%s-g>5@p1vdm^Ggf}d`Wd^a!}XS=Z&e(KL{ZQ{qq6yRl*k>m z*P%%vMNmyoW2cTcV-z;OUf535y1rnJ1enG)tIY}VT-MOX0FDJ;&1gLmSJT%=7fs)T zj!A5Qt0AAm?8`(=6Mc;_0s)u#%$O1L14zogjd5}^Lqv%y%2 zorpC=-`g&(563N+UiEkA-GIjR-&2OR$Mr-`w`@Rj%IM)%P{OAkm#*|l)yBqxUlLvw zwRT}oVMz<*WmU>QdnEUyeN2?iRXk&0rP4 zCOk`Nyb#NZ$4R5K)%>CJ7z5D|N4X`Q8jyN+0qJz-$)SlRbKuRQztbW@{d!}2J@}4l zoeMCE(ok$}Vqc=4jSPXVJ$x~^psVt)sTkkJ=eOArn#|GN#%pqbs1P2k2m zI-#X0qi3u^U(GG{7AM3RVCl81fknOiYi9A#Ke`NcdC|57&T3=a_@mPXShUxsWB*9L zBpEhmL|PrVz#wz6Xi@(?=x6BefVg*pKB_3#I4w4DMWH8{RQK()90GXGuZ-rt8JKJ4 zA`+DgQDbD3fey44O4GZC0wE|pCeWAv{EJ)*-~E#{KtBHW zga3St=>LOk$iK+Xkzeg!{XZZ4=i~q5mjC+>|0ECoebWE;mjC+>|E3}SXE^__QU32+ z{{I*h=PdX??(@IH5+D-orpoF6dACnmdJd)>`ij5p&&znc79u*puU?1#-g0mXkY#}K zJ4;E>X=h_J9&zeZpsyT+v;Kyd3cLc5vl_NF5V^kBZ@1NNKeAqbL{vipC(vGM23q&c zEw=F7oj$Pt?09y+8kcW1drQ{4ZPcGu;9S>@oL;EfUTR|ljf~u^HT1z9=sZw2eyP}$ z^9w}1c&x#H6NbPmytf@|^$OE7?ZK7o%Y|A!h^f>5qscEN26`k)84`*({~Hhv47sSFf|Cep#E;9Cdgwo5vB ziN=}>KAnCRkb{9|VXbHSiV?e8wx1gpE?)3+p?^C*Ba zjD(b%hIZ$M)c8w7a;Wu)sOIY~_j!-B5*?MCMD7HpzW*p6k58NV;8IHcMoRsPX8j5* zm6*%OMCGeFXQZDaQf~tcfvk;Wryw3;=3nLzkW5KD!`AyW#ia)As2#Lw9KJYR4}rdsS}bW^Cpr=?k65+*?i=s)hCx2qD%!&C96g z<@v!+G0(t@&zT32=o&C%9yD<`kpIPUPS9pf&=2;&AFHs(GIpmj)c%@>xr@4MUvA7< z)u*`Tp=cb?bKtn#ozK@F!v@%~Buwqj)$dg9ZCdZm46R2Eg^GJ4U4bsEzW5EK-ysU6 zwkMuNb$KAV`nXB?#;{&_5b>6jnyuTlFEUm;u+KRhw?=l6Ai}A^pCQv~pMIHa#Vc;a z-!g1D)when={d~r86CqwxAU=6U|WyRh27kNq6&QqLi7Mx-x%843gYqV(Ra&?_8D(P z-GM-G^WyD|8YVUy08;S;`ELV!^G1!(A)?gB-5)%=Pd8?xBv#2H0qBIIOQ!U&< zQK1f$ecKu9rX9zP9Z!PnKxDNdjTozXpf6FYqx2U+=0z2H9Lh3Hd zWbPP>fz16}w{=sie*w7-}3aGgTp2@AO)q{^4c4nrEz9BG{bO zy_~o!jaMB*TIaEPm_-+Ey6(-@H$jD`-;tqwKfe3@_<5eqi!n;! zuixFAd1ml2$*ERF_qpVuQ~-~M%+x|(?=vsv&ZK?JOt{ob>Rymi=_^3gq;lFhs5^4Y zlYC12yefMYB%~{}uU?(ddN66wr*Je;h5G8}Te<+=12n-4mV||v|f{D>QojBDoXnN-`U-4Nt^{VLgwBvSgTBZ_g&V|00 zYbx5?QJL6{wHE%p`E|$$w7Q%gCn2p(wY@D9IhNCC`(s$}Hnr=woX19Eb|+)P`Rwy( zuVawPMV3bvM#{p$8N_VGD9ex-tu`-lIRwHJVS8|JL*W_TfBbd)0c+OY7pr@td;% zS{RY&Z)v5!3=L(M{n!OY%a0ytn0J5paNusOPjQ3@D=|62HlIsUOozsH<~JH8^0VE} zy$!_!r+k8b*#}9s2>HY5Ivj1n(f?j{;p>9emxIKx&|ocsof@P*&OMBScAj<8R5j+5 zZYvPOdu|OlxsMdS-uDeB>2_;x8=l;k*Nl$wzbS0>cfNO{Ly!-@lof19{sp;ZkdD~d zUDyjuk#u_a5LyX3m6OB*buJX0`&e8kSJHas!P1b)iTIzp5kZS0W_9z$dz-}*TDX9z z$%Ve9Biq9wyEf+rj|cvIAev&@{$N{ow^h+$d73}<&fCTZ*k!?9!_u!-fUuyufSm@?Jxj^HCbX{%0xDCMI7*(+1U!(Ti6n66Tht&ri!--XyxDu0{q4TSbVXc23m-}#EoG0hLgnj zL^WMgjIQ0d@8f*kUqkWEvchIpjHIA4XFj*2@t>KLB`C-i!-HGyfU|qi_tfk~gJy|&5q^>XLkTfoJ@e?D)FU z$;I8d}^!q`$-RxuEsOK_)L3g#<7anTHgAdTrU& zUasHh=r|tf@bPfbWuDcjzkYKqfAxMfH6-^-W_fp6rAexT0%?DBWJY0o-ToNo#4#;z zc%bF#LrL-W=E~iU21%Q1Yk;B8&VZP1UGL9n{TJcYo?qk)OJ!mO=mDo~LN47Xja9QM zIz@_OxvhMbqW$>wy>XMHb!X42NIUIpu0q`07OFJG6VJ%|E$c42V?aT%U#&^Gg|Df^d-7pYg9G zDOs~jGiN0ge*xI2=-YSj;xDZwM%*)!eeTf<>AgXJdi5_eC&Xz9yhfMYn&K;y4_$|T zugkwNAgkSdRw$92Vcb`o)Rgbjps<|WS?-?)s=Q(&H&a{jww?wUvo!;M>#?##RQTi} ztqTL<U5YsD?jHH7KfR_$}>LqPwZPN%U$li!{EOA z{Q3fTvr28-rno?x@2f{()m}MP%Q~N@8w3kV-Kp%jK=az=_D^hVPmNzq8;a9mf)?3> zLi>p z^mqVeEPWn=w+Axj5Oa{98J|ZxUBBD6w>hO7Bojg{mtGPzUXUujG^-4{|8lL|whX4r zt8gsL6B<2N^ygKT2dEuD%umlh9k~LBSu!|2heWs*+a}s?6Fu7HzGQ35<*qObXhxsF z+dN7LYDB0&{LF~yUd&XJ4KD|?UdS!J{1-|Fu4=$-~!r`I@Ayk5Iw`V!01R{h=<^gS*|2|ZJ7pOC!h+2l+5 zk6=hNw4qT>@pkqi$4ZJY6}C>gNR<58N?)tBf0%kA;;7ETdU^kSKoqV{U1XePUjx%- zk7NCIsc$RnKgUZgJ%E=BkOcIZ$!5UACi8AqI)=F|lPey1{Pu3enklh+oSZK^3@S{W z#f@fjUrFrS>d83+2^MqrXA>e~+0)#bIh}EOD^~>MFGlaZR`cFE#FGP21P`BUeZ^8d zhvzlj4mAw3MAv?|MG8x9|8m{)Ziv`j(cSa(`g`X!`s9Gfx|G?=dhOFw&>-Du86UL7 zAB2;$sF)J?)kiC_FLWo6G^r46Zp+`1QKv3TmMdxQ`9oy>i=Zb*)H=kh3u>-+p()i- zyMVK^omU!zWFgNwUg)ug3guK@YkIUYnu0fY1F44o?V8eN&8f3UrvCLK_2IZ=9M*vo zKe#i0_qcz$iST*V#_>UGh>5-W_k;HqyEf-`f5ioX#os-i3+8=vg7&ZBM(YkX;?F&+15s#+CrhFz zx5cw$bf4q#z;^G8x&F9U9w|s%lk??9azyXjkWB3SUN&H75Fe$|A6y?N#V!L@nao(T zFIk%LgUwH6r+~$5m7VacrWjh74;0owMU^ji6CGW)n(s$d?q(&O`D2e;J_sq@2!ne4 zRIJx0VN2VVm}l5fbisWj>ary5eU9@IB_pPSk~shGo2xU+%A09yu6|#>U|A}9!&mPN z4ybegs`}%|X{*}EGcHY8_izJfjWpRI?f@nX1Sj~Bebq}>iUkrk-= zAId0My50=ng42cFP`KfFvER3u0Y%*ge;c=l`3^ZfF44L9ymkzc*BOLXS046eJ8qGf zbpt`^_tk+#zS~>BY3!VyWv!n5m-a89@In7;oA1*C*4xAEnlS@6;g(*+{^*J8xAp#+K!DNq`P)s4bpcSO(gE6Rm4 zBj}XCar?mRvG}aG_eau5PaaT_q`E;ogTGUr(#%qui0gBt+q2X3hS9gBBSk?g>Om4e zIqNn$T%%d=-Fjr3_mTORyeImCcKXZ}&-i^%qBXos3eZCf9butsuS{K(-XdCW5Cd&L z(n|IZZM?r5ILjZf@{A|yi%{j)r6^Z_+ct(O@GLa1^92Tihxm2;bWC-?GkeQ+tJ8dH zbvtveE!;Nt=|1B|=FV(QMX~Iea3sQ076wk%u?HEj#urHLw+(f)} z`cgQUsa7gSyK*LTbJ88_HA<<}piSL92QF(${oc;P!dj*?22;w1gwv!>op2t54yrG% z59L^zhmzPHXl%anwO|k0stl5i>Wiu#8TRG0FSh;tzG&qSKs~QiBW`8Hxy{KI$^&#o zBvMnc?}c6p?cv5N!5nm={X$`@=3H1#;1$(=8@X-La3QV#AhGu0RmIW=`+B{<1{*mV z)+J~=@1TGNd;2l%jCimG2Erq6awpZ5M+Yo#mPaMV1IsDRfyT1rrF3+_aW znakXNhtU!RzL5Lib7)j~J!k8<>6BFk)fuI;qtd@yO znf~eKn(dp^B$l7xElf=`7o-tn&E zV^WF0_yxb!eCuyt<)RF=0ek-#u-amsAz#PIj~Os$&Gtm!%GR){7UxZ;Ax`2iz>>KZ zuqgIKS02YSS$U&*cT<9vT@hQT(MAjblK)1_eWw+b*z>+za`qIHv67%Jr)I?{`P`Pz zw;8L-4#_X6UY|_K`Wh@#KNBz*({_y#h!gb{)v&m>SnsV`2Tf^OJI>G2B3c7x(KHe; zP;X3Xw*>%rqrso%IU1|poHPp-sB{hfoBwi0>ARhh)~sK;LJK^KEj53OW}fG2Y>KM> z9!$q;s*2+hVv0I}FFT^obTzZMrwah|P)ED*&nYBnl%i^DWmVpLm`|wjL>jma6Oi7B zN{f0Bgal+v-9WuGr-&_m1WPThO44?b`Z*nt^Sel7iNdW}(evp;!SoRrrC^p@@G)C1 zY07>uE*wlh$TcGtAQISvjL81~kdGlZ5>5(+|B$#z@GEosZ<y z;T|k>aQT&qU~DuE9VZ@V)T%TFaqFc7$f2(FGJ=5#PxcAF=`RX+0&*w%8stYJMCvAm zXld@xYci}~V#2=-VK~+8B2WmoNtFNi@dO@gSwH-^i4I6Dje*T9zHPtJkVZ23drBY} z!ba@IVSpYb*Z7i=hUPT8{L_=N@P2VF5C^Gxa2G=4GQtDfka2jpp7YONPu6x4$Ra?E z$4=?#IrOgzEEd7gT6&9qRYB(5KgiI*Z4<0K-w}998`gt`{Q=hHXJRl`^t|8=sG#77 zeE!^qS`40A%?#it^Cb=vueW?O*MlU33|S@)&@u^pQ~63LyYM1ffL$QB$rM$5Pq1Dt zphP|&e|8<_=Yw5m0hd%<-OkrWON6Y`nD%N1K2<91v2CCssshpPy&m@#x1uZrr%4Yo z<-s1%svYVR48~G16-ufLA^>j9{%XVHNG*nWX)3_cW1yZDf=Q0>4g@5m?ncFDB~&2g zFtMsb*&`JaW*^X9zQu=$j`hVlv_!Y&kB>hG2>=t}%H77v$Q!R(V4FVb1@!1kuC`Ki@=Pa#~ep!cJZG8NAs^!vY+{<}Lre ziF!XqSSWll_GHEKC?+fx-t(e000qtLx@OoeZ zCqr;vD373)*H{`_zjO)qe)4FkVBw@y}JbA$ylX@vkkc_L?0X zhCFEP92~BXeC*fdDUx6iGPlO(BJMDdXO*4RIHX;)6ey7wv+ABwaC!M0hOh>< z*@Ro}IZx5H(Qupk;#OK>$e^`D3&4EsjP#9Rd|_AR>(T**0x+g8skrXom5QCZ?m7C+ zfDU0wxk%nh8|Gy2L7Mxb23SY4oG-91%&=}Y!(jx2aTfe}u`C69V(K6uDUXWWrHN4H zjPddX=GBo*;u)c73(YBG9ZqjgDgj5u2V=JbA7)~U^0VZOWld1g1qf8*nn~A@pu8Lo zeXfvfdUG9z(8J3|F}5cSH@#?_-eVSXE|^JwfvZSKkr=Z9-9oAof}$gLaBP|;7-X~I z51%v~h|F8KY=SZ@pf`ngx#^(Ma!by*4V{~8DVRfWL^Gt--PF@bwo$b!&&1AMBU&V5 zZW2%2ji~NT4WgmZV{@K$JWH$!>J2{)y~;;xq=p@`8l~wa@0aASrrD z_{mV0J0LK}pQ7hv13I#Y`aA(HSVk4k2NJ~-6K!2cS0^+`4b`@o%->SHtU_?a=Dt_| zTKBPUg^G?piV=#yD0|W5cVKol?~}W+U(dd1OlF{Y&Be-u;_CE~!ln?q<#-lf>sT8I zW3A~`G1JF@d*w{Y#7a~fs7xi|+>Kh7XI^B)+!Ic#JI{&FCKxXL zaO$TOY$2}O$$b@se6e6YAYMhey1OvZpB6w{=-)nWoaot}orbG{T=`AnL=aBlm~yY- z6HNi>qO9LF<#HN<@8TZn;QEX$?Qo`wLaQO6o4c|4ja)7Pq=@)cw8s%lZ$Y=7u|}m& zQTbn{WNQlzCh_#MR;WySVKSSOqsS^kF`9gG>y4U#%s^|`iuRAJp00c!#^FE@|FQFH zdT6_|52#6}Uk~&dat%`r*mQTZRz5W!7;zY3`*`dUla}>tENq9QC|sNPE@wyAt;B3K zuX6k|dG5kb7Siavs0$q<9UgeCg&mvyP1{$xJ+LN6Yb{J&pLe#Y<5H zF}N-Yxz6sImmTG6cU%Mm&6QCO)B0vdN9v213uL!ta;VsCY_gk>T@CP)3*uNb+X}?+ z7U=T{A=lSFYJ$A}ppdYVpFs#|J3c#OXnr;MHrDhVNi4SpD?{jH!#~H?SK+h;D^52I zr~?)uDLqiAH!z;Dk|!F6Pr-$pmRIa0h;@?0W>cG_%L!%JZ3};KtQ&bstD_4XA>_R4HW1r}|h5q>Jz@njS8(*Wu=-Tn<;y_ikz^s~30i zV*7B^NO0~IuEkR^8>cr1bsdvU1}09zbp(5^OB3-SOv@u#1fM9)WM#eNYeo*C0%$<* z3si)WrOV%mgCLX*FF(rP>yI;6ZS5Ed+_=#`ZzT>LdXHfaJjZTlSDD;IpEer7*6&_p z2(#n;?Jx~Z^U-q*X)g~a=sn55(EBH~q=GO;k5RTG`oGH~6Ni7C!4$2~32CITfD+E0 z_KRu`yI{)*%Yj8WEl-PpfYl-YCNB)N<3af#_U8s%b)N02qpe1pe|L6x_YOhqF$iQM zP7Y(oq4bGq|N8m`5NxUb_4>|j-k+JV^`YHw2x1we?(_NOf`#~e0rDZUAc}x%Sl>|CR3%jFs(mGrV7s99p4(aE3O!%Z6dX}OmNhiwrlWJ)ix&)E zQ+4y4XN%ZyGE8_i;llD5fsI7P$&u>P>P!Tk8%Up`wfQLix~y+S*o9?nG%$KcZV;d5 z33S*cgyq1UIMxI1dYpIQ)E`(|G9wYX!(fAUQ(reOJwV?n{f(x;j>Xk#_s-vTQ5PcE z@lWyv_#P)X&_Y%_$ys2~%FTXz=cy!bvE}YgvpJu@D5sU`- z6Rtl;_EuL+NGUfHc}B@DwXu8ml_wwvN~&6Kl;7zd^9MQ?%8uAI_<$#>gA5i3`|qtP zORbq%ghxj)Bc6v-@HQaah1VDFog^>TUsbZ(uHvl(12i=G#`evhZ!c}Ji%=ptjW_M| zfO|-jc8s5;Jd|M<2vyQWT6xWx=MT`l&O8`n$H3nPDr*o0$nukYbecEplD(UAkJ8+~ z1Gc^p*ugLde}Wh(_=`|hu4D&^;OVWiAy5Ej3OA?7S%_tGHvD8$$jW z@A%=lGJz{RzK7;1Vf-$?#FM81C54pGog8>m&}(5xf;*O06m@^XA3spm%f*^??Z* zk!0R|Izh0gpddC>Y&Q3&GAw=>Q=LQx?k`on4xWQ}R2~QzC;v7vN7Zeo;TAU^S`fypDFrAWirV5B!?0#0$K5&S}`7dFgdDXa$ zG~dcqSE0LU&>1OH6Q{8goA+i3#Js zYi^CNB}HvNDyBq!?8+bd60dCkm7l1cIs~ut*M(}8trtlq**!eB3-9i5VaTjO!rC!^ zN7$e<5l$(>TX&Kx6?8E&opQ%c^>yWM>aY%_-)l&%f?o&dX(*}`mkdjc9 zLZ;C`x)l!p1&3Lz=K+Ce<6L1)DZCN1s1+L^5o+^n-Aa8PAe2&W(fhoY3b&$47U0{d zn?huc)nO^KR@jHj7EXkSY#3N9PrI-nfO85nk*I zd<;7{DemiUf_6mDl%>{Mnfo0w6p1X>v^sbJtMHB`VOB$FvvC5&D>T!p}9 zJ=ly4=OTZ}OHSndLkqL4~pFDyp3M8NxyFBB};H4%#Fgz&*V8ErFg&@Vv z25C^MBsw?Lz(fap!AFUd{%9<75NbxyOorm|tuX1T#N@+7In?tmG@MlJ(i>MrDFh=U z(@2$2LSw7(thWLHw@1LtJ6S(F&4A|}pQzC~J1nz+jCOywYDV9*gUJY6cEMw6? z3YCWe7?p2(=q2x5C5A!KpdCFW)O^PAH;@1rl>?co6Bf(WaF`fUauGI!YitV`z;gwS zZ%z%7wB6(9VIg09^##Z7+Y26?bvq-nB9|fqh3NGhSk6HVIvJ^}5A3gM2rnZU6r|V$ z5&>@@W<1@{xB=TO8}lSvn$mvn>duW|YD{4FYpAL>>~!w$b3k#KMM@|DvSbeO(Fnc@ z*qeV9`o4gep0n@ZGUUW6Cy+7>_KI8mz#rX@j}=x9atS=3f;MQkGSf!U5_?J(Zs~FQ zYyb)BZCP|yaTJ3-zx0oXEZ*O|`XHc=;Pi*eCcZ5M-mNDr)updN9GPcw1K~|okC7fQ z)<6{PQ?a#3GVCt##i$L4W4;PBwacT+Yv-pFJR+a5y$qeF+94R}vup?zBASk%?fxYp zAvDu3u0ik4KDG$jS!Jk(y>lMb{4p_@+U^@v*dBNvduy_-{9rnC`-80ad0Y#CENTEE z0_=l#2nu>_1#duok+0ChqBPbZkCe7R8vjHH*HEN9xWMQGleMlKVSrR=`*FNPL_OJ` z&vwD)2=?LM+J1THv2XI`#lD?+sW^p_=?C&af!0BPSyKrr2^E0O98oh6A)4B-(ycEj zyQoBdnhM^E{-0)bl@||F)ywboy&PoQI7bHP>5Iu$G;ojrnC*z3lFR|#z!+aXFWKL- zw?%Pxk)sX)YrK328zH3UzUzg&QP#gb-V{8b9+{ClnW)>kQ)i=?=x|4MRx{EKa7a5t zy#@v3(NJKH_nxr^=W#UN5g{f4w=^uA+Tw=Avc$&r%=SPvnfzEyP`m>e21m|Z0DV&j%HN)r&i&Ux8gVDh>( zGO4Qo)d+@oPfx=XFwbg}J5cHcXRc!!Op2@AgW4Vu7xsxFDTP4$f^4Ts*cU)O76PXO zjWW^;y8<_ZS|~g-z^+BGk^piZ#|}ofU^)G`v9eKPgGvxT>Nw#TC~D#d+N*3uD41ww?5=P zDP$mSES_)9_uu@OQMcH9$`ZnZ?clyCW(C>}u&@|EpaPcg<>bOA$nTqDfz^ux=FxF& zV30tPwX>7uM^;UVt(?#148(cQ#6Hu8wXvH>xAa9|8wf$CNC13?!ixNRJZ zlipRWJdD-K)68ZSz-X+TY)b=Heb}GaEP~Wq{`7k`S@6z4ZkWsQ%RKOzz3%6udj?jL z$3buLQ&uuJSoM|N*5<tK=NyDkHz0h@!1h=q)|Akz2AFiamNm~48|Jh`Mj`a8hQ#+ul5PDlk(mnWo@cD zLHs?T!Y*ZAeCCaAJ_17;MH%o?So3+NCW-@B^L=On&LkrJqbQ!0CS7oDEHuMH%l1+E zXHJa+&F0#{u5?}Jb)=vj!sJA8ZN_Q=Og<}?`v4=L4lOi&I`cTpc?61B4b*{$qktBk zHAy`w7;)fK>@x{YH?Au@ESx!8Dwc>?@;HWhxyF6(`(KM{2r-6(A41dt7PX}Ai(`GT zwL@tE=>PF8TJ)uHk4$&K@O>4d3^LG0 zcYT}%UY_>d&%mVdytt*MD@va2VnMMZG{AVMU6QiyRHH=pKDYDs5HZO@pi|w#OpGk` zQ$BkFBb2-}n4T0@H6*Ss1~U`_g{kh6?*N9ME0MW3&Obxy?V>yIDuBZlhod4Y3vy%= z1K`C(a^g3&w^5Y#tFwM9@5B9Zr?2olQ0snZra^Cmjz&bp`K_9jZq0n%l+F99Qo!H> zVTLlm>E)(n3DhL8IRUL{8_?o$3oOs*wb->Mry=o5?X1=4an zQ=hP6hI(gCYuysf<~x3vqg$3S0) zY6zFetSyM*(L*Sj4y}Z43G3>FGGkpV`Z{RFS3g2dS-2veC7p$SSdxAoIWifyl!vZP z1u;8Yp7#U?+>ivd&Y+?{5WV(mMnicTs=rW3VsD?s&_PmvUI?oP#Usqa4@{my_!huz zLJF}E&hr&z6Kc2p=i<;EXfhsXGlT#?UP|MgC-)XCAh-?>c}=aMP6pV|pa-FSEn^~>tN=hG7y@C8b>h4gfyA6kvc~3G zein^M*F}gR_wY{8T(EHllc4zD(Fdqv-j$fyYez1I3xr_MwS zf2;S!E3%kgGY!Z80@pH>OfJK^ z{5oDS{){A+aO6NQff*s0IhIHRpjoqmP!&~Y<#uD>AaS^gAgMG4Yzsl{W#ZuvebI8# zP+d73J$64m$$P_mXfq)n@>5E7pm_OTsZuhZR{PmehZT(ijN}udf9pFn86)#d6nS`P4g2xGUy)X!}`i+{Hl!R1!cAU&Hhcsm zKoHn?eK%to7-a>=fYlz1t=NX}57gN1ei8#T_{R11RNUapX<*o(2Xmy76p#ucFTn0g za>`#x?S0$yJ|CyjUtxbu58*^E(`*J9H33DwxkX(JsC=ooOmUO?-;lr*#Z%`!b5~`a ziRRJLU;g#H8U>EP{W=@h@H#O91GP0RIahHvx?K;8=GmE4=bJ7+irG8vw|3e~({zYo&%3Z{ z1=<>97f==#!%$DwO%l&Hay;)b$<&PU;dNwfb1GrKLcTop$5r(yZKhb>9X&RO(u3#j zpU}&@&r2JvA<-J|!8oyFXT6YoH#J$_W6pQsRp-QV%PSYgHDPhTo4;1mh72)iRJeGW z66%wFE>f!nF@H9BT4XRpGi>PlYe-p$R4+t4c>wtFt3Qei?#anmWonQC!8-GXz6Sd2 zu=+$k=_nh-yDbP|gX@ZQ76HKVfe2<({$u(%B=87_Op)CyX0L=g#)u~0?s{&gTQF^sZkm5$ED?(h>yH|lfT9R} zOUz8y_T^|6fkJ`Nn&gWHW2MOtO#Wns~!yCdMnjuDWfl?-T|2LoU`hMQsKVH3si!V6^m3IDKq_@h)2qIG~T z*l##Z^x+RVO6WN>IcZ!#m9Qd?qq?=6UW0l?AJF@gl?cXac8=%@VzI#Jw;^MP6(XnO zV6Z(?`LKxg;9TV)ibEvX{hcpV4eMmZPa}6eYQ5Q=*i8ejH%yQSrC_Fzd&opHS4+(T z=;Rq$85*%TjAnLu4=((S^%w!yu3}L{e%M*umyF8tVN?%*Rf*+Uo3im8kYkC_O^!Z)*%9}-A%6=sTgU1^|?jWk%tFx zKEdfClx_vI?s9Ju4inFmXn+OBXbTlqq z)6eE9(HA+_pkO@9_J?&>73B|LIzcG5(Hi>fA%d(-P}b>@pROQKGUb5czmqOE77rUg z1EK%bud}*l1IPMD){;FDVm*B1R49=tU|dv=M4p48%p0*c-&p+{Ul}%_m2j24^sgps zwOAN}15Lp6B7m<0@`vL4{QM1rv8saN*1|biZs=G;D2MuqH*miIh-Ub2E2=_c4_S1e zFhSi9t$n%t5TIT!@hCFuvx#Rd#*6k)WYS|~X<3xF9z|ibQNeOOaffP@347UB|3Es~ zo=VgTlVIl2+f4bq!E}=fo(mK#K?5g|Bo6UPB|9Z3l^c6@Cm}=R?#uAf1=#5}AN=bd zo-ibAE-e#DID~i^;F&Euh7-~aK>X)h&P)~JMLY{!y)Ns$a$#@I!N#?mHBWgV3k zyTXXFOc+K)mO@!t7#Si|$abQ!%dzkLu3P7v&-?xOem>vN`}h4vk9f^%Udw%7_jO;_ za{<+W$C&D#s_Su#vsE>7+QzP!QB-M{g#j4n5p|i}idyl^@lXul!=zTM&W0bPBJQ}n z{Yrr@C}LZ8u+*J<)l=|l!(B{x0f)BuaQxu7FJJdnuOY3~$sIz$cHkou7Y0~Cr@6ee8X@}S!Rp!dyhkD4TzU-5j16!&d*vsrV5`hu%Um3BF z64^h3VcweQ{h)n>-o8#X{zFSdTa6|?8jpuXOj{Jg!06!g>KX$ZnK?PS}PY(@PtGuv;ZpvxY2?54sUVg{3lM57J`)5-VGX zO@dMd3Cin(4LiR&s~r$>KDj-q(LLZiKWI5XBb zJ=Q~+s^PmDYifu{w@D`JcVyd|_x4)lQrG(fD;WzoEoPxQnK^L$Rw%&`Eq5F{bSKhb z;JO%Kbd0wnmPo8aBJ|+kQFs||2gv}7TcHejdr1rxB#hLl0$*D4pcamTtTGcUFC)8t z*g~t#xE;I(EAM0VE?n?ph6cl2kfXwAD$b0B&(2swSg)eQBCWlKgXrb&DoVxcNif&S zASD$(4YP4l5BB5wk|LzN80OT-k&IY6=sd(dgPQ4k48TSxsUsGiz9C&6Ah4qQ=gEzO zVIFWT5Ma;cQ3)9fSlB4fgnkxudo`vY1|@Ysr$x_L*CJuOqzpm(h!%gUa>ZNj%K2(8aq~{l$I5q0g6b;* z@hMdsz5;eU(jo_)EJduO$suL`a)_aUQC4NCo=AVn7pkybAR}c6*N?c&7gT^0p!B|q z2bL%Jzs^1A*#MbsuQ(O@(Hn2`)*`Kfb>{fbpKuJ6#s3Su_w(?-fBv_JqAcb=;GX{* z)%@qz{6}B*Uu5vff&=PT06Z@d0_ zjv}qVQjVk6a=@HI1oB_XMv2mY|EGO&I#V+sm5phlzP?()usJ{W^?4L&I?e2_<9XR_ zWQIHt2oa$gS^A+IV)bwbNQx!!^R1uI2Q83>j}r^pv~-{qguX-O)$o&SQ8xX(uTo{t z*J|T`h2QR#q@ie#wEesEYOi$nM`kN&tkvo9h=pTJfXdiBQEw-co67>tjq2@KwAMQt zBRvJxlmTFk+xeLgN41D19n6J}BQDsOv+FM|f&cEltCnya5WrMzULwh}4!@fbP@cJB z`f9abw>{B)Y)4ny&tr7})^6i$9I|WNKiL=js&&w+0Ff`Szff8^M6z3H|T=H^6SDrspJYl07v>;enCYB-k*!JH` z)?8`P+;@o^4Pmsm@y~d4p`aEK^fhO)j3QR1UR5r=LcEC{VCM%+#-ymh4!Z(&|E$G< zz3pG`is@X*UE!4~pKhKF`d$nSdfVf`Nd>(IH)tj3_;pMmrdPw1fjq4f(mzCVX#w|` z`rlWYq2$$xvl4o1$@$soQ1pnlPa?w0v(cBv+<(OJ7@^46poole_E> z&`I}!*h}XkL`%am5#jdSyBO51QEwi_gKHZVFT5Pe7`s^G4AowaUD&Z}b7`&ZH5#+| z%cbBG;efpAL4|-WNI14BC~?jn#IE=!Q`8Dmqh*hl#W<^vSTR>(O2ZVsx}B%rD(X<<*;u_2BEy-;(a}nam5CbG|utg?xc-E1=-Qo zcM=rF#v}q*I0K9$SW(7Pfa@2#?vpuquFH_m-C?}eihU5qhd>OzjMrfj;KyhwmEf2euU-IxD-YwkV^^!>)Z)X&=Qg)+3HeZBuPX1HwmZX zk?y<@C|_4^Ol!mgKFRSls@;;B9={^pet**4__^CBgk*mlgbOH-jhB4sxmKm^jFw=5 zB4*8r83|EgQoW;Ki!%R}M9#loy=!$SS5ibjG{0O_v_v$L2GqxWl*i_2Sb2@(PGh}C z@_bG?20jI)9h9`%C}2;Nvo;B5+v_mH~&cvm3ejm?n|)08Kyd6fk+vKK#xJ;NiE zq>;3%2Qlzlwqx%Hd+<%bmNc*~264%ijo8O#+&7br{ChcuzD0a<2C9UBL+8m#O0!jB z$o84x@rrSa@T$om-q>sW=_cI*bdc8MrbiX0^`RhH}|8D$|ahse;^*F zO|saD!W`@8=h(G$bTt)7_W`Iz19AN6OG7;OC$9h&T4xE{&&*Xz6 zyF@cLL*ugbT1AY@*#R{(|C_>F$f>{Khs4d&Z(53bmJ9{)2jQHbgh&3aj3J&r!JT8t za0;fdmrn{__?+;3eS)=9f^bb9V-gO_A9|KQybLF|>at>!s_(k==vMF7NNc#pUS28t z;3$a5#xCKi6RpY4Hl-fSKEb&X8Q9=|KPwSd`@QUj6tI+&VO|Dkd)gV*+1!@=p-14*WP2()R#S zEbJe~q3L@Z)YQH_+G&)Rl_Z@LDUMTNbDi@AaO6^&L?VwFq z=1ZtZFRoZ!7y_Hd-&~ACBG2lZPA?B=R+8GzZXuft|0Gz@XgPkMl+xY(C7k?=MV_5H zdDb9 z`6zK@vj0~>edE#}FGoE13V`=|Z9p|-Gyaa>lX?of#zctG^imMSn@abE*ge9|&F}8Q ziyY};<`DbydADOho(v^mXI*V&$`9^U;2l)fWx-~fKV`VSKk>DrBkyAZ-|Bb>!*UuHwjiPSS5W=Xpt-i( zu&Ug2Wf;V$o@ie6a3s7P0SVfM>uf}}kQWgwI-(6U+Xo7%A$>1#OkBe@8HV=JrPJgXklCtnKVM%1T;NpMBGIACtopzDxKaJOg;@4M?`3epuA^g)@ULn{}6HI2J-{Zt+w)q z84mF_H%@kbSZF~U9P}Q(hl;2=sC4msWpgxi_@yTC+&eV+Tei+Y`bZ6E&g3p1)FI#H zbbXA1jpo3<6NeY?f=UF#5v)HLD63Ltjd~*(v2>0?DUfnw|2|p2iOEU~4#k1io4ALa z)Dw00Gm0ATpozOU`Jl3FiWf#r?xG_o45L(qF8Z*HBgyD$taId%uj1nho^Nx%gL=RW zq_Z=(rTq`Y-3a_}-Y%x}K;`O4Ww+b#Gp!&7#1!&^xwv>UOY6+}(|IKmNU>;uD`jQW zSqK`lYy$|lNY_3q(`b{EICVeD|GP|-kHLT=Vm~TYy!>m8^wxg$0vueWB4Cugg;^*+ z%Z&{qi$Dhjz5+_K52g$q&eWo;hE9=nY0bORKBW{@kcR}?^GW-+|9p7irph!4eOXsF zEvm+Ci_tcC7v9swU*}o!0>;jVWfh-b#U9v_cdgNfd3a?dRHcWd<)?G(^#oY*`wi-jA9VkG zUP;)T(5qhaZLVLj(x*&FxBlv$G%IU6i@%o4b-z#La{dLS5sM;_+D?0xX1{4TC zNwR_*!s#!c_c8?XQ=_X)7GWr=WqLP{2a?xTkhBYW0kal=Ek107Rh!XS$Xu_)QYPCV z*aG(G9Pa~JUt`x4*?A}#(;<)V#4@lX|A)I0*m-5#ul5;kdB6s}dD|$-%e12^6PpXMiq4xnZmE`k?Cb5!G8mUZ6BqV4Sx+U$!y^Tmm5QQsSdCLZOAn=~(3gS?AF=!X z;xnRU@$bzs0v`YU>-D41FaG84_0g7EZm{cwgxs#BhR2?^`f@Di$9lW&#ul=bq4Cw9 zr~l_Cd=_fR=#+o|{K+i*`SULh;`-O4uUS77N*e$Ai@$&Ue?7y`gZ{%qQN?$i-T3os z{*y0*D{AClf7btdf#(0QX7bO8f7WXLtzz@9C7%CSZ}8vF@c(#G|3`7^f3ErbAJ?Do z9QDV)vZI<-zX~lqg-OB7FCl^XGa%+(o+{g;rPC_?cfAV~xpn~8@xR{kYaVSs5)`eM zQA{~=|0d4oQg10}W1FtiP>BUv)I>9^7PX>3Efj`LeE~?-oh_l@wXZ{vVPPxe0(L0| z>Y3#*oa$rgZ>D$h6Tal09ubDY+Rpu99u-hCgcn zXdpg7a|G2x)xPLLYhF(3&V>tm5Hq2zVSZx@blU4n2DgJ_6XRS|*d78_j@6^c|==AT%x!b99*z*TMLA@m-IgnNi@r?7_!z1T!feWU5DMfJSn zfY_sF;&g;7u2l(FS^hQ`H=$<*APMc+$N60Ep zbZ#OCo;E@>(Rg>ZUs9_Dtwfv;t$1u9ldP!A=3A2H_1FJ)swvuti+m$v8QZM2EQ`bO z9yhK{m=~AdLf&IEvL7Z<0{cn@Qv92}KRq;Xi-9o(JHB^>>GfOSxVU7dE-5@T04U_} z%%}+aS6448zf_h$FJC7N8u3g%iY)jbG+-fliZyfS>l@z(ukknGRHl8bDZV8jn9?k* zDq&>I>!4PCUUmni<6a;1J zdSBy^Kp|hXfTksV6Bv9>TJm2<-VJsv1#n>~i94-(-_1i7cW?2)oh%+ujM&if$!$mW ztOw577~}CKn2mT)iy9D*^9fnR8Hc1~tic;0ZkHT#mT7N2w1rl0Yh0VSByMR6Cyo=( z)z;V8>ytB$IHwpR1GNc??8i=f*C!rLCd2N{(G`osxk|fc)v#xDG#mzGwpyk3z*ur;0vou!r1x0r8H*)Hn z#VEyF;VvMp4Bk9Eavm@2AG}cEBu^u->pQmQd6?_*o7ZBQGeTjokOeBcz*(A8F@hh) zp$LE2b0PM|VgR}DU4a>4@#(VVh1#P!2`(-3)1{Ads?S&GAs*l&L&j%GXT5(jkY*VX!45s zVfT(~nsE<*hZUQ~n$=j3ojAM|fK}nJB{s!1@GN3j(^BxpdXG$YV>fb{9aHPfhv(>c zAI~@niBx#Sd#oqm!|?=+d4k@O)Ya6NFebvK9u7h?vfcnw*EW7Wog&M?&PeKz=`V4k zjCnb9b{0wNK0pZ&SyoD%L|ZGTutRFyH~^OZRIL zj|rO%TZyblIFk=)-0>d$`uzO92OBnAi2rtIC3?}93qy5L%frH?X%|S6<0DBD zz$y~S409%HPUCj$Of#Fq~B-2NLgcTUI$x- zwz72-DWaC0AWk-=#Vf(iMA*3>cfMSP%fp;zlTV@SN?^?l1L2wan-CNjx$FMtG`jkD%9p@|>Q1TgP@9N(wka^Woc2}_jbcr(VDlC5os{V%Qkv-4wwWss{Q#ujhnatEAn=@ zGBH$-{jGI0xhz7S+x=A(IkaT7sL(G7H4cO{#X;^RFXCp$W zB1PMP2W=o9@!itQ)U|Urx5*Yh^~(2J%t~4FobjCO<<#`Dv^ruA{f2l%2+=GMEIeOZ zG8JW)o|}lDYRB0W%cK(U3~5sPAcnZ(fCHbFh*-AQ7qec?5qE0*FDhcW-#=L*%A{zL zjmIbc+!^{9gzY#}#w2WnzRf68$vE?@Dsp{Qbb<|B{3z**y`M&wcI>wu(7^>Y-#Toy z^va!9R!$2(4t>8%uHU=>u3oh#TF7vCAzkgvtKh>-jOUV>NoXwo(v`;dC9y=npq?eu zn{jN`Fd*xzn%TjZ6V}h)9QJ43jll)Ipx%--w$D3oknsr!?m@tiLyTP`i8b@7ypt72liWK&Rafyf44QQp~|PMCSjYTpi=9lqLUYZG6Aeq5Tj`c~f6DHwJoP zWNRy!Vz~ph=SY88+93MfH+u{TZVCsGgSWCDlbC8jAlTDt8 z#~gkEgtpPU&^S5_fnBzcLCK;6h4A&;s%Rf04E&02W?>Qo4@5AiCiPAXWPF7QcJN+# zCGj1k$2UWf#cXVJj$OI6@qJ79eL_s_O{>McUDt!)X)kCSvq8i?s=-Fs1pnQ%o>~7T zTD}3gBd{?1`~UuLHmU!c=jmT+<#67gTL<8F{mVxZUarAtl{;GJzi}6KcK{p&4yWxo zKiB|uaL;lR7n$Sdzld47SEg738AtAiBq4cDgIB$5724(o%^t|()CbR? z2tAVnctNS5^v(p!IEA9%-j7>1oH^hbMv?Wz5G4?(X$_Y17{!pz0-g|}>kVOC|J|`^ zc(`_{`lV#m?vjHv18i#STE5+ZtV1yfJSi-Av=Z`LmKHr}=H&t&ccSZ6JkH4+81$~v zk3VS+c|SoNDXYSMAhQ74Z$shl&~a`$5fOLpoS?8395HHkd@&@_E9tB7+~Uz2JOKrN5pxJ1NKt9sHe_s)Zbsmw9`BDcB_ zVzA7d=Y6xdetBxqK+L>4LT%~#3ba5#Ip?6aFPLt3y8Utx6T`V!f}qBH!)#=N<&Ec|q(*dgD~c-_|^OZSI`?HXU6Ea)-kn&67J8hcr7>gFaZBTRlvR#V2>}2N{p@ zp|TAd+FsA_SFwK>U$mu;(Z{?$`T*I^7DYgRi$E|Il0w<@AE^maaX9qT6<6sM^X$U4>h5+H1e1DCsC2?;M<1hd) zlN>&C!bFOj#$SqSJO;18Ntvon9h`<6sT0`}y4C@5W`kD57dIIcgk%iq*#}$RXW^;U zt5Yd!WzlAFV00?KGpWMi+Rx93t6X0xbKa{0*LW;b+Hnx&FWPb>2`i%cOP(%d~%hkPgE_a*3%Jst(J>yR;EgYx@KY-%FB zb2*WWHLL{z3#n>iE2KJJxUcU$GV|t#jMw*+r{>sHuSWx|n#p>dlXkVe>PIGOWh6xfzm z#)&qKXM6IL|AK`BmxkXFG8EEDuFcHY$w@%A$sC3E;$#opFgrqsJrpHB)nR5R4#*j` zzM|+M4)n8Lkv`ybh=U}19dJ9X^dCmwC+PG2zRTL$2vo$63Y*c7%(bF>s*bgH74DM` z_!AE4`;ucN2Lw?I6XFFeYOr|HO2ymZ&)EZt<|=kD_b=D$!hSlb#uuzS>rq&b52G|k zn~4@r_Q`@lX@gJVqY?p=TtFp4Uu`rdW%>JE0OichE^Dff`}HiXRyY@aQ1A_xjfOtkT=NF$G7UPhy#GaQ`pr>C9-&*zxHN%aZ5iC!H>YtjKj0j<|1!j$3K#LBV ze@|^o3q&+PfTK-Ej%4Kvuf#`dvF&}}!h^tsmi`$l=OnHh08Pcv07ndpA&46TsMmwk zTNhun_j{%+bwZU`A{(9+jHCiaW9uVOZ?Ub+b8rmxz{S@X4FXa=!hbUNT6wmuVJ(94 z`WIz3gD(6}oM6YSyc2qE_gtE#>wY6sUE@sq6=^`?QfJe|gn(SF)jGgz!?N}fxya%4_!xTKgmaOy_zWV~ga5Fjh zaK}v5{sh>3SenBVn+?K=%OA?0SXgY>5macQcJ|+JxBujf#R%C2hbY* zGlCE6;FKrd3mYjx8M#$8YamSB-g^A|Ohu&qu=pt_?;X5Re*eiow4Siuw`dl_wG(d%H3` zIo9+WJJ&I`Ak(XD3}9HL+)!MBLv<+dgb?*CR~@0UVgt^?+oenKO=a!UWb%eS$N<;S zVEQzClxHj65TKvUCmrF_nws4dcCbpinVJa5#AnbquVHcYEOz^4Lwp(qZ$?{F=lS*VGsO{1TUpZ``vvbz_9~70C~+-4o;JAG6YAE5HnKi0eF)h@lNl--2{hY z>RJZRQ}L`=RC~Mhre+(K40cfwP_$co8yT|=Dy;DGZ^L{TZRZ$yS>G4^5MqvrF|XI1 z-pTPoenS6`xIlw!Gb@rt>R%T;eP*$t^p0>g;QQf~Q%8X<44suZz?=!AX{4$}yhEo+ zSQ9GX>3a#Op>uXhgoas(pEHVxthAhVSp9j$8n3aebLPH}T5Wt-! zqLi0X@Lo$qbWJid*7D~U!^sd=yB(MsnsA}*fiIo2lcAI7`w00RUwxI#03MG@s%Q)T zG$=tBtuy8#IE${1S!Y5$2#_7}4QifudfRtknOt@%L5$81sX)pB;BS5`ox*%v!H4YCpUCjFp*LbX`wrC6kUpK5Y9AT2fMQAMfAcH7t zCDX7@Lw7K%0kp6x@icg*!r=83Q=tyx_-qaF6a4`>*GAb#2^er<8sLSHXi#SUcSB^S~q z-U15(+@JvKpNKAohX7FV=li)pChR^Ii_Ag*+IudFO^0u75;;g}Mt-`F73+v4Y)Snj zr$@|Rh_4$#;sKKk@u?o?L%%~r%3ZmTz z_;A(GI1%5Ujgacx%2LsIQj{6%@%opoiNHuj&sCI%1~2!e60O%z1M+o~s#%23`u zsh6SqrEWX+z-IDBU$j(-g2SVELL~)X)<^#=I3Pt6fYlI)ak1ZvKhj&qxV9iaRapdUF#9R zH;AF@3qgDn`D#nJ44Yvna{F5tNEt_uJryJX*$8oRC?1;og)rDNR)yfe!g`j*|)0XcPmj*VPW zhmgA@sO~qrKBwoBAE4Pk}gYloOuCRsN zRus&OL}Cx4b&DR$j*LQ1n1^ga0LCL*$N-Oie`vkcC;<(Z%-ClDQ~kUKfJLdWz@CiE zlNeY`0xE%Ov_31a7!e2^^W5w9sdcAMBtEPGv};ui8YF<3u=BJm4`6ug@}g;A*IJ9()u!LBr}5Kfqd$&0c-h8JcCO@Rafa z+J*)`VlD!-Gv5?;2U^7YK`7Q;>V;TGzK1RgR@)OSSQ&QF*|Uh-kD&>)Av2Aq*^)~< zXmmy@^{xxHqK`7f{JMM0$OQV=6X4Nup~-_fgD~iTtR40S zhI0KWfg9^s=Nr<)L~(PlYl*#2zQIj8BwT$qj?F2Af~>459kX%0UO!ESdR_J{;=9a>nT$O zU=c*FlmC9_aTL5&o4W*Hhw*S;AeB;32jXX&yWkHrL62>Urc?0tBvLpcfPhNup%~E0 zz$t@$XBD7o_B!VPE5?rmr-Z3GW|i-te6T-@)HMxd4#_N!(Bb}>>Q0lF2f}K2IC*aC?J_Q887s5S6V`B3dXHpkRAq! zmjj|M=5=mSt}#5K$WC^;PrlCIE#v&I+=%4_nOcK2q7@t%D82`wN_P<^w zC|g4+9{;FBgf?0MG$WX(o&W@)!p^W6Z=!su{3He(fRLgxT+A~q1bA($5TnKdR~Hef ze2w)W(M-TI4r@{0WAin|A?4FA1(N~sz-7<_h(q)n1bakF3!v~>uz(1M?6NAy{=Oad zaWCZQlIJjv+p%zUFv(Lh1qo7s(ZJ!90TEI;S3dsF%006V~pF4dEs@cY1=lC}A@%uv)`e0H1 zv=|6V;|RY9tNDO$uioxaW$$u_E3pnG{-qBr0OPa9o2^2pETod~22*B0Q+#jIAdc<> zB9}~C+BSIR_~ia_;g***Vc&`s!B=O^>2dPL1Jbx{0d*pg(D1zp~(vWl6 z)cY+EvkgvkCO{B%9i1@yF^!m!@EtN(*xutXh^sZ~?+g`#n~W`BY3e^>(-UfIVu;bn z%sWVhcnkbWq6>~#i>N%Yc(@3_S1*FLQNRE@`pg|3LNw$PVqXHHFd>bDA@?pjHMaUT zV3l8q#RaKdE1&Xhuphsf3`>}8Nwt%q2MG0`cegUvGj6#RhSqvYCGQJz^#P!|L$_;QLhBdE29hb!YCt+c ztPh0RfRZjc*hz$T$j!fx9~tZ}j$onKj=mcVe2=j>lmg{Z1Zv9&4+XoZdGH170*Y8I z>}}D_7+`2>z%0U&UMy>UG6Qg5iqfui>u01T|g^{-=vX>PEMIhJ~XzBEV z&0g>O4u1t*Vg;F}h3{5UAtl zSv%w9cQmCsrRp=`c83SQ;@3L0`0C)j8rZwdLUM1xV z^vpF~um_Q;opcnOil65V=(bxKws3<)5-numFBgz)620}Q+vxam!+3hcN}=Xbp@XBv zQ|lvDF@a2R`=RBMG;MPqg20~uzHNogS_h(XH3qIuDFT-URfHzGk?0v@V0~cSA~>jy zV6Hq~UZ!OZ0tzrrCXiv_MUXCmokZGkIe?=+NN`rX!M-TGX@3-=q=1MI!W4^-W_xo?#iLvCgt9O~|G z&(dl+uBK7bi+mw$8W>tamV0|bkn>s_>0HelwQ0pBq}awtGtad}LJ)xfXC(m&cBZo98N{BD~f&fPeFHCRqQmj^(_fgFmz^bM~hjr)szl{>DuI5Mu*@VsOd zty75g0?ZZ*Iu%o~DR8WnBzbinT{sX9)~;N*Vylc+%v}>NlO72SSPaBez2ckJzc^as zA4aP2&$N_yRU*3XDQL^HRArhB zi+(bz>Iw=ES2tc9<+89f>23pP=*jc;tCJ7<&X+JB$#&PG5r=9^A1SiE^}8lc@%sG% znnU)5P9I9FUnglXq2Tv{6{}ohXX}i_9T1Ts!S4^Cs*NIZ+0)+NZZFt$Nl9G-M|K>G za$VwqzRl9!T1BG>@JfU_OXHpgy)Na<(MIG>GI*hZetsWdqNIIl^%wU=kh{1fLjdEw z44BuHsymIxjEAnB(KQ??`$fh9YJe&g&U28}Debdf)u@Pq4dA^7RX*i`A*jsd0}fP2 z{zGT2`#ch8V_V(oTMDpssa{imvMmgPG)|7*8*H<^d>th*{F+Rq>uF{yh(@(BK;*g5 zZ$`e&F;3u7?noL0^Z-j*60iL$<<{@hQKluLJ7F<9NB|Fvs3W<*W1Ierh_1f;U`Xj* zUvBdrKjAx_;j+7|JI{rlaPBK_o0Wb7E9(HLNUm`zCRXQ1a;u$K*J9X;{!q4=7}V8NxwNs^AMc(O^~ANaNbtvd|BjCC zFYP5M_bKeoL}&e?0!e1dH^BJ&f*G5VSK38ND2^~Y8P!2Et#(zNZsIBa4CIceUB%gb zq74ci7dZT!!dnM!X#2b<%<4Mq%R%5{Yz(1p>kWSBr9)Mk73C%%rGhv`%}$w~=3^&Y zzaKXH@b!ol#OZzw8mE{|=O2yAX{kqgR&TlI?(R}LJPK>iMzhquI*G!EP2B~{?{zhz zO|5x}jw5*;ZjR!uZJSjcyU8CyI(a~`>!k#rPP*FFCGSw%gxQX9B zUx2_@>;M<fG(RAH{tKKNMw7x2GbAO{7x|L)q-9 z`oo8EYXKZvpu|NkS-IzVB`lJ>k{j=?ukrG91nG)ls${%R&^P3mYFWV*ji@y4%U&dr zDjom9h^ONN-c3rs)-=zES7dhs{Jz-hlIsMr#0ud^2k7VHy_Q8^J308&uQC?3~G7R_L9ZeobdpiG0nID|eg1e!Nr` zdXE9nRhnddt8@>$Z_|lo<5W+>0cF{43ZB%6I=`Na$vVdmZ%GXw@%ydn&&OVCv>vaW zNsrVX=h{zffgQAaz3w&vi*;Qs`XqJFl4fpgle5wHJp>@iG!M6*!nQZ5yfZjE;-?%RQ|ZWQDwZ%OyTPE7a-f~-(B*qPLzHdA4$Zm2=grxYBuT06Epk4wWtM~phY(b2V9|otJka=63 z+P0e2Q=Gk9{FfBpwgur17&a?SsE%M|4tkaSf~k^_NIP5TVLa428%)7(|7=R5g_G*9 zkuc$t?oo3OB_Mu>fio`W=?1G$dF=ZF4xMlnD)!oOPE%^++JhPoYVZ$r$-It={Nu$D z&Ggx&aF2>@j`C+^oV*4D1RXmcz)n%~MqEMjWk&)-)w9Z~5@loty=QEOpE4fG?XrgZ z;*uMz`MNMCqiW^!U>azPbkdspD|Ncjz=d@RCyBc#-ZM|u*L$##GWj~bCaHdXtukG$zK#hjHtLis_;zooEbl5U_%-b(M5a#|a0{Z(pJJ&q$}`C&b$h)9aru6AamP?aKiyV1wLKl9=3m^j_wJemgft;Q{%Ij*_}xW@)|MX zxyEpGOp`MkpzpO-xax6w=j7DIhSqm=B&(`q@O`td_3s)i|7|wczWR+Oz-e!kF8E2` zNu`@`3k!!``pBF(w!$ZaN(9!1jqz0n3xDAZA|pDqcKd;T)liN@o)>(b>mL~#XY~xZ zXg+laA=m#97+b>5;&X)ABAzWr9a+q;+Vu%6ab#}y9nGs4)e=ef@#C+y@K5wwm^1Db z4s$$1VfRWjUJBa;n_Z(8)@8(U9GkcCC_9i)By3ofgaDRF8N|5y7y-hBl z;2FZFBmYtONwxaYvYkxR_4CUb4GX@sMlOy4$D+4Dl3gp9%FoX@RT-Zj&P7&gUX*-m z+?+Yb?e%AAO>)mTPXv)B3`F=iU|pmv>>sO|rinWgmNzwVB+u01E3rD4)Zd zf^eg+86IYO^awlYRCOm zOzvC^>>AwmasGv>u5DFEa44}dP3pAG*K*K%5CjD!ew!Y*32Ba}G=iObc#Z!6DXdGd z^*fKRV($^F&6pI~~?o{cv_@oH$&3_Uo*VRfb zIbhPWY#I2H%53#^m2$gKxjNl6{IH4+EGGx-3Mo%;DScw3Ecq2*Yu0-f>`G*DgH5uD zP>ObI9$OI3-{J~Mth%U7a^S6WBXj#@I1mX^46@FtI#u5U|lUe zu{9|L)g_N7y!tDd6Wj8~l0?$>T>D}3GQe5EAyRGf{Wk^wjGH)H9{cH3?o*nyqG`8q z4dtYWnRNI3PdvccYqQ)~#1sa><$ZU4kiPf?T+*!U2HCgWb4U0Fbe8U~p^m=&QQXB> ziKiOsof5dnlZB_Arpk6ZR!m*gJy0$}J!C>ie7m0a0iK?-HOE&+1jo^29PDt-ODFh< zOAEKZ;e!u@&SoiIR8L%$%ifx;pL{Z3$o*OMB)G53Plh>2*~cQQc=dE^Fy)(qf*7r8 zU>+F4sR4VsnGfufTRw>Tx>>03fT9E66~ne?@ahc@($W=$u3ZG8@_iYPipIq&ZQ|OE z5e;G?G5MsGkeJ-5<57-jQ`e)CK6Woj8UA@-FGXO`5qH;i!A$l+L-?w-WL+b(lgkb( z8QJXyGCUvL8nPZ8c~N|48+N8+*mNg$@dXcv)8dp-*Rtc9}2 z4^H{W#QN>nB>NTUBo8^BQ|sTFG(ee%A1@QV%Q#Z%u4yo^o){F`nbf*|bz05y?NEQ1 zo+Qkc_V!*&%Av1c@;{l>`FTU`p-Mm9>f>2%Cob)FK{JUNOX9a__lw|vWM$%-aH^n0 z>4c;BKo(=*E%XL*u6B~rpG$Dx<0V+{e(^0sEK=}$lD*r+YkAHm%+G1vctQN<@$J%J zDa+XC`08uVRRR5^Z2xq{yeRHRgBcj^dtugC;j| z^5b81g)TwOC9UXspTa;;Kvg;2d#2$j^jhvWswyfw)z>L69}L{xbTMzrd;-yZ>-#Fz zpH#E)=oJ)J%K>lZhn>6V36DD4hEjXC?A}xTIoAXaJoQZLJ=D)QUd(E}$thpgUly)3`dR%UkIjrw%&Piw%lDFl&wh-_qc+0=~S$oxlmt z%QZ6UHNzB)!lw-T=EXP(#}*EDdb>x~PK!d1Vu6+Lu|c!nCbxFTG)$#7xb=H)OVpJc zhxTX@kLS_OVx{&n!_I`SBpFMc`$V#MzB=1GySyilJ-}r6-N!KLXW!15I<~hZy4{|< z*vH7x4pIn-d${+C>w@oLg-UipYK@`4PW6Tmp4paF$!?8zwVWA&7g?~%G34qes>%^T zgKb|H2u=Kj&3#g$pHjY>w7U7GOaR)sYBjIXoG_FdGo>ck@^$y3dyR4S!VawaJxVDV z_F(+sROSs-k2+C$Z4Ya>h$<)T7`M?Q zt+sMi#mAP6n+?&hqPKHFa2yOhHsJ)*YVsDH%B`xc7-0MYld}_ zb919WzAf8$fA*DrwO{t=-NcX(#~m5QcYv0X}f)&tkC%&0RAzkCHwx$|KolvQS`OZNoF(J5VkFfYCF0P`^g2J(8Z7a^ zv}549P?+!B%GCrdVpg%NV-EGBh8y0%U*iJ>--BNMNz;VSYl#a+z2-fKZsHEez6Mo@ zrg@dx(jC%}ywvG5Y#b@J;17U`(&z;lLVs;%Og%GWGLNw zM>w(R#KK(lWIJIYZmy8dAQ~gfKtHgywX_~O{q!2M3*971 zf77R8ac8vv8He8~x%SG%wMdXjM^<3)ytftX7OZW~oi^0SV(mG{`7}GR*km=mnLqxa z<)CMk%{9m6cn7V=^7w3Z$Evf>;dQpC6D!u@Mf0cH8HFbY!0vn}7n#-nkBt#)icYks zk!5WVwxPd2F`<(M=JeUX5PjpEb9^IgnRe$k zCWYs_SSD5;w~yn_ON}JzJcodq_L&8w#kiCVf}qy(OvWGh%WVXh)!s5)(=S2&7u{JC(vlx zcA>8xUxOazg)QX4&cvzLi@(ch7snl2_;S=BR{n!@_25n!)zg)Wl7UwD<*rzI_1F5Z zL4)zwz=4%7#T8eiuvpj&9Zs5E9E1BDh_%=Aw;vGTuWm4K#xZ+4=kLvysxY{h!Q(b( z>Y;hcAMWtDlLGhDMM1G+JY2!IVYJBofuzp!Ue}t{8Tm{uvL5W#?RWHDC#~R?68##&L1Fy z;Q3{9=%vMk@ioBe4EsB}H~ImfCg)oFe5D-};xmt50t`!6_m5E{9lw#kA{_J zy#qj?b+t8?aCUrWdY9{w)oSA+z29G4JMc+;qm;8bV=?#v+U3)=B}`2(1=b(aVOHj8 zZB%$ukQvIbTloLjd(WsQx3%Bf&O%U@A}XLDA|Rnjmo6g3&>;|d5dzYscTic1 zAVo^(O(gW*J19sAO+Y#!(t@E%2}lXN^Im(8{hW8a=Q(@7W1Mk5oiCb%!fM?gd`g8Q@H-{#sjj%wcwq5_*zWtzt-D=TpT-I&v6DR;3z9C}W zQPE)yU|r$!&m#xe0)P}BP**74;+J{yu*4`v!te~*X;+r|gX+M11ybO-z>?{4HWa z9PRsS9$U$*KI{)%Lz|Fd*N{0?^_QR6Ruw`0;i1&%NWk zLH-RR=iiIJ`89l@za+R8U%}1R6L%wAeP53GK;tmrJ?|HOt73e|?r`mo`C45fc3<}$ zUr|c@iV9CT`%&lAsCi=Z;zM7Rn`ImB$C?T$Xt=Gf^@pc|*z-`VjeTQB3q*VqQ>S|$b!qUPEdX!` z5U%oU>>s@~^Z)WK6zCSYf$$rB^7r55z3Cl4@}boBa3b&F?otm(j)2F>mwx3f zgUAQ%k&pug=j^@YEH02>LKherNR$jX1s+aGm^M`qZqd5Lo=97}!Tqxju@X+Ep`L)} zvA`iy=Nj`#70>iHDaeT8tpp**TUA6z9>mNQNI+8D8hE(`xAZrzCC8zu$Xt*n@<(?z zFR<~^KeNcWg%H0{9uC)RLHTgu`AJ>;!0Sw%5X=2R2lTI3&!KNtjWB&34(urE+eJn? z#)<9S=`%1x;eU|;wwai2$5<{3DS&nem;Zt>y#i+YuQH>^{zQPXvN8&Xp{$)Y?m1T0 zr1U4w6F+?zcd_jF@*?%nJMYBE()9*n7gWN?Zs0&A=-snfk9}4pQ(pFUo4UU2?0Gb1#*fwcwv!(X|E8N& z%WTJ)uX^MgtBzJb_ zbNm|rrrjZK&MO4&+1Y&_J=gtSM?+k+)P=7u`p5zknk@H1KZYsi1?}nbF!62o~aJ|Z3GrqhV49hr|q)T zTYse@C(KLLWO?`s0|c2=QR|aRy27tiwwXj{swozutYA^*lHRb*vibF!(_hZq^2~U> z#M>XR0}pmOuH!q!hHoK7JicW_gF5~;sfb^5eTu_@$qv=seqLcM_C_MoODiAo4q09t z&iNe;P^x=u{~qaMdd~=Q27)9?2MXnOSCWt+YZ9aXWyN9x(-Mq@sOPffgG56OFcxLC8FpDWaF zA^j#Jde?j>l+&@jeeo{5a7{TrR=%OyRFxh3U2Ndby=WNKRPb*%5U@PHvoy;uhYSd5 z_Dcg`oVl!@qCc{Z?rA3Q$iUvNRJKg%jn|ZQBB5#~)cS6>J2}ZfgMNeGw1s`Ip=oF% z=@u82>OVpcui>NZta-{q4S4c8gz0^osi5|5n2#_R>f=u+`u1C(pwaQ#XNiq~uXjz+ zht|%CfJIp_FE7EE>Re!x5l#-AXrMKl$p2#UJ8PM3pgc1~(}Mdd?T0V9H(6~%(l?uK zXwjQq<@oE*FoUGe344ody3cx-3Ll=Qdy&TWnl)4N){8UF7th?yxZ9O=8g&8nwx?<7 z5MNQ-vo?ck*b~b+Oj&mgY{_&K_V*dbHSNxJPg%cX<0fkv&0T*LwP;p>yuMy%#VhX_VzDE*{ z;(w<~xpkd7xNE98fEf*us#D(VIX~8WJJv@p)c>N%a+Qso7ZB4*i+(#@Vvk(`xox2+^*9jywx&N4c(sv>8>+Mg`&TFj6!lOXm zCDE8i`;MiCDQ{lk&k#k>hEU7*fMRO4>E+gXA{AHo6tT&uKIM}0k_6s+`nx%8Ara zcXO=xsQ@<`ECL%;RGNDxcaYZNhkm5XI=K28TOY2;xs{h= zNKj;oQ511=`pplem(QWs6jKoME=oy*AI1D%D@N2R1rD43eq)+2m~0ic(Iw}aS5bTt zeB0~pErHarCsNyQRqap*$IJ6wG1$qVzX@zQ6zDF@^aN&Dg;mg5vBv;YksEm9Z2ZMk zxCSvIBH=eNiMh1yE?oI~QiA5biu&NO^i5V7uWdz6S=4ml%F1;_`SXt25bYK0&OCO= z;58O;SrzPtPArH<-`d}O=s0G%y|%PoS{CE(p+_p;;i~#3i^=IN0o@E@wta2?Jc}ue zj7|Y(g)ohOos?tPk7YHrvl7`Qi(wD9D!-FU3ySNirG44UZjaSic7^-`FyhVDnO&(d zZbcwNud6}7XE=5^yf9+)t1N)BVEsVH)co0dxQkOD%fO$p`q!!ve+I8U6Z z|LF~9r)R#=GnOOIw)H8Jv419d#tkR78G38{1Q-!BC4R#}PjsqO!x4jwSA{%^90Vsn zK}{A!F=_;1xeX95By$XM(@=m=8WFf+Gbw&@w5=E+bIY^q_Wj#l0#9+Zo&!=Ve2s@}{oSp^yOl&$BZ%+(F2<<$Nnx~J~`JeJ;*3WUEY3_~(; zG}&8ErbJ)B*q_vfnyHWCP#qSC(6CeeI>p3*-5sS{(k;4|HBjT5h~nt$`s+p3Bg<$8 z0o<>yjH|>K5AU5URV*prtno2>w)=~PLeBEYUmShu>2va81<0q98gjM|3JnngH`y-f_rPd+l^hk9870vV5vAki zQf0#^XK9&7SDle9OW_PE>6d=;ZTFS9DWIBtRnjscRHn8hR8JCm9i9FVXVpFqr%;TL zXwM3GP;6>bu%1w5mE#rGb$=>^b~WO#lq`%+3nG4?kyv=5M+jk-rf_yu)>2FvKW(!> zG?zz5rco^QtcE=8;~Hf(we%RKt$`mxAXq zIyJe}g`(e00~c;dlibjG+}SS1f+UzH*+ z2X$Lg7AxA93>f&eIzX#u>3XcbROeC^Gm~M-2am&BBis5uGvdCRFa>K2)gn{^E$MHh zVdL2SIzls1kAR%SIUAwOcsdts6*?_}vog_@X0|;{DljbTL4~bT+H}cERw!hD`r0ZI zp3Pm65W>z;93kKmGgNYigSc+{^Q?^fc3bh!CunKdeyQAOiLs^zCp(kqO12felE9XN zuFfr#&yP-gZDr(HLO1d28B(N^DzQrYzLy3R2)!X3M5I9kXAXH5nEk zaM191hq+nzHe##XeII8z$Zek_vJk?U!WpI=xk}$6y|6U_DRLMMCf7!IDbIa|@PFK; zf=4eOl**%iQ{D|y-;xh!j1U$`q?tGjRe6^}CAU~7oV#N`Rhuz%;w)l&8(ptM%jc?zfCQw~8i@ZBk68KQ@IH@Ps=G@Jp(g&NcNk|+Al>E8Q zg|dmcFW!u;ThM-WQpONUDpXQ40>G{-8AQdzNF~$4Bq$n>Hg} z_)@XZlLap9{S2rg2T=4qX%aZEY$OE|^qG`X(X73$86*GE@mGkd7i_5s5Uc< z%F6pwAx|7rb>AAk(6}WD6);^Qg3HjF$#hp)FO&Xz4C=pp8}I$0;CY=!()keAL>VqQ zaOa$4Kqo9PQ#6FMTT#gslB%2YS<sTFB67sW(V|kI-D5kOlCq8F5|=;TZ`>%f zfLUbz&#h+qEw7`^PMVWaJ10#W*i;OkR>*9Luf?@A4_mV*qkb@K6dqOOk|Gi( zgwC@1(3H2D02R+^2dh0K5}o;F8agIxEEmFbR$g=$(jh`CQ>-t%xfS7}0T1arj(#Z0 zf3I*;CDtYzYF1tPk)q6qV(vcUr~h1X!lwSlmF&Zw;-9C*ek^Ew7(ic58%QV-ou_df zHIzrbxx_ahD0QcI2jgN5W9hSS>2Fy~NKx~Y^WlHUNd-%m$t_JR1n~FXuKb{jP`NZv zuKxoDy#O-wP<1#|XZ?@=|1Mj94ZigE=g^nV{NsZ6NHRx z&5l1f$`8VJ{R!cefOaXU6pz>X`%A;;(1huBsFweMrz}R{h8cQoz)Du4uwOs2wm~Cnb)8};Q_Lq zm8h&i|1i3l5dqIm*voh(3F_i|D-eV+fyFTeVAfH6ifzyp1*aAq!b%JtP?g+}#aN;x z#|LOmopQaeEVWCmp`VpeK13h7o#<)^*f}r7Vh@gfWeAW0OjoWT;JWnm|3D1xBkWeW zlD6zXeUxB^Wy#$l@3`IGi&1%9kI2-~VaSMVL+g(UA+Tpc6C+Ii{?2osk;1%^UC7%v@@ z_3kSpNK+{=VKf5=+b7UH2;O}8y$oOnb--f$i|*(4-p3RIF2h9b5JO({#$A0jbBZqcM8vOZyJTYpTzurfP zMejmg;{qxa-nqhQaC|D5#{nZc z$f8Oy%mwsxf{!Z}T*|;t*jhC6wv(%pKP3!RbYKPl8%A_pORS`iv&ejhEnUTE$@x0D z*@_PFmQNFJkLYd@E?D5d2s$cy5&0`N%KC#pV|7$i8Utie1_gOSj5-EQExo!xhe{BA z@n}S1rx)J1n^9ih&fk|IfQy2e*ZF|4`i45Aq-tF!QZ3Qg6}ub+SN{Z`n4V^8-kPs1 zjP|&%4?2poxnht-_c1D<0lT9staQ1SpPW%1%0iL~hZT9yeOd;i6A%=1)&+!6;h(gv z#y0E@z=~xLxo-w`^q}B55rAo%=YsZl?--iWP{-h(1IP=4wspz|$DT^w{u)Bpq;HFDmb zi(vfJYjv)RWbJq!z)sXKVyO8bY<|wk^2VuATciyNZ8Z$H`>W{1Di3k*gt(?_O(%pJ#9}<1(RIA$kD{SZ`@mnY*I59oa>lqlDB00U1xrOzb zC^KD~4m4N%BEJC=;B20A*&?2z8=k~Eb7HK};w|gH_TPzS4UY*0VZ+Md>D9a3ne4aau7KtBg+*FRfnP#N~H~i2acTbKjp>8!QE~1^JsEL z4Gh(i6cl2QowspsW7A^GIHQ!F(O(5UwPya6^=HC+ImlN~00}wT z<~yl1S~NUX_!_HHfrTaMUhtrd0Z6j(Ry_f&Kxuk<&H5mM>wJB>5RS>T0&v?d!YaC8 z1Ker?jjX5xPk_w$JDjbabyF zlR_}oW!ZqT;Ku}fl@_#l9bE&l*41-Q}$jm;mEbvnMH9iP!p@Y{R2 zfe}7IkJP>`O8oU}_P#2sHWscpFd&4nx75%lrhFBytOOdoqJd$$MmEPE#=?edt)if# zA6F45-n7JA!PKe12hN$2=e0z@!0|bUrW4MmzEvVn#-Uf>ju~kc;4RyM&>ead^Z9x6 zTTk=$BJAafA@|#S{T$5T`jHvsfHK8f9JWF^%8aZJ!N}Bf0kUDCAp`L#J8m~7EyAP# zpx)xNMmcj;R?mtQ6>0^3v>1R=3Mk5=z`{qW@2h|zv6K<6-7{KvAQ!xb1%+|oOa_DYe!=&W!YLpjS&zJi?06IROi318z{L9M zF@w8U1`14}D_nmAj0q34qb_lSa#WRjF%WspKz9(5s~x_0kJX~P)lVRc4t1=Jy>Jda zf`SIl!hkFJ{#dvtXsWgUu;LS5I8j_li86YU*cMyPFN;#rr5~>xaxk{x`BSVIIo|&5 zZZ9~#G)+shhxB8A4dvg&3w6}KE1!+IGN7!XFFyBoH-`>R%KqtQHUWvrL!{HfOExlVXYckPSvp$HSXyV#EG z*l-${T{n!&)|lvImFVLGPBmn#;ZU%<^_@Ut{MZ>xY#jjXfuWk9=a_I>g7H*qzy0z^ z4Pcc}MtNr8 z5Hs=udtBaMz-A=GC0|%6p~ja#-j1Up@aC%Y!dP2&k~#8P7FD^-WxyQNcDDQ1i}Lz+ z0>PXpwIJxJddm@qhR8wVk3>t_3GeQMwA^U4-rJKn$b!62R?)qoMi5Kq)&lMu%Dv@t zpPdz$s?3KchY@n9V38Pz!6 zZLrU#`SkqEsXHluz#fCgvy0<(=r^z@30e3=C6JPe!K!|9qeS z_MrcDY5v_l|L=wR4@cDhy*dB)LP1Z}zn<3r;K^wQX6L`m+y4SY`)`fte?D3Kdm{>r z;=eZcKe599I?VrlhWq~%e)oU>3`e+|YY*m}1i=P-5c^0F{C|yr|1t^xGPnPld;V#m zsjDk1E8SOg@n;K9ikFn33Ev%HBK|~J(tZ2`ynFK|Ba;5Rzw9##@LIOaT&h4d|M@s+ zl?sfr2X*~fMPRT0xTEG_$xhAtAm?%bNPjQBh=Wp$W;alnG3~Qb)Ewxc#(4xmtY`v$ zoi&9N-H!B#i|zorM24`K03A7CeBpOq`az!YTl(WJ(D}Z`23oO!(yy2ctNXO|M;@L6 zDOj@H60IiGzrVyS0pi1u9%Tq0D#y%P+ECc3Bsad?`0X3swLf4Ikb2uX=p0$$b^QT} zy{d^5@3s7qyy_>oCFBYk@%?`VG5hta`+TM{c`+BlG2=1iK+C0LYiy5IY6-QSzP1NX zi1C~IgD>|*0|Y>*cz3I9O3Pr~El0v1fELo{C@BfQ5N=q`oGi*Rdd)PTV>LEa;@xKl zcvoLWiCFLVs-|Lw!SC*_oqdh{QVYbwhu)QGCR$F@GXHL+fC>}?ZeQ1|ct55D07yhb z{K4oF^{9y?wHm2+9Ac@qFPCe_~>cyqMd3O)jXpa@zLWI#DQ zvd;bGA&V-0Zz104DcA-oeLA7)385Xhx3;`Yvn`>AlOm>r^tl!2~Z6O z-6Yc6Kr)VxWub1s-%&IX#Cbv?Oa@4$+xol?_e14e7fHO@qWvRA1yTjXa9B=gFBfkD z;L&eT66W-{rxy9#J*Ssx2k=98fDrXAuDmvSz{yy#`GqVhLX8q|20aCtAUYYuAsDSo zfEuL%m6T%xsM`wFSOUOa3~53|pb7`&37uyb-myk_feI?AVjDJ|2X%3)5|$N&{b+UU zT4j3UZdHi``@ZAL$ePAQo+@8UcSu%o>g!b}q0-V9ADg`Jpj#-#eFq>-H89E=iBx$9 zm6QCC43oRGj80yzaLv6Uai8i^u_Mr;?sIa8msyYf1$51h0HWxAJh*Z#yWr#%z;ydnhe}_2xW5oHOf9g$GxR!LwvEU;oz!(#ePGoDzu)h<%<4*4 zRK?#{V7hF{oePB13i6o3C77Y=BD+hS*y2r4JKTuqsYh2Jo43#t+I|K0(v-7KzEe{^ z0^MC6XfS}JH3sMK=3!oi4m+fHKLZzqr>CXT8m~^tNLT(_KsP$0L^Nk zokYCqdkjb+wUWm`9nIzTxx}alD8JT27ITOtv()7Z_a6gh+a6P22?$yRP z^WpZ#!@}t9?VPceW70e?M-J_KE)#opvGZAo$SHjREw)RM5U8SqEsmO;&j;WVBvYI8 zKtNUD=l!*C%x$tDp{OxP0!v2_jtOOE%e5?zWpVXbTo?qZW8k!tBL^PEYSA2a1|D@1 z$bc^6vu=ML4QbFFDR3M4bDhliZTF=ge9ysV>=zqt_9>N){{CGo`9s4LT2;^d@%JjZXK5Kmd8EUcPpY@&2MDrbgJ$ZkoTvJFs83$Rj$%(PxIu zmA1} zSJx8-fEt;|A9z}HI6Sxlgtc+CANi`9X3!6=%hxAW!d+ZNx{k>hfVFD8{~@o{H%&)8 zY@{&Se|V1uVD7o3{D)Z!5-$lH`OBAl(`Gi%)Zw&Fg*wcgT$Q2fODj6nLhCfl<_k-& zu2s3dx19vFve3K6X=(aKohUQ`##QPt%Pf;b4$gj^)%BID4OYi%l{&|yXcW*rn^H;Ia>HTeeTaa0IH*zm>zk6TZ zN%aVVHdFvLUN00Ta?|a0Vr(tF{l)q+Gz?^YN_8013kB6iqZWut_2VhXn<-86$gT#NxAD zTQa_69s-Qt%reJ>`0u?Z?^LuW>Ti9JHNpjtAR(@^!P!0k6Yjsla6bLw27~z^?KoUn zS*XvCjjCs)TL*|LXf~Sxn4=A;87ALV=MD1%Q;v0)_y)Hc7GzXss=Zl%5Ij?9y>Vwq zS@6WBg7`^45Buzj{wXh`P3eusZOA!_>SK=z*w;r3-)M3pKl`{S6g#_GR2txNcr?JK!*()A*2uG4>BQ1*!g~D z;?HQE<({DPbRxl{y~Z~3_Ex_MGlP<7%XXyT?1IN7CRR-T82pKQZ^GnFS4XKJxJQi9 zAKR%^Fvje}KZnd~Mk1n^IOmxf>mLam>F1?XiCE3SXC9=>E&^zvuhV1dN-zclyzmk=Y5Swlfcl9}NF2-r# zTu#LyzuLM}GICyNS^Da_GjRUL!4HY)*~@?oskX2ESgk2=%-FLDNR-#`lvs}Wkf-sX zQl*b%F2{U^eMw=PRaAa|kgfu&C|ck58S z)pUL8$Re2eTj#dMAlz5qp$wo%MNq*4SVgooIETnJRdC!x)VHJBu`38Y?xN<;MJG+a zzEBN^4&hl*mS5DJx}8@K?=Z`M57`GfupKmekZR*S>%*Pqw_{6!-GhSjx~XA->#Fv1Pr!incC! zO=-Yii=1Lrn9)C@LT?~e&K_?S=XP~vx2D&NTt5d2B+75fVybi70Rn9%U5n}9TX00f zI~|nJ0)RvsYjr~K{T-j)iCn~>fVH!CYKjduNLjlbpaBpff>JSJlQEO8FnFl_so{HaAiSsd@NuU@3G58?$g2Q7wm%S0t6D&0w}Xr1{Ndy?)-Z!A zdF3d;|3Crw)U-JqX>)I0g}iANmfD19Rmq;O`>r4ks7@56x1vTaZPOw2uCaQ)LWBsM zvwJ27zp8ZIRz-2HSKla}g<-5RnzGsDO3`69@oe0~SSC~45|eZL6#}PKnjlyosri5L zYCX&J4YULmF&8|vt=Y|W4q8=AE4!*xPbGa!Oh3+Jeh+qLAn~}MCqOVa>nh;e4=~*W z_YA;?%?()Fo?1mK+4+f|5*<(!(_c6+cpHE3Ll~oZ`qS1G$wQvRtz+N?81Hcs*?%kn zDyO$w7fW#E+tMX`!}KC=i)3rb5w@gqz1ygbgio_xt1p~!&5^+6NnBv}k1#ec6FiT0 zuqft=&(JE_im=zD-+v0&E*ULI>W6Z7GCXHx2i&bsCNf}zVTnT62~ zO5eM`#ja})N%cCl8`r zzvh{rG&gH28o=(5eDrm*J>7vZVC_TEFFi3XX1e~x;KJ}8P~E+h?HOV2s15hq`NX`Y z8x7%}i;JhN9v(w_;_FmQfcV;1<7lk^a5?u=(gb_W%H-}@&qoQVqqbwfX8ER9L|jE2 zoqLVFl`5I`>o1O8aj(M45CV{Bk4t}hUOd%OCVwRLC#bk0+hBNLB%%RAuMn5jwX>tW z;f)>}s3+li+5qNwvo^=l%il-3CJo{(@0TT{r(z7!ucI9`ocokDCcF~=K#fW@cPz$@ zP6?lg)(B?fOWIP>)s|%&M>3-wo9CG(q}KP&q1VwrvQw1nWxt*`Bj|^^T_@V2r&O{9 z(Aa6fshqk9gBH%dCJR8{z~13$eo&DVLOwhkXNnDXx6|b83lpn*hz7pi9B}KOW8MWG zF9r7NbqpM50TQ5K5hKZCJ8b~C-y}SNOzMv+k7(BhtB_HvBQpM^$uw9@#v4(TaTNqC zdrYu<84YR11KsWm^D`{kS$fNk2@%-JK?Nt`$+U~>8UiN(65oENIMEshS#xNzAwi!lkOtPoU^V$)U2(xk``lx#FH*3y<-mg{ZXNyt zV{b2DV-Fx2)rD(jFFAR&0V7I=P;yeIv8kXGRMxH(9(Rx*x!1Q4Lp`bJ|$9pfmm#UPST z{poKfl0rca698?qVg%ljQ{}`zDtRXD}et5UwMa*cHZyn zY*}+~;@X2Kj{le$cPZ%f?$l*SxDZz+Wx2W=>v}2yVOp;b*#I2Ch9E+++^(wEz~e5L z&_0olF8&q{nZmEcob;J#)$$>alXE^8JfV>G8VJ>;X1IVey-W$Cuc&p{-_o%|W%|qm z*1Lmr^X{>2c=CYN4h7w9HvNb6r$!k>jZmFXEW}v6Da(E=L#saZ`*pPB9h#NPNqjm@ z+^3yK^{s8}Ej5LF^)S>qW&_XGgI&tI{_-takH_GeZPy=q!>w|j7ujy?ojHdNAC!C) zNd=4I-5_%=UWxR2vhodoiS8)_n7`D$kA~BBE5*r!P|YhqD@ZRM_l6vOi_5 zaLIW*YILf>2hd3qv&xEwGowcnR<8hxvImi4*xwOm0=Z|7&HGlT)6Pc+MD{)N0~{Gu z(Ui*Lnu%ntewmY_G~`1-qD`Xd|D!jmg!&IyVF5LR4Jt@L0&a8fsmslpt*1V{co3YT zRlkWTjuWc`5J{(MUNZZY#YS|y)<(C50i$;O$8oEKi&R|R(y2_HB%BZMJNy9^z4~A{ zM-w$YFGwZtTDC^eik50%v9H~^pZ3tvGIS^n@7K_KYk2!dihq&?V#tX2_oawuf3GXj z^=pqF^V)GS&t2WttdD1ZBAu)+^XK~l{l*G8wm9nt{soc8U2Ly9`&Q6g?HBqgY6VET zT$$=*o+RsHh;rFi_eEWb^gYd3c#Wsep{IBm+H_s6GKK;v$TSVWM_B4;ebvSMi7x7V zoCL%VWCfUbHP3qO)P#qZ#eZfvTi?-ZZ;vMF$bdDyly!pD^X|3p%So5Q$nmE@yg>Dn z{ugz?ZXdl8Fha2(h5Vc(1*@PynQx>ayB?ho%u4lZc0q1|_Ph3DUWqjU<-XOyK(@68 zF+M2VM{bEB@T%jQQ~Dd7H-rxkV(--ibkIo#(6?Wo(1ohgvt^*L8u|wGNgo z7oa(L&OY`?WCY!db_B)U;nMxxD#D2gVEh%0FeZJ(2tQ{^`0Drv;Jdee7LO5xu*YqO zsJ?zVrmYVuy^}aK>l-4NLY2Mp_@+X7q=5Mzh$Etc2&W%u%zEhK#f_JkcT0mE5Kkrm z)D~LT#=bFXzv{sR;y-N=&|(R=S;Q3-#@pk<%0#5|h&62v%OfyblQL4I+lk)pyW-gI zAKDtOw6Tx(DCpv_wS`dyW2aYciLCknzmt+?UE5oG{_Vu{;rCxU3~N*m3d zE>d9kX?$Sh%~`$R_U1Gvmk(B;#f?{WdV1j6IA#+kPua?_my|#Wk_fA|+2{cR_>R zwm0ZY6Ga!Azc9!hx<$$qzb@NtGW(5QKbGMPe-A`(c&h>%heV>SVi@DKvBJvAGDgRu zHBNO>xznS6z-o9-M036R4qk}8kFFU~Zqcq&d+_2UWsp7Z+B&uMYZjSJH}!if+o$P_ z7yYjA-R)hs)32#9v7l;6jZZZP>e9ODI<`ifQVm-hLk=~(I2YVdIFirUb7DvbZ&*KD zj!7<7p(vGPUxRrc0fwK%U96UqZuj646lmhTBtz1c5nd0B1mRG}rwq|(4yIVU;7ZQ8 z{xqa7XtYU++zwIq%amOGJs&x6atzGVMD^(=)M(x7+R$zN?WP0YgR(Tm@dA*3sYoZU zGJ?q$lG_Ci(G1k14w-U@HtY?+#Sy-a+hfd3Jvjlmj~-z*YlDni=>FXJ%W};}phJEK zfSz|<1BM{*;*wh~T_KEh69kp+hh-oGhvCY5U z@a=4jiFp^foh%c*$9=-i9=-;3&JQQ*r*T{-4s~F4TlAE@#(#ka@83_W<~t{Cdh*`C z|Dfl@o5T`W#p^Wf7(6-tg97;9QbpOB%x`cPxk50Rb5RTXun z-6QW@sdiu(7XOM_3flsvVl^3mM99ZK3zyk!&nall<=d9$r$*1_;QA1x- z$5%d*IRIl{Pch~ATz^1rFJJLIwV!^&Qa=%J$e-0+jcR&$xCI!|Uq)wASL~*&Cfc;) zOu{m>%Ab519&`CN?tHRa9s9m{JN$+Wxlkssi!HKYPbYF}EvfT(c_b3ImPy|5#l|o> zcl(-DxNVOpPgNg1%%uK3s0=hYR>fSTfN@Q^i4eKdU-VH0qJi&k%baZc@<{;wvleh; z99GrJcgH*Y6k?A|n1P3;Oj=iU@)?FYj3C@JSVPdX9O5!^mr95Qc0|8^mjdr~Z0exS8HwKx?OY4^I=@>>;BPRl$EV479hI0K{XPs> z$p|(&X>N;itlK3_?ndXeVqN1Uf*gz)G#L(p;NHC{=g>Cc4YtCsJ(KS&ZGUUY1;Dp|VYy0D+VE}|W4jbH5l;=yH{Kxj>Zp+jr_>)5 z3bFJ4JaLK7XmG0NBtYn^nj3fS5NSmic)_ayc1?{UavBr$Yr7g{C|{0ZM6Te&m|j}5Fc49?HCH-#K$QfJ-7|22>}f0Pq&p80Qs+9JO-&q zU0#yZ+?7Zr&nhSh8+rUQ5|9+f+y}A)H*W;;3pg7G>@N~2<76=I7&?n)fQVQV=l~R& zA5l4pewisbflHnNOTcMXG_X$t3BULN6swFya{*x1{|1Z2Q3yzFf8<0Sb;3k`0~$?u za@gV~y{8pziH3mp&)0mQ`4pN< zS2et&ZZ!Rr_tq=+s=@}+TcT@}8dR~}$d-c7a!5IQu?OU!IW z=;ZbCL3@o~(OIB@<5@auTUGVFNs0kh*n}DxX#w^zZi~>2I#0CaPJa z;(>EYD8Iy+IHBsY`5V|dqk*^p zCPGBBn6z{#=LZtkS+^*#6ayWxH;9)h^5HjLE{Adz{~qs@_5hLpoD#Ka5$o=wi>xvM z#c$nAi<&jnK9QmpbGN{qDbcAb>@4VV@a-f}!K&mjfk*~VYV~gQ2_?RALMoI++5Yj} zp@Xi+@w7*`dq5C2;)PZawurdyKD;Glsw`@<-`8@8N6L8YRc89_y`)Gvv8GXKfWL-2 z8i9aKvCr!InDha1V1Ju}{3931hb#9t^LLQXYg?&_fVt2bA`br1>wI#IKkM|-t1vHTz zJ+*z@cbikncG;AcdaKao5G2Zku zHT3U5t33Lb3GZgMOCf2e*;z1b1GoFz#{v&)0=eAZ4Iyh0?2x=d&7=t&JvWZ3+XTqS zxizOiUo{a-d_R=YCmd_N23-LLlb*Qtm-+X;e-op*h8&K#qqNkrarf+#vbV@zsClW#{x07CTS)0kF^b$SxrWn+RY^L<#_WJCnNV3mHf-x z$GhV{1mv(P(xW9-%<6phKY@O!nxt6|_rvpORc3_G99&(4XhlpB=m4|u>HcPIsu#QO zR;dDF&r`w-Ybek=1OxGno#FJ~Cq{=qanU+Me{JE;t_PAV0!dqBkCV^ydq>9sdrv7! zkM`EiGP@C|PJ9j}&3aa3s-vF{m>r-mpxvHxy6;g}sV`=yh?Iwm(H`}*94Q~~WKc-` zoU|(;Z!S%iW3pl^Z!Rbyi?_1pqP0wxSLlU?F2bVh3`l8`Y-2U>Ke{n7OthwrN=P>j(VT|AW;eYjm-6_rJYs9wA2eeyT^Vul1SL%p3qgjr#V;Ro_0T7WM)tLp*wU`}gWnx3On!+ie4j!k&4%aEEEk zWm!}}Be0Wwk;fp9>-AfHUm&A z8ABEI%9NDdy~4&hJakxAGmM={5LVrdJjsM`G-+-k z_41``-Z(t^$=`t4yr+5}?|(!V2$ROO@=k1w4HrkpMqh|?y*zyw8z}yQSiivs^hb8< z!aT$JH_}OV?mV!~G4K!-Q7ru+{qB^J4z%5MgWMH8*doXh>#sadF3{f}hE{jizSSdR zZX(nUtjygf;P98|k(93Sow2q}H7yS1!=; zz^LxESVUBZlyLdaL7IUa>`Rp%aYp7)tYad7I5y*|SYM?Hhp$H7Spd)?|wWWgSLaU2RzPY*nYbMQ6IF!P|_ZpPGB{v66G>qZ+A{)x8zsg}71l+SwK6!-5-3zgh)Ne^p`_ zi+d}og$aLhsd5=A7U{LrXGA{@g*SOG*voUSW0rLEckT?OMDK>DJX>JjI!`Wlv5S80ga`k^8OtFhaaFc}$imCnt7yBX zqKs%`FH4Gwut=;aEeo)V%b;?gFPY%uI*@n2;B%~UKI)FHU~X(*zZ9$Q)`DLQvWITs zI3^{dJQ0YUvicE?gj^mz%lo->y6Dwi`g7<|7oQ=Kz4)qgXlEe46DBFO-|}|3ysD~8 zm}VYV#KYzdbcyK>2%Ij{U%2r=v_Uz>;{Pe`JENND-Z()7l%nseB7z7iMG_#vARt{t zTIeBwbQA&UN)kY0U}l0ZZQ2_+O?X@;ty2_dkPy21p z=0h^)WaiABJ9p+j_j!J0%NWR6MppM#Izc2-b@6iEGgT5!gDq;P_|Y-OgExSr3cZL8 zAU)n$mUuj!Jp#^!5mU2jjKY@&#~#5|I%Ji?CJwa>)>Q(|y^XbENNf*rJdSe)qx5B` zc|V@&O*w<%;0iPMAtQ-Z%ic)odgey@q`J{EG-m6qMg8PgDO|M=s|R$~#gOokh==J_ zhaiovOEUU=YG5gHxNwuUJ%im&gPAJoOB0~8YzmmHo-#kQ+cipC1fYsG zE9c~acMcBV$4KF7Zm~MBAw7-Z05m1@PMB2an~x>}n4rX!rMfclgC!e#82^mnXW1gr zc=DiVcdLLO8vTXn()8a6Cl`Ah!*<{}8~$5&RDj)JN8#bZ9X@xbj(w$_K-+~gCj z8o|R6iz8FjTaop3`mG_sG1ojU%;)&plDVT_f?_1NurT~y-K-r;A_zN71{Gcz8VWb9 zx$p1x629jmncrthK;0i1XALaFqNQa{O zl5c#IGlpyjOAOxB0;NMcdva+k64E)x#zqP*(rcCu3wN?}k%QZo9#8n^%!p&<_zl74E zeK-0E->vfA6OgOj?oAdPn|OXhA46cMhZ1ztgS0Dg58$E=F@D-#8H=U0s(9l{2_(fe zHMbM2NgF1`&_H3%W!ry-+SRH_jawM`bb*lIXhzk=$m%OXvlFhI>)lN=YJ%IraR6~f>p&LW2~R)9yb zyX>NL=kQ1otdd95B8FV0?urcibjE>;ch_ zwT8U&vW`dFGQ+RGVLwBB_XL?eJh$pGHfT%?L zsM-QlolP^ntiI&?N z|1!Rx*Y>nDv8&^7buz^T$l!E!}_9*l^6>R3#&oW|-bsZ>8tN zswxV#G6lem_%SgxMXcwGe>bLzNsQi#$0G=5MlISLr8vzAVk}EfYu(6|VcLqFwJ|}- zx)G3Bj#DC{rS<(oB@po}iFXJHVR&4gsC?Vjzp{1EQ#H&l`|exyfk-l+|L4FchRe4z zn5L}_sTbi4#pjCZ8<~nZt~z12uuZkp%<&0M7#l2ef$O^9{~;*g)pi9vjgq@rPmE9s z0J4jIoT^7?-Xbar#_y9&Y1!z71%|5;hn(BPGOhHgwOk5Ei*6$n41MST0+!bo;OsF7 zOp|%sa>)K54eL6vzx2T~hHN0cKV)$AH#BiRe1FxueXkqmu|~DW;-Xvhu*&U2>_=z+ zp56Sg;e9%8N$ZCk5noXVM5%r?3ho$qqJ*XA3fcC>xV;Ha^d%T2$ZI<=fBWs1XLKPj z6Wim1)psYfz42XV_xi#?GICi(b;s$As zTHMu}3K(rkHIR-uqL@hSY7c&QIxeU!s5=^KfHCjk$82AY`_!YWU|u+$^~!?&3J&@$!M8pU0vt+ei6uqaDGiN}EdT`al(PNzbSX7=fx!|gzAfyZ zn`Jc<>5dDy+OzSZU|DErjWljRK>5#z5SY4Pp)#PQ5G~{Bvdd1QO?pg`W*#y=ivVie z7S~0=71bFgne394oDwPm7v?Id+Se~1`7<%F6mPHjii zCy8R32)JUCeqRO5B4!($3+ugbvIOhz&i~Wn90`B}hy?(1_Dui^CUqB3f7CVfi4@k? z5R9i6W(IlyvufREM_%85A5qew%7o)1)M1EwBAMnOuI#=;CCDU%vQm^_N&2Etoqx!6 zCbpw>A8S`duBZ~x&e4k+KwUB-KOpiaCDozy)4UD!WrMr`zT0!`W=_~)Wz623U8tKa z8C0TNa?|BH?SwlUQTkPr{$r6LJty02rN|ERO4F$0 zJ)krG+IM}Es?Qep7{37m!HE*Qnz_);;G?OM$egv1;aG>##~YS5<4DIBW^{ZE7>+bM z8Rt==?p3HG(9}PenQBK$nVlZWXsAUEG-SC+dul_5FKEVW#fiBf=L|K85q*BZ;17oj znIwFUhzhW>$=kP*S>G+SWO;b1Jgh?ox+t~L3u$?@R`f5RfvhVI;nz2x1es7ANHH}6 zm>FU^1Ze5Wja6dw9PSDw0ap#KF|@MqLh{4B+*;+|?O}O2LG_}Op8h429AU<%rgr0M!8}$wdWXWWK>GhAk*+cR z(FzM(+llHKJ>_odb15hRU~AK+1j0BrwYrx^D*BWdxVLM3 z-ef?JOVatWDiQxgZvK%aCYLF}@)*NN~$Z2QLSbBS5c_N>Dt>%HYD|K&++&SIzZ=KJvmKs(;ePo+;OLPt1KvpC^G zdhF0U$_>k8imqH%$T^5sOT??fzWKX8tZtC1*?KsKFg0#;1AO2-f4;_Zt^vO^rI#E{jZ8&8Qf zyY#=I)KBZ1Jyd+_jV$Qv{o z)^TUze$5tHmz_TV&NrEHfr19@fW0$+Mvv{{A1yHXIYs$w<}qZ&(FO`XKF_}z9JjEA zTKZtBut_DuoDzE?xjXyJ=4ffH7yKfH7h%?_8ELPB8ZwN1vyxsnv|MB+AGyAiY7K?Yo&|C>XI`~aFV#RiSo;;Q{jzxWstqW(-zrT- z{PB7g5?T|D7}lL{xkSfhNn^N2CrjUITcVPSW2^T4Hr zGd8VTwhn_QHJjGl=I?xwhP}a~*z@vucjbJP)gPPb`yn;TJv_Jp4V$W%IxTwhK-7$- zHLs1hm1ITPlYevN3LfTz_F@i=D%dDErTG5FvVC`=GO}Sr5o!reA?ZpGVL%P43kCD3 zuMMzW8=R*VtF%|mHwfdRU>}2E^|P*PmueEu-M3|7ix)JP&9W!e67SHq!yYa4ms)?{ zwSCjEP3toh|w{(0h`ZvD}BF8Ln`Y*O@N;7o+_ z3#$difB(z*zTXa^7=$Nr>6X`aFA{gRLmy{nBx|$;VE`8t;ZaHPsozb1Brip9q{a$i z)xYx!VNq<+!i7 z=-e@-d;{U`|18(~D03<__g=(K%?NKdjNX6f(hPb#UMN|7lj{D3R}e!MS=;UUS+%zP zlqvDt^KgMfI$EKEU`W645qZazqiaAuU6n&aL290l;jd zGIdy_+u+k&l|wp3zrB0II&|ahqa)0|l{mWn-DUfbIg6W{E=^IGB1z|0B4V*H}oFc)OOXx9SK!+o=7R)9I9fieHWIe2E=1n%T9= z%oadZapGZLjdh#KaqbVX4^IKEZ&_g`3K@2s9!|9}J%`k$&*D;^=5M5Nw5Mh)ZAY6G z1P%Fty{;A(hI!=cp+J61HQvj)t}Mu{hS!5903vHtK4P!cJE0pJ1WK|x_8*Ok)0}aW z?=y#a3}-LOigFx%@&>nx1dj4_JQTnru4rU0sQI6^8^z`9D8cjtvT*)KBytN-$q<0D zin;~Jv)LF$zi-)9#CY@YY(6Pw@`-}|-X#L2_BS&2^YvEqS%8s_mJQ7M$>@h)ai_P* zKL4O0oBNiV3jbGGi2p^#{XbrE2Q1bw}VUgR&8OZPixZuF2Wh~+?@FRHI z>@y$}{tNKkjEAGTx9ut>ywe3lPvwQ)8+C#O-$Xfss*c zEKmw<-)pPX)j;(oRJu3Msqph0BU5uVW%T3zGu97a8&R_qjMj1Uv`c48j-mGl%DHz2 zMz5NZKZM-~)k!t;Gou;57zu6XGE82v=V>yDyO+_3vZr?nSpMInd7C2u<8#5|(s-W9 z_C!w^iYrm1=q8-ujadaAA;D zDYbu9)xXFrG(|#4^|?|wMfrWBTvoN|I%}{5F4N&A@x{a8wu0;pmpi1V=-`j74j&Yb zVK&O7hI7-=2<}`({UjVWy~@Pp*bi2d?D^~R5o1x0cVEvv`Wab45<492;iL7VyI$+!9YTgaYeN@^9VswLJQIg_ zHL(NG3J#RbPvtS|L-$K$w?3lx`!z-ZdS?6X9%t3GngVRxJ|(0~I=WAPR>jEsP3I|> zLBiHb3lH@*MX!2~qxYuf105GEoXvL} znlwi>g-7@-2M6g-;GFk5IU5g>Sf>L?|MLXkNxMgE z>eC7F{yO~N3T#JM!v?gxKnjk2fd&*JJ9sVj$*%-&6XuW;%>~-n6! z67Z0bs`ozu@l$l9CSw5UG0>B?gS`)9X45$Uw1IQgD-BIMEC%8S)CCM@lhTDvvk(+l zd;I_2iJ9tRCwD6W?l1i6;KGs$!$f+XOq+oi2YqzS*4bkPQxNV}KV-BxUy!)hYJbZru^7`0O{tcQP-!N5~Pg1!{_q!uT^A(2-=yzasuJvKNcT!bpC_6Q|PDh4jDXI;8UQ0UdqB=kJuV zyLoweOWzx|RwmA_+I{%LSDxdLGl|*tbUe8CXOPCSig(W2=^!s#@7ecec$(gV$ zsV*RL&Gz9VoDj7v&^=Ufqa?s9KEOr)$j$W2UI$~U1W*_76(qb1I@$(|y(g)fe1Nc^ zA^$NGO%2`nopl<)LRcY-Vq?HXp8Gcb`TZ@xjT1bJ4dFRxmTH9H?XHRDXz?mDQL*iY zu-O;Du(=a21|0v&Dtg@sU*hwdFA^2{V~@+xKXemJO0B9Y(gAMVL!-=NX=T2H{pTX) zzvQo+d0(rf?Q<)1Gqlun#KW=lKZ5^lGZGSnn5bd{!0SrwD@u*`v!aNOn%DX<7ro4v zwMhEt^Xti!UamX~Z^q866BAIaF2s_Och{T!-64 zAxx|HPMS1EbMk>vx={ZBI140y7U>R=rKm95PSwhf#5 Date: Tue, 24 May 2022 19:09:47 +0300 Subject: [PATCH 30/52] Add recovery_key_cubit.dart --- lib/logic/api_maps/server.dart | 20 ++++-- .../recovery_key/recovery_key_cubit.dart | 72 +++++++++++++++++++ .../recovery_key/recovery_key_state.dart | 37 ++++++++++ .../models/json/recovery_token_status.dart | 23 ++++-- .../models/json/recovery_token_status.g.dart | 6 +- 5 files changed, 142 insertions(+), 16 deletions(-) create mode 100644 lib/logic/cubit/recovery_key/recovery_key_cubit.dart create mode 100644 lib/logic/cubit/recovery_key/recovery_key_state.dart diff --git a/lib/logic/api_maps/server.dart b/lib/logic/api_maps/server.dart index 9320af9d..db8230bd 100644 --- a/lib/logic/api_maps/server.dart +++ b/lib/logic/api_maps/server.dart @@ -628,7 +628,7 @@ class ServerApi extends ApiMap { .replaceAll('"', ''); } - Future> getRecoveryTokenStatus() async { + Future> getRecoveryTokenStatus() async { Response response; var client = await getClient(); @@ -639,7 +639,7 @@ class ServerApi extends ApiMap { return ApiResponse( errorMessage: e.message, statusCode: e.response?.statusCode ?? HttpStatus.internalServerError, - data: RecoveryTokenStatus(exists: false, valid: false)); + data: RecoveryKeyStatus(exists: false, valid: false)); } finally { close(client); } @@ -654,17 +654,23 @@ class ServerApi extends ApiMap { } Future> generateRecoveryToken( - DateTime expiration, int uses) async { + DateTime? expiration, + int? uses, + ) async { Response response; var client = await getClient(); + var data = {}; + if (expiration != null) { + data['expiration'] = expiration.toIso8601String(); + } + if (uses != null) { + data['uses'] = uses; + } try { response = await client.post( '/auth/recovery_token', - data: { - 'expiration': expiration.toIso8601String(), - 'uses': uses, - }, + data: data, ); } on DioError catch (e) { print(e.message); diff --git a/lib/logic/cubit/recovery_key/recovery_key_cubit.dart b/lib/logic/cubit/recovery_key/recovery_key_cubit.dart new file mode 100644 index 00000000..377bd010 --- /dev/null +++ b/lib/logic/cubit/recovery_key/recovery_key_cubit.dart @@ -0,0 +1,72 @@ +import 'package:selfprivacy/logic/api_maps/server.dart'; +import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart'; +import 'package:selfprivacy/logic/models/json/recovery_token_status.dart'; + +part 'recovery_key_state.dart'; + +class RecoveryKeyCubit + extends ServerInstallationDependendCubit { + RecoveryKeyCubit(ServerInstallationCubit serverInstallationCubit) + : super(serverInstallationCubit, RecoveryKeyState.initial()); + + final api = ServerApi(); + + @override + void load() async { + if (serverInstallationCubit.state is ServerInstallationFinished) { + final status = await _getRecoveryKeyStatus(); + if (status == null) { + emit(state.copyWith(loadingStatus: LoadingStatus.error)); + } else { + emit(state.copyWith(status: status, loadingStatus: LoadingStatus.good)); + } + } else { + emit(state.copyWith(loadingStatus: LoadingStatus.uninitialized)); + } + } + + Future _getRecoveryKeyStatus() async { + final ApiResponse response = + await api.getRecoveryTokenStatus(); + if (response.isSuccess) { + return response.data; + } else { + return null; + } + } + + Future refresh() async { + emit(state.copyWith(loadingStatus: LoadingStatus.refreshing)); + final status = await _getRecoveryKeyStatus(); + if (status == null) { + emit(state.copyWith(loadingStatus: LoadingStatus.error)); + } else { + emit(state.copyWith(status: status, loadingStatus: LoadingStatus.good)); + } + } + + Future generateRecoveryKey({ + DateTime? expirationDate, + int? numberOfUses, + }) async { + final ApiResponse response = + await api.generateRecoveryToken(expirationDate, numberOfUses); + if (response.isSuccess) { + refresh(); + return response.data; + } else { + throw GenerationError(response.errorMessage ?? 'Unknown error'); + } + } + + @override + void clear() { + emit(state.copyWith(loadingStatus: LoadingStatus.uninitialized)); + } +} + +class GenerationError extends Error { + final String message; + + GenerationError(this.message); +} diff --git a/lib/logic/cubit/recovery_key/recovery_key_state.dart b/lib/logic/cubit/recovery_key/recovery_key_state.dart new file mode 100644 index 00000000..60e86ecf --- /dev/null +++ b/lib/logic/cubit/recovery_key/recovery_key_state.dart @@ -0,0 +1,37 @@ +part of 'recovery_key_cubit.dart'; + +enum LoadingStatus { + uninitialized, + refreshing, + good, + error, +} + + +class RecoveryKeyState extends ServerInstallationDependendState { + const RecoveryKeyState(this._status, this.loadingStatus); + + RecoveryKeyState.initial() + : this(RecoveryKeyStatus(exists: false, valid: false), LoadingStatus.refreshing); + + final RecoveryKeyStatus _status; + final LoadingStatus loadingStatus; + + bool get exists => _status.exists; + bool get isValid => _status.valid; + DateTime? get generatedAt => _status.date; + DateTime? get expiresAt => _status.date; + int? get usesLeft => _status.usesLeft; + @override + List get props => [_status, loadingStatus]; + + RecoveryKeyState copyWith({ + RecoveryKeyStatus? status, + LoadingStatus? loadingStatus, + }) { + return RecoveryKeyState( + status ?? this._status, + loadingStatus ?? this.loadingStatus, + ); + } +} diff --git a/lib/logic/models/json/recovery_token_status.dart b/lib/logic/models/json/recovery_token_status.dart index 9c94b41a..8904f84f 100644 --- a/lib/logic/models/json/recovery_token_status.dart +++ b/lib/logic/models/json/recovery_token_status.dart @@ -1,23 +1,34 @@ +import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; part 'recovery_token_status.g.dart'; @JsonSerializable() -class RecoveryTokenStatus { - RecoveryTokenStatus({ +class RecoveryKeyStatus extends Equatable { + RecoveryKeyStatus({ required this.exists, required this.valid, this.date, this.expiration, - this.uses_left, + this.usesLeft, }); final bool exists; final DateTime? date; final DateTime? expiration; - final int? uses_left; + @JsonKey(name: 'uses_left') + final int? usesLeft; final bool valid; - factory RecoveryTokenStatus.fromJson(Map json) => - _$RecoveryTokenStatusFromJson(json); + factory RecoveryKeyStatus.fromJson(Map json) => + _$RecoveryKeyStatusFromJson(json); + + @override + List get props => [ + exists, + date, + expiration, + usesLeft, + valid, + ]; } diff --git a/lib/logic/models/json/recovery_token_status.g.dart b/lib/logic/models/json/recovery_token_status.g.dart index 3f213364..cef9abbd 100644 --- a/lib/logic/models/json/recovery_token_status.g.dart +++ b/lib/logic/models/json/recovery_token_status.g.dart @@ -6,8 +6,8 @@ part of 'recovery_token_status.dart'; // JsonSerializableGenerator // ************************************************************************** -RecoveryTokenStatus _$RecoveryTokenStatusFromJson(Map json) => - RecoveryTokenStatus( +RecoveryKeyStatus _$RecoveryKeyStatusFromJson(Map json) => + RecoveryKeyStatus( exists: json['exists'] as bool, valid: json['valid'] as bool, date: @@ -15,5 +15,5 @@ RecoveryTokenStatus _$RecoveryTokenStatusFromJson(Map json) => expiration: json['expiration'] == null ? null : DateTime.parse(json['expiration'] as String), - uses_left: json['uses_left'] as int?, + usesLeft: json['uses_left'] as int?, ); From edce25ec55c732d4a697774ad9cec7c49bb231ed Mon Sep 17 00:00:00 2001 From: NaiJi Date: Tue, 24 May 2022 20:45:13 +0300 Subject: [PATCH 31/52] Hot bug fixing of recovery flow Co-authored-by: Inex Code --- analysis_options.yaml | 2 +- assets/translations/en.json | 21 ++++- assets/translations/ru.json | 14 +++- lib/config/bloc_config.dart | 3 + lib/logic/api_maps/server.dart | 4 +- .../initializing/backblaze_form_cubit.dart | 1 + .../recovery_key/recovery_key_cubit.dart | 2 +- .../server_installation_cubit.dart | 11 +++ lib/main.dart | 3 +- lib/ui/pages/recovery_key/recovery_key.dart | 14 ++++ .../recovery_key/recovery_key_receiving.dart | 1 + .../recovering/recover_by_new_device_key.dart | 64 +++++++++------- .../recovering/recover_by_old_token.dart | 47 +++++++----- .../recovery_confirm_backblaze.dart | 8 +- .../recovery_confirm_cloudflare.dart | 5 +- .../recovering/recovery_confirm_server.dart | 76 +++++++++++++++---- .../setup/recovering/recovery_routing.dart | 4 + pubspec.lock | 14 ++++ pubspec.yaml | 1 + 19 files changed, 220 insertions(+), 75 deletions(-) create mode 100644 lib/ui/pages/recovery_key/recovery_key.dart create mode 100644 lib/ui/pages/recovery_key/recovery_key_receiving.dart diff --git a/analysis_options.yaml b/analysis_options.yaml index 61b6c4de..1caa1fad 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -23,7 +23,7 @@ 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 + prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule # Additional information about this file can be found at # https://dart.dev/guides/language/analysis-options diff --git a/assets/translations/en.json b/assets/translations/en.json index ca3c1984..7cbfdd4f 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -313,11 +313,25 @@ "choose_server_description": "We couldn't figure out which server your are trying to connect to.", "no_servers": "There is no available servers on your account.", "domain_not_available_on_token": "Selected domain is not available on this token.", + "modal_confirmation_title": "Is it really your server?", + "modal_confirmation_description": "If you connect to a wrong server you may lose all your data.", + "modal_confirmation_dns_valid": "Reverse DNS is valid", + "modal_confirmation_dns_invalid": "Reverse DNS points to another domain", + "modal_confirmation_ip_valid": "IP is the same as in DNS record", + "modal_confirmation_ip_invalid": "IP is not the same as in DNS record", "confirm_cloudflare": "Connect to CloudFlare", "confirm_cloudflare_description": "Enter a Cloudflare token with access to {}:", - "confirm_backblze": "Connect to Backblaze", + "confirm_backblaze": "Connect to Backblaze", "confirm_backblaze_description": "Enter a Backblaze token with access to backup storage:" - + }, + "recovery_key": { + "key_main_header": "Recovery key", + "key_main_description": "Is needed for SelfPrivacy authorization when all your other authorized devices aren't available.", + "key_amount_toggle": "Limit by number of uses", + "key_amount_field_title": "Max number of uses", + "key_duedate_toggle": "Limit by time", + "key_duedate_field_title": "Due date of expiration", + "key_receive_button": "Receive key" }, "modals": { "_comment": "messages in modals", @@ -331,7 +345,8 @@ "8": "Remove task", "9": "Reboot", "10": "You cannot use this API for domains with such TLD.", - "yes": "Yes" + "yes": "Yes", + "no": "No" }, "timer": { "sec": "{} sec" diff --git a/assets/translations/ru.json b/assets/translations/ru.json index d240c6e3..7b060a49 100644 --- a/assets/translations/ru.json +++ b/assets/translations/ru.json @@ -315,11 +315,22 @@ "choose_server_description": "Не удалось определить, с каким сервером вы устанавливаете связь.", "no_servers": "На вашем аккаунте нет доступных серверов.", "domain_not_available_on_token": "Введённый токен не имеет доступа к нужному домену.", + "modal_confirmation_title": "Это действительно ваш сервер?", + "modal_confirmation_description": "Подключение к неправильному серверу может привести к деструктивным последствиям.", "confirm_cloudflare": "Подключение к Cloudflare", "confirm_cloudflare_description": "Введите токен Cloudflare, который имеет права на {}:", "confirm_backblze": "Подключение к Backblaze", "confirm_backblaze_description": "Введите токен Backblaze, который имеет права на хранилище резервных копий:" }, + "recovery_key": { + "key_main_header": "Ключ восстановления", + "key_main_description": "Требуется для авторизации SelfPrivacy, когда авторизованные устройства недоступны.", + "key_amount_toggle": "Ограничить использования", + "key_amount_field_title": "Макс. кол-во использований", + "key_duedate_toggle": "Ограничить срок использования", + "key_duedate_field_title": "Дата окончания срока", + "key_receive_button": "Получить ключ" + }, "modals": { "_comment": "messages in modals", "1": "Сервер с таким именем уже существует", @@ -332,7 +343,8 @@ "8": "Удалить задачу", "9": "Перезагрузить", "10": "API не поддерживает домены с таким TLD.", - "yes": "Да" + "yes": "Да", + "no": "Нет" }, "timer": { "sec": "{} сек" diff --git a/lib/config/bloc_config.dart b/lib/config/bloc_config.dart index 3456b2de..222b70bf 100644 --- a/lib/config/bloc_config.dart +++ b/lib/config/bloc_config.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.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/logic/cubit/app_settings/app_settings_cubit.dart'; import 'package:selfprivacy/logic/cubit/backups/backups_cubit.dart'; @@ -22,6 +23,7 @@ class BlocAndProviderConfig extends StatelessWidget { var servicesCubit = ServicesCubit(serverInstallationCubit); var backupsCubit = BackupsCubit(serverInstallationCubit); var dnsRecordsCubit = DnsRecordsCubit(serverInstallationCubit); + var recoveryKeyCubit = RecoveryKeyCubit(serverInstallationCubit); return MultiProvider( providers: [ BlocProvider( @@ -36,6 +38,7 @@ class BlocAndProviderConfig extends StatelessWidget { BlocProvider(create: (_) => servicesCubit..load(), lazy: false), BlocProvider(create: (_) => backupsCubit..load(), lazy: false), BlocProvider(create: (_) => dnsRecordsCubit..load()), + BlocProvider(create: (_) => recoveryKeyCubit..load()), BlocProvider( create: (_) => JobsCubit(usersCubit: usersCubit, servicesCubit: servicesCubit), diff --git a/lib/logic/api_maps/server.dart b/lib/logic/api_maps/server.dart index db8230bd..d29dffa9 100644 --- a/lib/logic/api_maps/server.dart +++ b/lib/logic/api_maps/server.dart @@ -628,7 +628,7 @@ class ServerApi extends ApiMap { .replaceAll('"', ''); } - Future> getRecoveryTokenStatus() async { + Future> getRecoveryTokenStatus() async { Response response; var client = await getClient(); @@ -649,7 +649,7 @@ class ServerApi extends ApiMap { return ApiResponse( statusCode: code, data: response.data != null - ? response.data.fromJson(response.data) + ? RecoveryKeyStatus.fromJson(response.data) : null); } diff --git a/lib/logic/cubit/forms/setup/initializing/backblaze_form_cubit.dart b/lib/logic/cubit/forms/setup/initializing/backblaze_form_cubit.dart index fc9062e7..9958effa 100644 --- a/lib/logic/cubit/forms/setup/initializing/backblaze_form_cubit.dart +++ b/lib/logic/cubit/forms/setup/initializing/backblaze_form_cubit.dart @@ -51,6 +51,7 @@ class BackblazeFormCubit extends FormCubit { isKeyValid = await apiClient.isValid(encodedApiKey); } catch (e) { addError(e); + isKeyValid = false; } if (!isKeyValid) { diff --git a/lib/logic/cubit/recovery_key/recovery_key_cubit.dart b/lib/logic/cubit/recovery_key/recovery_key_cubit.dart index 377bd010..eec3bfb1 100644 --- a/lib/logic/cubit/recovery_key/recovery_key_cubit.dart +++ b/lib/logic/cubit/recovery_key/recovery_key_cubit.dart @@ -26,7 +26,7 @@ class RecoveryKeyCubit } Future _getRecoveryKeyStatus() async { - final ApiResponse response = + final ApiResponse response = await api.getRecoveryTokenStatus(); if (response.isSuccess) { return response.data; diff --git a/lib/logic/cubit/server_installation/server_installation_cubit.dart b/lib/logic/cubit/server_installation/server_installation_cubit.dart index 13b84610..579269e7 100644 --- a/lib/logic/cubit/server_installation/server_installation_cubit.dart +++ b/lib/logic/cubit/server_installation/server_installation_cubit.dart @@ -64,6 +64,10 @@ class ServerInstallationCubit extends Cubit { } void setCloudflareKey(String cloudFlareKey) async { + if (state is ServerInstallationRecovery) { + setAndValidateCloudflareToken(cloudFlareKey); + return; + } await repository.saveCloudFlareKey(cloudFlareKey); emit((state as ServerInstallationNotFinished) .copyWith(cloudFlareKey: cloudFlareKey)); @@ -431,12 +435,19 @@ class ServerInstallationCubit extends Cubit { .showSnackBar('recovering.domain_not_available_on_token'.tr()); return; } + await repository.saveDomain(ServerDomain( + domainName: serverDomain.domainName, + zoneId: zoneId, + provider: DnsProvider.Cloudflare, + )); + await repository.saveCloudFlareKey(token); emit(dataState.copyWith( serverDomain: ServerDomain( domainName: serverDomain.domainName, zoneId: zoneId, provider: DnsProvider.Cloudflare, ), + cloudFlareKey: token, currentStep: RecoveryStep.BackblazeToken, )); } diff --git a/lib/main.dart b/lib/main.dart index cdb817fc..1889f65a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -88,8 +88,9 @@ class MyApp extends StatelessWidget { : RootPage(), builder: (BuildContext context, Widget? widget) { Widget error = Text('...rendering error...'); - if (widget is Scaffold || widget is Navigator) + if (widget is Scaffold || widget is Navigator) { error = Scaffold(body: Center(child: error)); + } ErrorWidget.builder = (FlutterErrorDetails errorDetails) => error; return widget!; diff --git a/lib/ui/pages/recovery_key/recovery_key.dart b/lib/ui/pages/recovery_key/recovery_key.dart new file mode 100644 index 00000000..bf0fdd6d --- /dev/null +++ b/lib/ui/pages/recovery_key/recovery_key.dart @@ -0,0 +1,14 @@ +/*import 'package:flutter/src/foundation/key.dart'; +import 'package:flutter/src/widgets/framework.dart'; + +class RecoveryKey extends StatefulWidget { + const RecoveryKey({Key? key}) : super(key: key); + + @override + State createState() => _RecoveryKeyState(); +} + +class _RecoveryKeyState extends State { + @override + Widget build(BuildContext context) {} +}*/ diff --git a/lib/ui/pages/recovery_key/recovery_key_receiving.dart b/lib/ui/pages/recovery_key/recovery_key_receiving.dart new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/lib/ui/pages/recovery_key/recovery_key_receiving.dart @@ -0,0 +1 @@ + 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 e93b4c4f..de5112e2 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 @@ -40,34 +40,44 @@ class RecoverByNewDeviceKeyInput extends StatelessWidget { FieldCubitFactory(context), ServerRecoveryMethods.newDeviceKey, ), - child: Builder( - builder: (context) { - var formCubitState = context.watch().state; - - return BrandHeroScreen( - heroTitle: "recovering.recovery_main_header".tr(), - heroSubtitle: "recovering.method_device_input_description".tr(), - hasBackButton: true, - hasFlashButton: false, - children: [ - CubitFormTextField( - formFieldCubit: - context.read().tokenField, - decoration: InputDecoration( - border: OutlineInputBorder(), - labelText: "recovering.method_device_input_placeholder".tr(), - ), - ), - SizedBox(height: 16), - FilledButton( - title: "more.continue".tr(), - onPressed: formCubitState.isSubmitting - ? null - : () => context.read().trySubmit(), - ) - ], - ); + child: BlocListener( + listener: (context, state) { + if (state is ServerInstallationRecovery && + state.currentStep != RecoveryStep.NewDeviceKey) { + Navigator.of(context).pop(); + } }, + child: Builder( + builder: (context) { + var formCubitState = context.watch().state; + + return BrandHeroScreen( + heroTitle: "recovering.recovery_main_header".tr(), + heroSubtitle: "recovering.method_device_input_description".tr(), + hasBackButton: true, + hasFlashButton: false, + children: [ + CubitFormTextField( + formFieldCubit: + context.read().tokenField, + decoration: InputDecoration( + border: OutlineInputBorder(), + labelText: + "recovering.method_device_input_placeholder".tr(), + ), + ), + SizedBox(height: 16), + FilledButton( + title: "more.continue".tr(), + onPressed: formCubitState.isSubmitting + ? null + : () => + context.read().trySubmit(), + ) + ], + ); + }, + ), ), ); } 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 4d36262b..363519c9 100644 --- a/lib/ui/pages/setup/recovering/recover_by_old_token.dart +++ b/lib/ui/pages/setup/recovering/recover_by_old_token.dart @@ -13,24 +13,33 @@ class RecoverByOldTokenInstruction extends StatelessWidget { RecoverByOldTokenInstruction({required this.instructionFilename}); Widget build(BuildContext context) { - return BrandHeroScreen( - heroTitle: "recovering.recovery_main_header".tr(), - hasBackButton: true, - hasFlashButton: false, - onBackButtonPressed: () => - context.read().revertRecoveryStep(), - children: [ - BrandMarkdown( - fileName: instructionFilename, - ), - SizedBox(height: 16), - FilledButton( - title: "recovering.method_device_button".tr(), - onPressed: () => context - .read() - .selectRecoveryMethod(ServerRecoveryMethods.oldToken), - ) - ], + return BlocListener( + listener: (context, state) { + if (state is ServerInstallationRecovery && + state.currentStep != RecoveryStep.Selecting) { + Navigator.of(context).pop(); + Navigator.of(context).pop(); + } + }, + child: BrandHeroScreen( + heroTitle: "recovering.recovery_main_header".tr(), + hasBackButton: true, + hasFlashButton: false, + onBackButtonPressed: () => + context.read().revertRecoveryStep(), + children: [ + BrandMarkdown( + fileName: instructionFilename, + ), + SizedBox(height: 18), + FilledButton( + title: "recovering.method_device_button".tr(), + onPressed: () => context + .read() + .selectRecoveryMethod(ServerRecoveryMethods.oldToken), + ) + ], + ), ); } @@ -66,7 +75,7 @@ class RecoverByOldToken extends StatelessWidget { labelText: "recovering.method_device_input_placeholder".tr(), ), ), - SizedBox(height: 16), + SizedBox(height: 18), FilledButton( title: "more.continue".tr(), onPressed: formCubitState.isSubmitting diff --git a/lib/ui/pages/setup/recovering/recovery_confirm_backblaze.dart b/lib/ui/pages/setup/recovering/recovery_confirm_backblaze.dart index 521a1860..6c1779e1 100644 --- a/lib/ui/pages/setup/recovering/recovery_confirm_backblaze.dart +++ b/lib/ui/pages/setup/recovering/recovery_confirm_backblaze.dart @@ -30,26 +30,28 @@ class RecoveryConfirmBackblaze extends StatelessWidget { textAlign: TextAlign.center, scrollPadding: EdgeInsets.only(bottom: 70), decoration: InputDecoration( + border: const OutlineInputBorder(), hintText: 'KeyID', ), ), - Spacer(), + const SizedBox(height: 18), CubitFormTextField( formFieldCubit: context.read().applicationKey, textAlign: TextAlign.center, scrollPadding: EdgeInsets.only(bottom: 70), decoration: InputDecoration( + border: const OutlineInputBorder(), hintText: 'Master Application Key', ), ), - Spacer(), + const SizedBox(height: 18), BrandButton.rised( onPressed: formCubitState.isSubmitting ? null : () => context.read().trySubmit(), text: 'basis.connect'.tr(), ), - SizedBox(height: 10), + const SizedBox(height: 18), BrandButton.text( onPressed: () => showModalBottomSheet( context: context, diff --git a/lib/ui/pages/setup/recovering/recovery_confirm_cloudflare.dart b/lib/ui/pages/setup/recovering/recovery_confirm_cloudflare.dart index c77948c7..a0966f4f 100644 --- a/lib/ui/pages/setup/recovering/recovery_confirm_cloudflare.dart +++ b/lib/ui/pages/setup/recovering/recovery_confirm_cloudflare.dart @@ -32,17 +32,18 @@ class RecoveryConfirmCloudflare extends StatelessWidget { textAlign: TextAlign.center, scrollPadding: EdgeInsets.only(bottom: 70), decoration: InputDecoration( + border: const OutlineInputBorder(), hintText: 'initializing.5'.tr(), ), ), - Spacer(), + const SizedBox(height: 18), BrandButton.rised( onPressed: formCubitState.isSubmitting ? null : () => context.read().trySubmit(), text: 'basis.connect'.tr(), ), - SizedBox(height: 10), + const SizedBox(height: 18), BrandButton.text( onPressed: () => showModalBottomSheet( context: context, diff --git a/lib/ui/pages/setup/recovering/recovery_confirm_server.dart b/lib/ui/pages/setup/recovering/recovery_confirm_server.dart index b83f00f7..8a5c45c3 100644 --- a/lib/ui/pages/setup/recovering/recovery_confirm_server.dart +++ b/lib/ui/pages/setup/recovering/recovery_confirm_server.dart @@ -36,11 +36,11 @@ class _RecoveryConfirmServerState extends State { Widget build(BuildContext context) { return BrandHeroScreen( heroTitle: _isExtended - ? "recovering.choose_server".tr() - : "recovering.confirm_server".tr(), + ? 'recovering.choose_server'.tr() + : 'recovering.confirm_server'.tr(), heroSubtitle: _isExtended - ? "recovering.choose_server_description".tr() - : "recovering.confirm_server_description".tr(), + ? 'recovering.choose_server_description'.tr() + : 'recovering.confirm_server_description'.tr(), hasBackButton: true, hasFlashButton: false, children: [ @@ -68,7 +68,7 @@ class _RecoveryConfirmServerState extends State { if (servers?.isEmpty ?? true) Center( child: Text( - "recovering.no_servers".tr(), + 'recovering.no_servers'.tr(), style: Theme.of(context).textTheme.headline6, ), ), @@ -99,7 +99,7 @@ class _RecoveryConfirmServerState extends State { ), SizedBox(height: 16), FilledButton( - title: "recovering.confirm_server_accept".tr(), + title: 'recovering.confirm_server_accept'.tr(), onPressed: () => _showConfirmationDialog(context, server), ), SizedBox(height: 16), @@ -166,19 +166,65 @@ class _RecoveryConfirmServerState extends State { context: context, builder: (context) { return AlertDialog( - title: Text('ssh.delete'.tr()), - content: SingleChildScrollView( - child: ListBody( - children: [ - Text("WOW DIALOGUE TEXT WOW :)"), - ], - ), + title: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Icon(Icons.warning_amber_outlined), + const SizedBox(height: 8), + Text( + 'recovering.modal_confirmation_title'.tr(), + style: Theme.of(context).textTheme.headlineSmall, + ), + ], + ), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('recovering.modal_confirmation_description'.tr(), + style: Theme.of(context).textTheme.bodyMedium), + const Divider(), + Text( + server.name, + style: Theme.of(context).textTheme.titleMedium, + textAlign: TextAlign.start, + ), + const SizedBox(height: 4), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Icon(server.isReverseDnsValid ? Icons.check : Icons.close), + const SizedBox(width: 8), + Text(server.isReverseDnsValid + ? 'recovering.modal_confirmation_dns_valid'.tr() + : 'recovering.modal_confirmation_dns_invalid'.tr()), + ], + ), + const SizedBox(height: 4), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Icon(server.isIpValid ? Icons.check : Icons.close), + const SizedBox(width: 8), + Text(server.isIpValid + ? 'recovering.modal_confirmation_ip_valid'.tr() + : 'recovering.modal_confirmation_ip_invalid'.tr()), + ], + ), + ], ), actions: [ TextButton( - child: Text('basis.cancel'.tr()), + child: Text('modals.no'.tr()), onPressed: () { - Navigator.of(context)..pop(); + Navigator.of(context).pop(); + }, + ), + TextButton( + child: Text('modals.yes'.tr()), + onPressed: () { + context.read().setServerId(server); + Navigator.of(context).pop(); }, ), ], diff --git a/lib/ui/pages/setup/recovering/recovery_routing.dart b/lib/ui/pages/setup/recovering/recovery_routing.dart index d3a8a0b9..743e4aab 100644 --- a/lib/ui/pages/setup/recovering/recovery_routing.dart +++ b/lib/ui/pages/setup/recovering/recovery_routing.dart @@ -9,6 +9,8 @@ import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.da import 'package:selfprivacy/ui/pages/setup/recovering/recover_by_old_token.dart'; import 'package:selfprivacy/ui/pages/setup/recovering/recover_by_recovery_key.dart'; import 'package:selfprivacy/ui/pages/setup/recovering/recover_by_new_device_key.dart'; +import 'package:selfprivacy/ui/pages/setup/recovering/recovery_confirm_backblaze.dart'; +import 'package:selfprivacy/ui/pages/setup/recovering/recovery_confirm_cloudflare.dart'; import 'package:selfprivacy/ui/pages/setup/recovering/recovery_confirm_server.dart'; import 'package:selfprivacy/ui/pages/setup/recovering/recovery_hentzner_connected.dart'; import 'package:selfprivacy/ui/pages/setup/recovering/recovery_method_select.dart'; @@ -43,8 +45,10 @@ class RecoveryRouting extends StatelessWidget { currentPage = RecoveryConfirmServer(); break; case RecoveryStep.CloudflareToken: + currentPage = RecoveryConfirmCloudflare(); break; case RecoveryStep.BackblazeToken: + currentPage = RecoveryConfirmBackblaze(); break; } } diff --git a/pubspec.lock b/pubspec.lock index 965eb666..de1631fe 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -363,6 +363,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.9.2" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" flutter_localizations: dependency: transitive description: flutter @@ -560,6 +567,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "6.2.0" + lints: + dependency: transitive + description: + name: lints + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" local_auth: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index f8b02e64..a99c36a0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -52,6 +52,7 @@ dev_dependencies: flutter_launcher_icons: ^0.9.2 hive_generator: ^1.0.0 json_serializable: ^6.1.4 + flutter_lints: ^2.0.1 flutter_icons: android: "launcher_icon" From 14acfdec6b2a8d13fc236de1b924c06ab21c12b2 Mon Sep 17 00:00:00 2001 From: Inex Code Date: Tue, 24 May 2022 21:55:39 +0300 Subject: [PATCH 32/52] Linting --- analysis_options.yaml | 2 +- lib/config/bloc_observer.dart | 4 +- lib/config/brand_colors.dart | 2 +- lib/config/brand_theme.dart | 20 +- lib/config/hive_config.dart | 2 +- lib/config/localization.dart | 4 +- lib/config/text_themes.dart | 6 +- lib/logic/api_maps/api_map.dart | 8 +- lib/logic/api_maps/backblaze.dart | 7 +- lib/logic/api_maps/cloudflare.dart | 3 +- lib/logic/api_maps/hetzner.dart | 53 +- lib/logic/api_maps/server.dart | 61 +- lib/logic/common_enum/common_enum.dart | 2 +- .../authentication_dependend_cubit.dart | 2 +- .../app_settings/app_settings_cubit.dart | 2 +- lib/logic/cubit/backups/backups_cubit.dart | 28 +- lib/logic/cubit/backups/backups_state.dart | 2 +- .../cubit/dns_records/dns_records_cubit.dart | 12 +- .../cubit/dns_records/dns_records_state.dart | 2 +- .../forms/factories/field_cubit_factory.dart | 4 +- .../initializing/cloudflare_form_cubit.dart | 7 +- .../setup/initializing/domain_cloudflare.dart | 2 +- .../initializing/hetzner_form_cubit.dart | 2 +- .../recovery_domain_form_cubit.dart | 2 +- .../cubit/forms/user/ssh_form_cubit.dart | 2 +- .../cubit/forms/validations/validations.dart | 2 +- .../hetzner_metrics_cubit.dart | 5 +- .../hetzner_metrics_repository.dart | 14 +- .../hetzner_metrics_state.dart | 6 +- .../cubit/providers/providers_cubit.dart | 2 +- .../recovery_key/recovery_key_cubit.dart | 2 +- .../recovery_key/recovery_key_state.dart | 8 +- .../server_detailed_info_cubit.dart | 2 +- .../server_detailed_info_repository.dart | 8 +- .../server_detailed_info_state.dart | 2 +- .../server_installation_cubit.dart | 96 +- .../server_installation_repository.dart | 28 +- .../server_installation_state.dart | 24 +- lib/logic/cubit/services/services_cubit.dart | 2 +- lib/logic/cubit/services/services_state.dart | 4 +- lib/logic/cubit/users/users_cubit.dart | 9 +- lib/logic/get_it/api_config.dart | 2 +- lib/logic/get_it/console.dart | 2 +- lib/logic/models/hive/backblaze_bucket.dart | 2 +- .../models/hive/backblaze_credential.dart | 4 +- lib/logic/models/hive/server_details.dart | 7 +- lib/logic/models/hive/server_details.g.dart | 12 +- lib/logic/models/hive/server_domain.dart | 6 +- lib/logic/models/hive/server_domain.g.dart | 12 +- lib/logic/models/hive/user.dart | 5 +- lib/logic/models/job.dart | 4 +- lib/logic/models/json/api_token.dart | 5 +- lib/logic/models/json/api_token.g.dart | 2 +- .../models/json/auto_upgrade_settings.dart | 2 +- .../models/json/recovery_token_status.dart | 2 +- lib/logic/models/message.dart | 2 +- lib/main.dart | 4 +- lib/theming/factory/app_theme_factory.dart | 6 +- .../action_button/action_button.dart | 2 +- .../brand_bottom_sheet.dart | 4 +- .../components/brand_button/brand_button.dart | 4 +- .../{FilledButton.dart => filled_button.dart} | 2 +- .../components/brand_cards/brand_cards.dart | 16 +- .../components/brand_header/brand_header.dart | 30 +- .../brand_hero_screen/brand_hero_screen.dart | 12 +- .../components/brand_icons/brand_icons.dart | 2 +- .../components/brand_loader/brand_loader.dart | 4 +- .../components/brand_radio/brand_radio.dart | 6 +- .../brand_radio_tile/brand_radio_tile.dart | 4 +- .../brand_span_button/brand_span_button.dart | 2 +- .../brand_tab_bar/brand_tab_bar.dart | 2 +- .../components/brand_timer/brand_timer.dart | 8 +- .../dots_indicator/dots_indicator.dart | 2 +- lib/ui/components/error/error.dart | 2 +- .../icon_status_mask/icon_status_mask.dart | 6 +- .../components/jobs_content/jobs_content.dart | 16 +- .../not_ready_card/not_ready_card.dart | 6 +- lib/ui/components/one_page/one_page.dart | 6 +- .../components/pre_styled_buttons/close.dart | 2 +- .../components/pre_styled_buttons/flash.dart | 8 +- .../{flashFab.dart => flash_fab.dart} | 8 +- .../pre_styled_buttons.dart | 2 +- .../components/progress_bar/progress_bar.dart | 26 +- .../components/switch_block/switch_bloc.dart | 6 +- lib/ui/helpers/modals.dart | 2 +- .../pages/backup_details/backup_details.dart | 24 +- lib/ui/pages/dns_details/dns_details.dart | 18 +- lib/ui/pages/more/about/about.dart | 8 +- .../pages/more/app_settings/app_setting.dart | 45 +- lib/ui/pages/more/console/console.dart | 8 +- lib/ui/pages/more/info/info.dart | 6 +- lib/ui/pages/more/more.dart | 20 +- lib/ui/pages/onboarding/onboarding.dart | 16 +- lib/ui/pages/providers/providers.dart | 20 +- .../pages/{rootRoute.dart => root_route.dart} | 6 +- lib/ui/pages/server_details/chart.dart | 36 +- lib/ui/pages/server_details/cpu_chart.dart | 8 +- lib/ui/pages/server_details/header.dart | 12 +- .../pages/server_details/network_charts.dart | 8 +- .../server_details/server_details_screen.dart | 18 +- .../pages/server_details/server_settings.dart | 42 +- lib/ui/pages/server_details/text_details.dart | 22 +- .../pages/server_details/time_zone/lang.dart | 858 +++++++++--------- .../server_details/time_zone/time_zone.dart | 26 +- lib/ui/pages/services/services.dart | 58 +- lib/ui/pages/setup/initializing.dart | 136 +-- .../recovering/recover_by_new_device_key.dart | 28 +- .../recovering/recover_by_old_token.dart | 27 +- .../recovering/recover_by_recovery_key.dart | 16 +- .../recovery_confirm_backblaze.dart | 20 +- .../recovery_confirm_cloudflare.dart | 12 +- .../recovering/recovery_confirm_server.dart | 43 +- .../recovery_hentzner_connected.dart | 22 +- .../recovering/recovery_method_select.dart | 50 +- .../setup/recovering/recovery_routing.dart | 57 +- lib/ui/pages/ssh_keys/new_ssh_key.dart | 12 +- lib/ui/pages/ssh_keys/ssh_keys.dart | 10 +- lib/ui/pages/users/empty.dart | 10 +- lib/ui/pages/users/fab.dart | 6 +- lib/ui/pages/users/new_user.dart | 16 +- lib/ui/pages/users/user.dart | 5 +- lib/ui/pages/users/user_details.dart | 36 +- lib/ui/pages/users/users.dart | 8 +- lib/utils/color_utils.dart | 2 +- lib/utils/extensions/duration.dart | 4 +- lib/utils/extensions/text_extensions.dart | 26 +- lib/utils/route_transitions/slide_bottom.dart | 4 +- lib/utils/route_transitions/slide_right.dart | 2 +- pubspec.lock | 2 +- pubspec.yaml | 1 + test/widget_test.dart | 10 +- 131 files changed, 1289 insertions(+), 1259 deletions(-) rename lib/ui/components/brand_button/{FilledButton.dart => filled_button.dart} (100%) rename lib/ui/components/pre_styled_buttons/{flashFab.dart => flash_fab.dart} (92%) rename lib/ui/pages/{rootRoute.dart => root_route.dart} (92%) diff --git a/analysis_options.yaml b/analysis_options.yaml index 1caa1fad..39d63410 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -22,7 +22,7 @@ linter: # `// ignore_for_file: name_of_lint` syntax on the line or in the file # producing the lint. rules: - # avoid_print: false # Uncomment to disable the `avoid_print` rule + avoid_print: false # Uncomment to disable the `avoid_print` rule prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule # Additional information about this file can be found at diff --git a/lib/config/bloc_observer.dart b/lib/config/bloc_observer.dart index e54b399b..c53f5961 100644 --- a/lib/config/bloc_observer.dart +++ b/lib/config/bloc_observer.dart @@ -8,7 +8,7 @@ class SimpleBlocObserver extends BlocObserver { SimpleBlocObserver(); @override - void onError(BlocBase cubit, Object error, StackTrace stackTrace) { + void onError(BlocBase bloc, Object error, StackTrace stackTrace) { final navigator = getIt.get().navigator!; navigator.push( @@ -19,6 +19,6 @@ class SimpleBlocObserver extends BlocObserver { ), ), ); - super.onError(cubit, error, stackTrace); + super.onError(bloc, error, stackTrace); } } diff --git a/lib/config/brand_colors.dart b/lib/config/brand_colors.dart index c68d6dd8..a06b8895 100644 --- a/lib/config/brand_colors.dart +++ b/lib/config/brand_colors.dart @@ -10,7 +10,7 @@ class BrandColors { static const Color gray3 = Color(0xFFFAFAFA); static const Color gray4 = Color(0xFFDDDDDD); static const Color gray5 = Color(0xFFEDEEF1); - static Color gray6 = Color(0xFF181818).withOpacity(0.7); + static Color gray6 = const Color(0xFF181818).withOpacity(0.7); static const Color grey7 = Color(0xFFABABAB); static const Color red1 = Color(0xFFFA0E0E); diff --git a/lib/config/brand_theme.dart b/lib/config/brand_theme.dart index 8ebc54ee..5837748b 100644 --- a/lib/config/brand_theme.dart +++ b/lib/config/brand_theme.dart @@ -9,7 +9,7 @@ final lightTheme = ThemeData( fontFamily: 'Inter', brightness: Brightness.light, scaffoldBackgroundColor: BrandColors.scaffoldBackground, - inputDecorationTheme: InputDecorationTheme( + inputDecorationTheme: const InputDecorationTheme( border: InputBorder.none, contentPadding: EdgeInsets.all(16), enabledBorder: OutlineInputBorder( @@ -39,7 +39,7 @@ final lightTheme = ThemeData( color: BrandColors.red1, ), ), - listTileTheme: ListTileThemeData( + listTileTheme: const ListTileThemeData( minLeadingWidth: 24.0, ), textTheme: TextTheme( @@ -48,25 +48,25 @@ final lightTheme = ThemeData( headline3: headline3Style, headline4: headline4Style, bodyText1: body1Style, - subtitle1: TextStyle(fontSize: 15, height: 1.6), // text input style + subtitle1: const TextStyle(fontSize: 15, height: 1.6), // text input style ), ); var darkTheme = lightTheme.copyWith( brightness: Brightness.dark, - scaffoldBackgroundColor: Color(0xFF202120), - iconTheme: IconThemeData(color: BrandColors.gray3), + scaffoldBackgroundColor: const Color(0xFF202120), + iconTheme: const IconThemeData(color: BrandColors.gray3), cardColor: BrandColors.gray1, - dialogBackgroundColor: Color(0xFF202120), + dialogBackgroundColor: const Color(0xFF202120), textTheme: TextTheme( headline1: headline1Style.copyWith(color: BrandColors.white), headline2: headline2Style.copyWith(color: BrandColors.white), headline3: headline3Style.copyWith(color: BrandColors.white), headline4: headline4Style.copyWith(color: BrandColors.white), bodyText1: body1Style.copyWith(color: BrandColors.white), - subtitle1: TextStyle(fontSize: 15, height: 1.6), // text input style + subtitle1: const TextStyle(fontSize: 15, height: 1.6), // text input style ), - inputDecorationTheme: InputDecorationTheme( + inputDecorationTheme: const InputDecorationTheme( labelStyle: TextStyle(color: BrandColors.white), hintStyle: TextStyle(color: BrandColors.white), border: OutlineInputBorder( @@ -82,6 +82,6 @@ var darkTheme = lightTheme.copyWith( ), ); -final paddingH15V30 = EdgeInsets.symmetric(horizontal: 15, vertical: 30); +const paddingH15V30 = EdgeInsets.symmetric(horizontal: 15, vertical: 30); -final paddingH15V0 = EdgeInsets.symmetric(horizontal: 15); +const paddingH15V0 = EdgeInsets.symmetric(horizontal: 15); diff --git a/lib/config/hive_config.dart b/lib/config/hive_config.dart index 360c5e55..28748a98 100644 --- a/lib/config/hive_config.dart +++ b/lib/config/hive_config.dart @@ -41,7 +41,7 @@ class HiveConfig { } static Future getEncryptedKey(String encKey) async { - final secureStorage = FlutterSecureStorage(); + const secureStorage = FlutterSecureStorage(); var hasEncryptionKey = await secureStorage.containsKey(key: encKey); if (!hasEncryptionKey) { var key = Hive.generateSecureKey(); diff --git a/lib/config/localization.dart b/lib/config/localization.dart index 09d5ac07..9fe2dc20 100644 --- a/lib/config/localization.dart +++ b/lib/config/localization.dart @@ -11,9 +11,9 @@ class Localization extends StatelessWidget { @override Widget build(BuildContext context) { return EasyLocalization( - supportedLocales: [Locale('ru'), Locale('en')], + supportedLocales: const [Locale('ru'), Locale('en')], path: 'assets/translations', - fallbackLocale: Locale('en'), + fallbackLocale: const Locale('en'), saveLocale: false, useOnlyLangCode: true, child: child!, diff --git a/lib/config/text_themes.dart b/lib/config/text_themes.dart index f14d54e1..8e775783 100644 --- a/lib/config/text_themes.dart +++ b/lib/config/text_themes.dart @@ -3,7 +3,7 @@ import 'package:selfprivacy/utils/named_font_weight.dart'; import 'brand_colors.dart'; -final defaultTextStyle = TextStyle( +const defaultTextStyle = TextStyle( fontSize: 15, color: BrandColors.textColor1, ); @@ -51,7 +51,7 @@ final headline5Style = defaultTextStyle.copyWith( color: BrandColors.headlineColor.withOpacity(0.8), ); -final body1Style = defaultTextStyle; +const body1Style = defaultTextStyle; final body2Style = defaultTextStyle.copyWith( color: BrandColors.textColor2, ); @@ -69,7 +69,7 @@ final smallStyle = defaultTextStyle.copyWith(fontSize: 11, height: 1.45); final linkStyle = defaultTextStyle.copyWith(color: BrandColors.blue); -final progressTextStyleLight = TextStyle( +const progressTextStyleLight = TextStyle( fontSize: 11, color: BrandColors.textColor1, height: 1.7, diff --git a/lib/logic/api_maps/api_map.dart b/lib/logic/api_maps/api_map.dart index 5e07d838..a00757fe 100644 --- a/lib/logic/api_maps/api_map.dart +++ b/lib/logic/api_maps/api_map.dart @@ -56,7 +56,7 @@ class ConsoleInterceptor extends InterceptorsWrapper { @override Future onRequest( RequestOptions options, - RequestInterceptorHandler requestInterceptorHandler, + RequestInterceptorHandler handler, ) async { addMessage( Message( @@ -64,13 +64,13 @@ class ConsoleInterceptor extends InterceptorsWrapper { 'request-uri: ${options.uri}\nheaders: ${options.headers}\ndata: ${options.data}', ), ); - return super.onRequest(options, requestInterceptorHandler); + return super.onRequest(options, handler); } @override Future onResponse( Response response, - ResponseInterceptorHandler requestInterceptorHandler, + ResponseInterceptorHandler handler, ) async { addMessage( Message( @@ -80,7 +80,7 @@ class ConsoleInterceptor extends InterceptorsWrapper { ); return super.onResponse( response, - requestInterceptorHandler, + handler, ); } diff --git a/lib/logic/api_maps/backblaze.dart b/lib/logic/api_maps/backblaze.dart index 678ec333..abf460ea 100644 --- a/lib/logic/api_maps/backblaze.dart +++ b/lib/logic/api_maps/backblaze.dart @@ -23,6 +23,7 @@ class BackblazeApplicationKey { class BackblazeApi extends ApiMap { BackblazeApi({this.hasLogger = false, this.isWithToken = true}); + @override BaseOptions get options { var options = BaseOptions(baseUrl: rootAddress); if (isWithToken) { @@ -97,9 +98,9 @@ class BackblazeApi extends ApiMap { 'bucketType': 'allPrivate', 'lifecycleRules': [ { - "daysFromHidingToDeleting": 30, - "daysFromUploadingToHiding": null, - "fileNamePrefix": "" + 'daysFromHidingToDeleting': 30, + 'daysFromUploadingToHiding': null, + 'fileNamePrefix': '' } ], }, diff --git a/lib/logic/api_maps/cloudflare.dart b/lib/logic/api_maps/cloudflare.dart index 9b950546..8fc4aa1c 100644 --- a/lib/logic/api_maps/cloudflare.dart +++ b/lib/logic/api_maps/cloudflare.dart @@ -25,6 +25,7 @@ class CloudflareApi extends ApiMap { this.customToken, }); + @override BaseOptions get options { var options = BaseOptions(baseUrl: rootAddress); if (isWithToken) { @@ -164,7 +165,7 @@ class CloudflareApi extends ApiMap { await Future.wait(allCreateFutures); } on DioError catch (e) { print(e.message); - throw e; + rethrow; } finally { close(client); } diff --git a/lib/logic/api_maps/hetzner.dart b/lib/logic/api_maps/hetzner.dart index cbcacf80..aa1d19b6 100644 --- a/lib/logic/api_maps/hetzner.dart +++ b/lib/logic/api_maps/hetzner.dart @@ -10,11 +10,14 @@ import 'package:selfprivacy/logic/models/hive/user.dart'; import 'package:selfprivacy/utils/password_generator.dart'; class HetznerApi extends ApiMap { + @override bool hasLogger; + @override bool isWithToken; HetznerApi({this.hasLogger = false, this.isWithToken = true}); + @override BaseOptions get options { var options = BaseOptions(baseUrl: rootAddress); if (isWithToken) { @@ -60,12 +63,12 @@ class HetznerApi extends ApiMap { Response dbCreateResponse = await client.post( '/volumes', data: { - "size": 10, - "name": StringGenerators.dbStorageName(), - "labels": {"labelkey": "value"}, - "location": "fsn1", - "automount": false, - "format": "ext4" + 'size': 10, + 'name': StringGenerators.dbStorageName(), + 'labels': {'labelkey': 'value'}, + 'location': 'fsn1', + 'automount': false, + 'format': 'ext4' }, ); var dbId = dbCreateResponse.data['volume']['id']; @@ -93,7 +96,7 @@ class HetznerApi extends ApiMap { final base64Password = base64.encode(utf8.encode(rootUser.password ?? 'PASS')); - print("hostname: $hostname"); + print('hostname: $hostname'); /// add ssh key when you need it: e.g. "ssh_keys":["kherel"] /// check the branch name, it could be "development" or "master". @@ -103,18 +106,18 @@ class HetznerApi extends ApiMap { print(userdataString); final data = { - "name": hostname, - "server_type": "cx11", - "start_after_create": false, - "image": "ubuntu-20.04", - "volumes": [dbId], - "networks": [], - "user_data": userdataString, - "labels": {}, - "automount": true, - "location": "fsn1" + 'name': hostname, + 'server_type': 'cx11', + 'start_after_create': false, + 'image': 'ubuntu-20.04', + 'volumes': [dbId], + 'networks': [], + 'user_data': userdataString, + 'labels': {}, + 'automount': true, + 'location': 'fsn1' }; - print("Decoded data: $data"); + print('Decoded data: $data'); Response serverCreateResponse = await client.post( '/servers', @@ -129,7 +132,7 @@ class HetznerApi extends ApiMap { createTime: DateTime.now(), volume: dataBase, apiToken: apiToken, - provider: ServerProvider.Hetzner, + provider: ServerProvider.hetzner, ); } @@ -166,7 +169,7 @@ class HetznerApi extends ApiMap { for (var volumeId in volumes) { await client.post('/volumes/$volumeId/actions/detach'); } - await Future.delayed(Duration(seconds: 10)); + await Future.delayed(const Duration(seconds: 10)); for (var volumeId in volumes) { laterFutures.add(client.delete('/volumes/$volumeId')); @@ -203,9 +206,9 @@ class HetznerApi extends ApiMap { var client = await getClient(); Map queryParameters = { - "start": start.toUtc().toIso8601String(), - "end": end.toUtc().toIso8601String(), - "type": type + 'start': start.toUtc().toIso8601String(), + 'end': end.toUtc().toIso8601String(), + 'type': type }; var res = await client.get( '/servers/${hetznerServer!.id}/metrics', @@ -243,8 +246,8 @@ class HetznerApi extends ApiMap { await client.post( '/servers/${hetznerServer!.id}/actions/change_dns_ptr', data: { - "ip": ip4, - "dns_ptr": domainName, + 'ip': ip4, + 'dns_ptr': domainName, }, ); close(client); diff --git a/lib/logic/api_maps/server.dart b/lib/logic/api_maps/server.dart index d29dffa9..b58aff55 100644 --- a/lib/logic/api_maps/server.dart +++ b/lib/logic/api_maps/server.dart @@ -44,6 +44,7 @@ class ServerApi extends ApiMap { this.overrideDomain, this.customToken}); + @override BaseOptions get options { var options = BaseOptions(); @@ -73,7 +74,7 @@ class ServerApi extends ApiMap { Response response; var client = await getClient(); - String? apiVersion = null; + String? apiVersion; try { response = await client.get('/api/version'); @@ -82,8 +83,8 @@ class ServerApi extends ApiMap { print(e.message); } finally { close(client); - return apiVersion; } + return apiVersion; } Future isHttpServerWorking() async { @@ -98,8 +99,8 @@ class ServerApi extends ApiMap { print(e.message); } finally { close(client); - return res; } + return res; } Future> createUser(User user) async { @@ -227,7 +228,7 @@ class ServerApi extends ApiMap { try { response = await client.put( '/services/ssh/key/send', - data: {"public_key": ssh}, + data: {'public_key': ssh}, ); } on DioError catch (e) { print(e.message); @@ -293,7 +294,7 @@ class ServerApi extends ApiMap { try { response = await client.delete( '/services/ssh/keys/${user.login}', - data: {"public_key": sshKey}, + data: {'public_key': sshKey}, ); } on DioError catch (e) { print(e.message); @@ -331,10 +332,11 @@ class ServerApi extends ApiMap { res = false; } finally { close(client); - return res; } + return res; } + @override String get rootAddress => throw UnimplementedError('not used in with implementation'); @@ -351,8 +353,8 @@ class ServerApi extends ApiMap { res = false; } finally { close(client); - return res; } + return res; } Future switchService(ServiceTypes type, bool needToTurnOn) async { @@ -433,8 +435,8 @@ class ServerApi extends ApiMap { print(e); } finally { close(client); - return backups; } + return backups; } Future getBackupStatus() async { @@ -453,8 +455,8 @@ class ServerApi extends ApiMap { print(e.message); } finally { close(client); - return status; } + return status; } Future forceBackupListReload() async { @@ -496,8 +498,8 @@ class ServerApi extends ApiMap { print(e.message); } finally { close(client); - return result; } + return result; } Future reboot() async { @@ -514,8 +516,8 @@ class ServerApi extends ApiMap { print(e.message); } finally { close(client); - return result; } + return result; } Future upgrade() async { @@ -532,13 +534,13 @@ class ServerApi extends ApiMap { print(e.message); } finally { close(client); - return result; } + return result; } Future getAutoUpgradeSettings() async { Response response; - AutoUpgradeSettings settings = AutoUpgradeSettings( + AutoUpgradeSettings settings = const AutoUpgradeSettings( enable: false, allowReboot: false, ); @@ -553,8 +555,8 @@ class ServerApi extends ApiMap { print(e.message); } finally { close(client); - return settings; } + return settings; } Future updateAutoUpgradeSettings(AutoUpgradeSettings settings) async { @@ -616,7 +618,7 @@ class ServerApi extends ApiMap { } if (response.statusCode != HttpStatus.ok) { - return ""; + return ''; } final base64toString = utf8.fuse(base64); @@ -639,7 +641,7 @@ class ServerApi extends ApiMap { return ApiResponse( errorMessage: e.message, statusCode: e.response?.statusCode ?? HttpStatus.internalServerError, - data: RecoveryKeyStatus(exists: false, valid: false)); + data: const RecoveryKeyStatus(exists: false, valid: false)); } finally { close(client); } @@ -677,7 +679,7 @@ class ServerApi extends ApiMap { return ApiResponse( errorMessage: e.message, statusCode: e.response?.statusCode ?? HttpStatus.internalServerError, - data: ""); + data: ''); } finally { close(client); } @@ -686,7 +688,7 @@ class ServerApi extends ApiMap { return ApiResponse( statusCode: code, - data: response.data != null ? response.data["token"] : ''); + data: response.data != null ? response.data['token'] : ''); } Future> useRecoveryToken(DeviceToken token) async { @@ -706,7 +708,7 @@ class ServerApi extends ApiMap { return ApiResponse( errorMessage: e.message, statusCode: e.response?.statusCode ?? HttpStatus.internalServerError, - data: ""); + data: ''); } finally { client.close(); } @@ -715,7 +717,7 @@ class ServerApi extends ApiMap { return ApiResponse( statusCode: code, - data: response.data != null ? response.data["token"] : ''); + data: response.data != null ? response.data['token'] : ''); } Future> authorizeDevice(DeviceToken token) async { @@ -735,16 +737,14 @@ class ServerApi extends ApiMap { return ApiResponse( errorMessage: e.message, statusCode: e.response?.statusCode ?? HttpStatus.internalServerError, - data: ""); + data: ''); } finally { client.close(); } final int code = response.statusCode ?? HttpStatus.internalServerError; - return ApiResponse( - statusCode: code, - data: response.data["token"] != null ? response.data["token"] : ''); + return ApiResponse(statusCode: code, data: response.data['token'] ?? ''); } Future> createDeviceToken() async { @@ -758,7 +758,7 @@ class ServerApi extends ApiMap { return ApiResponse( errorMessage: e.message, statusCode: e.response?.statusCode ?? HttpStatus.internalServerError, - data: ""); + data: ''); } finally { client.close(); } @@ -767,7 +767,7 @@ class ServerApi extends ApiMap { return ApiResponse( statusCode: code, - data: response.data != null ? response.data["token"] : ''); + data: response.data != null ? response.data['token'] : ''); } Future> deleteDeviceToken() async { @@ -781,15 +781,14 @@ class ServerApi extends ApiMap { return ApiResponse( errorMessage: e.message, statusCode: e.response?.statusCode ?? HttpStatus.internalServerError, - data: ""); + data: ''); } finally { client.close(); } final int code = response.statusCode ?? HttpStatus.internalServerError; - return ApiResponse( - statusCode: code, data: response.data != null ? response.data : ''); + return ApiResponse(statusCode: code, data: response.data ?? ''); } Future>> getApiTokens() async { @@ -828,7 +827,7 @@ class ServerApi extends ApiMap { return ApiResponse( errorMessage: e.message, statusCode: e.response?.statusCode ?? HttpStatus.internalServerError, - data: ""); + data: ''); } finally { client.close(); } @@ -837,7 +836,7 @@ class ServerApi extends ApiMap { return ApiResponse( statusCode: code, - data: response.data != null ? response.data["token"] : ''); + data: response.data != null ? response.data['token'] : ''); } Future> deleteApiToken(String device) async { diff --git a/lib/logic/common_enum/common_enum.dart b/lib/logic/common_enum/common_enum.dart index 41fe6528..edaaf567 100644 --- a/lib/logic/common_enum/common_enum.dart +++ b/lib/logic/common_enum/common_enum.dart @@ -131,5 +131,5 @@ extension ServiceTypesExt on ServiceTypes { } } - String get txt => this.toString().split('.')[1]; + String get txt => toString().split('.')[1]; } diff --git a/lib/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart b/lib/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart index 217cb15c..080fd684 100644 --- a/lib/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart +++ b/lib/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart @@ -1,5 +1,5 @@ import 'dart:async'; -import 'package:bloc/bloc.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; diff --git a/lib/logic/cubit/app_settings/app_settings_cubit.dart b/lib/logic/cubit/app_settings/app_settings_cubit.dart index a4a2d0f7..bafc1d96 100644 --- a/lib/logic/cubit/app_settings/app_settings_cubit.dart +++ b/lib/logic/cubit/app_settings/app_settings_cubit.dart @@ -1,4 +1,4 @@ -import 'package:bloc/bloc.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:hive/hive.dart'; import 'package:selfprivacy/config/hive_config.dart'; diff --git a/lib/logic/cubit/backups/backups_cubit.dart b/lib/logic/cubit/backups/backups_cubit.dart index 47ad4796..45e24717 100644 --- a/lib/logic/cubit/backups/backups_cubit.dart +++ b/lib/logic/cubit/backups/backups_cubit.dart @@ -5,7 +5,6 @@ import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/logic/api_maps/backblaze.dart'; import 'package:selfprivacy/logic/api_maps/server.dart'; import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart'; -import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; import 'package:selfprivacy/logic/models/hive/backblaze_bucket.dart'; import 'package:selfprivacy/logic/models/json/backup.dart'; @@ -13,16 +12,18 @@ part 'backups_state.dart'; class BackupsCubit extends ServerInstallationDependendCubit { BackupsCubit(ServerInstallationCubit serverInstallationCubit) - : super(serverInstallationCubit, BackupsState(preventActions: true)); + : super( + serverInstallationCubit, const BackupsState(preventActions: true)); final api = ServerApi(); final backblaze = BackblazeApi(); + @override Future load() async { if (serverInstallationCubit.state is ServerInstallationFinished) { final bucket = getIt().backblazeBucket; if (bucket == null) { - emit(BackupsState( + emit(const BackupsState( isInitialized: false, preventActions: false, refreshing: false)); } else { final status = await api.getBackupStatus(); @@ -30,7 +31,7 @@ class BackupsCubit extends ServerInstallationDependendCubit { case BackupStatusEnum.noKey: case BackupStatusEnum.notInitialized: emit(BackupsState( - backups: [], + backups: const [], isInitialized: true, preventActions: false, progress: 0, @@ -40,12 +41,12 @@ class BackupsCubit extends ServerInstallationDependendCubit { break; case BackupStatusEnum.initializing: emit(BackupsState( - backups: [], + backups: const [], isInitialized: true, preventActions: false, progress: 0, status: status.status, - refreshTimer: Duration(seconds: 10), + refreshTimer: const Duration(seconds: 10), refreshing: false, )); break; @@ -72,12 +73,12 @@ class BackupsCubit extends ServerInstallationDependendCubit { progress: status.progress, status: status.status, error: status.errorMessage ?? '', - refreshTimer: Duration(seconds: 5), + refreshTimer: const Duration(seconds: 5), refreshing: false, )); break; default: - emit(BackupsState()); + emit(const BackupsState()); } Timer(state.refreshTimer, () => updateBackups(useTimer: true)); } @@ -126,11 +127,11 @@ class BackupsCubit extends ServerInstallationDependendCubit { switch (status) { case BackupStatusEnum.backingUp: case BackupStatusEnum.restoring: - return Duration(seconds: 5); + return const Duration(seconds: 5); case BackupStatusEnum.initializing: - return Duration(seconds: 10); + return const Duration(seconds: 10); default: - return Duration(seconds: 60); + return const Duration(seconds: 60); } } @@ -146,8 +147,9 @@ class BackupsCubit extends ServerInstallationDependendCubit { refreshTimer: refreshTimeFromState(status.status), refreshing: false, )); - if (useTimer) + if (useTimer) { Timer(state.refreshTimer, () => updateBackups(useTimer: true)); + } } Future forceUpdateBackups() async { @@ -173,6 +175,6 @@ class BackupsCubit extends ServerInstallationDependendCubit { @override void clear() async { - emit(BackupsState()); + emit(const BackupsState()); } } diff --git a/lib/logic/cubit/backups/backups_state.dart b/lib/logic/cubit/backups/backups_state.dart index 3d618a75..3f0e2c3f 100644 --- a/lib/logic/cubit/backups/backups_state.dart +++ b/lib/logic/cubit/backups/backups_state.dart @@ -7,7 +7,7 @@ class BackupsState extends ServerInstallationDependendState { this.progress = 0.0, this.status = BackupStatusEnum.noKey, this.preventActions = true, - this.error = "", + this.error = '', this.refreshTimer = const Duration(seconds: 60), this.refreshing = true, }); diff --git a/lib/logic/cubit/dns_records/dns_records_cubit.dart b/lib/logic/cubit/dns_records/dns_records_cubit.dart index 88ad9ba0..9d9bdf8e 100644 --- a/lib/logic/cubit/dns_records/dns_records_cubit.dart +++ b/lib/logic/cubit/dns_records/dns_records_cubit.dart @@ -1,6 +1,5 @@ import 'package:cubit_form/cubit_form.dart'; import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart'; -import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; import 'package:selfprivacy/logic/models/hive/server_domain.dart'; import 'package:selfprivacy/logic/models/json/dns_records.dart'; @@ -13,16 +12,17 @@ class DnsRecordsCubit extends ServerInstallationDependendCubit { DnsRecordsCubit(ServerInstallationCubit serverInstallationCubit) : super(serverInstallationCubit, - DnsRecordsState(dnsState: DnsRecordsStatus.refreshing)); + const DnsRecordsState(dnsState: DnsRecordsStatus.refreshing)); final api = ServerApi(); final cloudflare = CloudflareApi(); + @override Future load() async { emit(DnsRecordsState( dnsState: DnsRecordsStatus.refreshing, dnsRecords: _getDesiredDnsRecords( - serverInstallationCubit.state.serverDomain?.domainName, "", ""))); + serverInstallationCubit.state.serverDomain?.domainName, '', ''))); print('Loading DNS status'); if (serverInstallationCubit.state is ServerInstallationFinished) { final ServerDomain? domain = serverInstallationCubit.state.serverDomain; @@ -75,7 +75,7 @@ class DnsRecordsCubit : DnsRecordsStatus.good, )); } else { - emit(DnsRecordsState()); + emit(const DnsRecordsState()); } } } @@ -88,7 +88,7 @@ class DnsRecordsCubit @override Future clear() async { - emit(DnsRecordsState(dnsState: DnsRecordsStatus.error)); + emit(const DnsRecordsState(dnsState: DnsRecordsStatus.error)); } Future refresh() async { @@ -104,7 +104,7 @@ class DnsRecordsCubit await cloudflare.removeSimilarRecords(cloudFlareDomain: domain!); await cloudflare.createMultipleDnsRecords( cloudFlareDomain: domain, ip4: ipAddress); - await cloudflare.setDkim(dkimPublicKey ?? "", domain); + await cloudflare.setDkim(dkimPublicKey ?? '', domain); await load(); } diff --git a/lib/logic/cubit/dns_records/dns_records_state.dart b/lib/logic/cubit/dns_records/dns_records_state.dart index 7055d698..59c266b7 100644 --- a/lib/logic/cubit/dns_records/dns_records_state.dart +++ b/lib/logic/cubit/dns_records/dns_records_state.dart @@ -42,7 +42,7 @@ class DnsRecordsState extends ServerInstallationDependendState { class DesiredDnsRecord { const DesiredDnsRecord({ required this.name, - this.type = "A", + this.type = 'A', required this.content, this.description = '', this.category = DnsRecordsCategory.services, diff --git a/lib/logic/cubit/forms/factories/field_cubit_factory.dart b/lib/logic/cubit/forms/factories/field_cubit_factory.dart index a86f6445..86f3b70b 100644 --- a/lib/logic/cubit/forms/factories/field_cubit_factory.dart +++ b/lib/logic/cubit/forms/factories/field_cubit_factory.dart @@ -16,7 +16,7 @@ class FieldCubitFactory { /// - Must not be a reserved root login /// - Must be unique FieldCubit createUserLoginField() { - final userAllowedRegExp = RegExp(r"^[a-z_][a-z0-9_]+$"); + final userAllowedRegExp = RegExp(r'^[a-z_][a-z0-9_]+$'); const userMaxLength = 31; return FieldCubit( initalValue: '', @@ -40,7 +40,7 @@ class FieldCubitFactory { /// - Must fail on the regural expression of invalid matches: [\n\r\s]+ /// - Must not be empty FieldCubit createUserPasswordField() { - var passwordForbiddenRegExp = RegExp(r"[\n\r\s]+"); + var passwordForbiddenRegExp = RegExp(r'[\n\r\s]+'); return FieldCubit( initalValue: '', validations: [ diff --git a/lib/logic/cubit/forms/setup/initializing/cloudflare_form_cubit.dart b/lib/logic/cubit/forms/setup/initializing/cloudflare_form_cubit.dart index c6dea6c5..fd700633 100644 --- a/lib/logic/cubit/forms/setup/initializing/cloudflare_form_cubit.dart +++ b/lib/logic/cubit/forms/setup/initializing/cloudflare_form_cubit.dart @@ -8,7 +8,7 @@ import 'package:selfprivacy/logic/cubit/forms/validations/validations.dart'; class CloudFlareFormCubit extends FormCubit { CloudFlareFormCubit(this.initializingCubit) { - var regExp = RegExp(r"\s+|[!$%^&*()@+|~=`{}\[\]:<>?,.\/]"); + var regExp = RegExp(r'\s+|[!$%^&*()@+|~=`{}\[\]:<>?,.\/]'); apiKey = FieldCubit( initalValue: '', validations: [ @@ -48,9 +48,4 @@ class CloudFlareFormCubit extends FormCubit { } return true; } - - @override - Future close() async { - return super.close(); - } } diff --git a/lib/logic/cubit/forms/setup/initializing/domain_cloudflare.dart b/lib/logic/cubit/forms/setup/initializing/domain_cloudflare.dart index fe595927..bf9e1eb0 100644 --- a/lib/logic/cubit/forms/setup/initializing/domain_cloudflare.dart +++ b/lib/logic/cubit/forms/setup/initializing/domain_cloudflare.dart @@ -39,7 +39,7 @@ class DomainSetupCubit extends Cubit { var domain = ServerDomain( domainName: domainName, zoneId: zoneId, - provider: DnsProvider.Cloudflare, + provider: DnsProvider.cloudflare, ); serverInstallationCubit.setDomain(domain); diff --git a/lib/logic/cubit/forms/setup/initializing/hetzner_form_cubit.dart b/lib/logic/cubit/forms/setup/initializing/hetzner_form_cubit.dart index 0ac97e84..0d343191 100644 --- a/lib/logic/cubit/forms/setup/initializing/hetzner_form_cubit.dart +++ b/lib/logic/cubit/forms/setup/initializing/hetzner_form_cubit.dart @@ -8,7 +8,7 @@ import 'package:selfprivacy/logic/cubit/forms/validations/validations.dart'; class HetznerFormCubit extends FormCubit { HetznerFormCubit(this.serverInstallationCubit) { - var regExp = RegExp(r"\s+|[-!$%^&*()@+|~=`{}\[\]:<>?,.\/]"); + var regExp = RegExp(r'\s+|[-!$%^&*()@+|~=`{}\[\]:<>?,.\/]'); apiKey = FieldCubit( initalValue: '', validations: [ diff --git a/lib/logic/cubit/forms/setup/recovering/recovery_domain_form_cubit.dart b/lib/logic/cubit/forms/setup/recovering/recovery_domain_form_cubit.dart index c67f3bee..0064cae8 100644 --- a/lib/logic/cubit/forms/setup/recovering/recovery_domain_form_cubit.dart +++ b/lib/logic/cubit/forms/setup/recovering/recovery_domain_form_cubit.dart @@ -34,7 +34,7 @@ class RecoveryDomainFormCubit extends FormCubit { final bool domainValid = await api.getApiVersion() != null; if (!domainValid) { - serverDomainField.setError("recovering.domain_recover_error".tr()); + serverDomainField.setError('recovering.domain_recover_error'.tr()); } return domainValid; diff --git a/lib/logic/cubit/forms/user/ssh_form_cubit.dart b/lib/logic/cubit/forms/user/ssh_form_cubit.dart index 47bf2994..bebbbcd5 100644 --- a/lib/logic/cubit/forms/user/ssh_form_cubit.dart +++ b/lib/logic/cubit/forms/user/ssh_form_cubit.dart @@ -12,7 +12,7 @@ class SshFormCubit extends FormCubit { required this.user, }) { var keyRegExp = RegExp( - r"^(ssh-rsa AAAAB3NzaC1yc2|ssh-ed25519 AAAAC3NzaC1lZDI1NTE5)[0-9A-Za-z+/]+[=]{0,3}( .*)?$"); + r'^(ssh-rsa AAAAB3NzaC1yc2|ssh-ed25519 AAAAC3NzaC1lZDI1NTE5)[0-9A-Za-z+/]+[=]{0,3}( .*)?$'); key = FieldCubit( initalValue: '', diff --git a/lib/logic/cubit/forms/validations/validations.dart b/lib/logic/cubit/forms/validations/validations.dart index 91a8f75c..b7d054d0 100644 --- a/lib/logic/cubit/forms/validations/validations.dart +++ b/lib/logic/cubit/forms/validations/validations.dart @@ -8,7 +8,7 @@ abstract class LengthStringValidation extends ValidationModel { @override String? check(String value) { var length = value.length; - var errorMessage = this.errorMassage.replaceAll("[]", length.toString()); + var errorMessage = errorMassage.replaceAll('[]', length.toString()); return test(value) ? errorMessage : null; } } diff --git a/lib/logic/cubit/hetzner_metrics/hetzner_metrics_cubit.dart b/lib/logic/cubit/hetzner_metrics/hetzner_metrics_cubit.dart index 3795c828..d16b13b0 100644 --- a/lib/logic/cubit/hetzner_metrics/hetzner_metrics_cubit.dart +++ b/lib/logic/cubit/hetzner_metrics/hetzner_metrics_cubit.dart @@ -1,6 +1,6 @@ import 'dart:async'; -import 'package:bloc/bloc.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:selfprivacy/logic/common_enum/common_enum.dart'; import 'package:selfprivacy/logic/models/hetzner_metrics.dart'; @@ -10,12 +10,13 @@ import 'hetzner_metrics_repository.dart'; part 'hetzner_metrics_state.dart'; class HetznerMetricsCubit extends Cubit { - HetznerMetricsCubit() : super(HetznerMetricsLoading(Period.day)); + HetznerMetricsCubit() : super(const HetznerMetricsLoading(Period.day)); final repository = HetznerMetricsRepository(); Timer? timer; + @override close() { closeTimer(); return super.close(); diff --git a/lib/logic/cubit/hetzner_metrics/hetzner_metrics_repository.dart b/lib/logic/cubit/hetzner_metrics/hetzner_metrics_repository.dart index 11ce2bc1..fe601cc6 100644 --- a/lib/logic/cubit/hetzner_metrics/hetzner_metrics_repository.dart +++ b/lib/logic/cubit/hetzner_metrics/hetzner_metrics_repository.dart @@ -11,13 +11,13 @@ class HetznerMetricsRepository { switch (period) { case Period.hour: - start = end.subtract(Duration(hours: 1)); + start = end.subtract(const Duration(hours: 1)); break; case Period.day: - start = end.subtract(Duration(days: 1)); + start = end.subtract(const Duration(days: 1)); break; case Period.month: - start = end.subtract(Duration(days: 15)); + start = end.subtract(const Duration(days: 15)); break; } @@ -28,14 +28,14 @@ class HetznerMetricsRepository { api.getMetrics(start, end, 'network'), ]); - var cpuMetricsData = results[0]["metrics"]; - var networkMetricsData = results[1]["metrics"]; + var cpuMetricsData = results[0]['metrics']; + var networkMetricsData = results[1]['metrics']; return HetznerMetricsLoaded( period: period, start: start, end: end, - stepInSeconds: cpuMetricsData["step"], + stepInSeconds: cpuMetricsData['step'], cpu: timeSeriesSerializer(cpuMetricsData, 'cpu'), ppsIn: timeSeriesSerializer(networkMetricsData, 'network.0.pps.in'), ppsOut: timeSeriesSerializer(networkMetricsData, 'network.0.pps.out'), @@ -51,6 +51,6 @@ class HetznerMetricsRepository { List timeSeriesSerializer( Map json, String type) { - List list = json["time_series"][type]["values"]; + List list = json['time_series'][type]['values']; return list.map((el) => TimeSeriesData(el[0], double.parse(el[1]))).toList(); } diff --git a/lib/logic/cubit/hetzner_metrics/hetzner_metrics_state.dart b/lib/logic/cubit/hetzner_metrics/hetzner_metrics_state.dart index bbd3f7d1..b6204db9 100644 --- a/lib/logic/cubit/hetzner_metrics/hetzner_metrics_state.dart +++ b/lib/logic/cubit/hetzner_metrics/hetzner_metrics_state.dart @@ -7,7 +7,8 @@ abstract class HetznerMetricsState extends Equatable { } class HetznerMetricsLoading extends HetznerMetricsState { - HetznerMetricsLoading(this.period); + const HetznerMetricsLoading(this.period); + @override final Period period; @override @@ -15,7 +16,7 @@ class HetznerMetricsLoading extends HetznerMetricsState { } class HetznerMetricsLoaded extends HetznerMetricsState { - HetznerMetricsLoaded({ + const HetznerMetricsLoaded({ required this.period, required this.start, required this.end, @@ -27,6 +28,7 @@ class HetznerMetricsLoaded extends HetznerMetricsState { required this.bandwidthOut, }); + @override final Period period; final DateTime start; final DateTime end; diff --git a/lib/logic/cubit/providers/providers_cubit.dart b/lib/logic/cubit/providers/providers_cubit.dart index 5f92e225..5d48ecda 100644 --- a/lib/logic/cubit/providers/providers_cubit.dart +++ b/lib/logic/cubit/providers/providers_cubit.dart @@ -1,4 +1,4 @@ -import 'package:bloc/bloc.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:selfprivacy/logic/models/provider.dart'; import 'package:selfprivacy/logic/models/state_types.dart'; diff --git a/lib/logic/cubit/recovery_key/recovery_key_cubit.dart b/lib/logic/cubit/recovery_key/recovery_key_cubit.dart index eec3bfb1..6092a03d 100644 --- a/lib/logic/cubit/recovery_key/recovery_key_cubit.dart +++ b/lib/logic/cubit/recovery_key/recovery_key_cubit.dart @@ -7,7 +7,7 @@ part 'recovery_key_state.dart'; class RecoveryKeyCubit extends ServerInstallationDependendCubit { RecoveryKeyCubit(ServerInstallationCubit serverInstallationCubit) - : super(serverInstallationCubit, RecoveryKeyState.initial()); + : super(serverInstallationCubit, const RecoveryKeyState.initial()); final api = ServerApi(); diff --git a/lib/logic/cubit/recovery_key/recovery_key_state.dart b/lib/logic/cubit/recovery_key/recovery_key_state.dart index 60e86ecf..c88a9138 100644 --- a/lib/logic/cubit/recovery_key/recovery_key_state.dart +++ b/lib/logic/cubit/recovery_key/recovery_key_state.dart @@ -7,12 +7,12 @@ enum LoadingStatus { error, } - class RecoveryKeyState extends ServerInstallationDependendState { const RecoveryKeyState(this._status, this.loadingStatus); - RecoveryKeyState.initial() - : this(RecoveryKeyStatus(exists: false, valid: false), LoadingStatus.refreshing); + const RecoveryKeyState.initial() + : this(const RecoveryKeyStatus(exists: false, valid: false), + LoadingStatus.refreshing); final RecoveryKeyStatus _status; final LoadingStatus loadingStatus; @@ -30,7 +30,7 @@ class RecoveryKeyState extends ServerInstallationDependendState { LoadingStatus? loadingStatus, }) { return RecoveryKeyState( - status ?? this._status, + status ?? _status, loadingStatus ?? this.loadingStatus, ); } diff --git a/lib/logic/cubit/server_detailed_info/server_detailed_info_cubit.dart b/lib/logic/cubit/server_detailed_info/server_detailed_info_cubit.dart index 86b44be7..dea0081a 100644 --- a/lib/logic/cubit/server_detailed_info/server_detailed_info_cubit.dart +++ b/lib/logic/cubit/server_detailed_info/server_detailed_info_cubit.dart @@ -1,4 +1,4 @@ -import 'package:bloc/bloc.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/logic/cubit/server_detailed_info/server_detailed_info_repository.dart'; diff --git a/lib/logic/cubit/server_detailed_info/server_detailed_info_repository.dart b/lib/logic/cubit/server_detailed_info/server_detailed_info_repository.dart index ee407da5..842fa527 100644 --- a/lib/logic/cubit/server_detailed_info/server_detailed_info_repository.dart +++ b/lib/logic/cubit/server_detailed_info/server_detailed_info_repository.dart @@ -8,9 +8,9 @@ class ServerDetailsRepository { var hetznerAPi = HetznerApi(); var selfprivacyServer = ServerApi(); - Future<_ServerDetailsRepositoryDto> load() async { + Future load() async { print('load'); - return _ServerDetailsRepositoryDto( + return ServerDetailsRepositoryDto( autoUpgradeSettings: await selfprivacyServer.getAutoUpgradeSettings(), hetznerServerInfo: await hetznerAPi.getInfo(), serverTimezone: await selfprivacyServer.getServerTimezone(), @@ -18,14 +18,14 @@ class ServerDetailsRepository { } } -class _ServerDetailsRepositoryDto { +class ServerDetailsRepositoryDto { final HetznerServerInfo hetznerServerInfo; final TimeZoneSettings serverTimezone; final AutoUpgradeSettings autoUpgradeSettings; - _ServerDetailsRepositoryDto({ + ServerDetailsRepositoryDto({ required this.hetznerServerInfo, required this.serverTimezone, required this.autoUpgradeSettings, diff --git a/lib/logic/cubit/server_detailed_info/server_detailed_info_state.dart b/lib/logic/cubit/server_detailed_info/server_detailed_info_state.dart index b4524751..034d2a47 100644 --- a/lib/logic/cubit/server_detailed_info/server_detailed_info_state.dart +++ b/lib/logic/cubit/server_detailed_info/server_detailed_info_state.dart @@ -23,7 +23,7 @@ class Loaded extends ServerDetailsState { final AutoUpgradeSettings autoUpgradeSettings; final DateTime checkTime; - Loaded({ + const Loaded({ required this.serverInfo, required this.serverTimezone, required this.autoUpgradeSettings, diff --git a/lib/logic/cubit/server_installation/server_installation_cubit.dart b/lib/logic/cubit/server_installation/server_installation_cubit.dart index 579269e7..311f41e1 100644 --- a/lib/logic/cubit/server_installation/server_installation_cubit.dart +++ b/lib/logic/cubit/server_installation/server_installation_cubit.dart @@ -1,6 +1,6 @@ import 'dart:async'; -import 'package:bloc/bloc.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:equatable/equatable.dart'; import 'package:selfprivacy/config/get_it_config.dart'; @@ -17,7 +17,7 @@ export 'package:provider/provider.dart'; part '../server_installation/server_installation_state.dart'; class ServerInstallationCubit extends Cubit { - ServerInstallationCubit() : super(ServerInstallationEmpty()); + ServerInstallationCubit() : super(const ServerInstallationEmpty()); final repository = ServerInstallationRepository(); @@ -54,7 +54,7 @@ class ServerInstallationCubit extends Cubit { if (state is ServerInstallationRecovery) { emit((state as ServerInstallationRecovery).copyWith( hetznerKey: hetznerKey, - currentStep: RecoveryStep.ServerSelection, + currentStep: RecoveryStep.serverSelection, )); return; } @@ -104,12 +104,12 @@ class ServerInstallationCubit extends Cubit { } void createServerAndSetDnsRecords() async { - ServerInstallationNotFinished _stateCopy = + ServerInstallationNotFinished stateCopy = state as ServerInstallationNotFinished; - var onCancel = () => emit( + onCancel() => emit( (state as ServerInstallationNotFinished).copyWith(isLoading: false)); - var onSuccess = (ServerHostingDetails serverDetails) async { + onSuccess(ServerHostingDetails serverDetails) async { await repository.createDnsRecords( serverDetails.ip4, state.serverDomain!, @@ -120,8 +120,8 @@ class ServerInstallationCubit extends Cubit { isLoading: false, serverDetails: serverDetails, )); - runDelayed(startServerIfDnsIsOkay, Duration(seconds: 30), null); - }; + runDelayed(startServerIfDnsIsOkay, const Duration(seconds: 30), null); + } try { emit((state as ServerInstallationNotFinished).copyWith(isLoading: true)); @@ -134,7 +134,7 @@ class ServerInstallationCubit extends Cubit { onSuccess: onSuccess, ); } catch (e) { - emit(_stateCopy); + emit(stateCopy); } } @@ -163,7 +163,8 @@ class ServerInstallationCubit extends Cubit { serverDetails: server, ), ); - runDelayed(resetServerIfServerIsOkay, Duration(seconds: 60), dataState); + runDelayed( + resetServerIfServerIsOkay, const Duration(seconds: 60), dataState); } else { emit( dataState.copyWith( @@ -171,7 +172,8 @@ class ServerInstallationCubit extends Cubit { dnsMatches: matches, ), ); - runDelayed(startServerIfDnsIsOkay, Duration(seconds: 30), dataState); + runDelayed( + startServerIfDnsIsOkay, const Duration(seconds: 30), dataState); } } @@ -183,7 +185,7 @@ class ServerInstallationCubit extends Cubit { var isServerWorking = await repository.isHttpServerWorking(); if (isServerWorking) { - var pauseDuration = Duration(seconds: 30); + var pauseDuration = const Duration(seconds: 30); emit(TimerState( dataState: dataState, timerStart: DateTime.now(), @@ -202,10 +204,11 @@ class ServerInstallationCubit extends Cubit { isLoading: false, ), ); - runDelayed(finishCheckIfServerIsOkay, Duration(seconds: 60), dataState); + runDelayed( + finishCheckIfServerIsOkay, const Duration(seconds: 60), dataState); }); } else { - runDelayed(oneMoreReset, Duration(seconds: 60), dataState); + runDelayed(oneMoreReset, const Duration(seconds: 60), dataState); } } @@ -219,7 +222,7 @@ class ServerInstallationCubit extends Cubit { var isServerWorking = await repository.isHttpServerWorking(); if (isServerWorking) { - var pauseDuration = Duration(seconds: 30); + var pauseDuration = const Duration(seconds: 30); emit(TimerState( dataState: dataState, timerStart: DateTime.now(), @@ -238,10 +241,11 @@ class ServerInstallationCubit extends Cubit { isLoading: false, ), ); - runDelayed(oneMoreReset, Duration(seconds: 60), dataState); + runDelayed(oneMoreReset, const Duration(seconds: 60), dataState); }); } else { - runDelayed(resetServerIfServerIsOkay, Duration(seconds: 60), dataState); + runDelayed( + resetServerIfServerIsOkay, const Duration(seconds: 60), dataState); } } @@ -260,7 +264,8 @@ class ServerInstallationCubit extends Cubit { emit(dataState.finish()); } else { - runDelayed(finishCheckIfServerIsOkay, Duration(seconds: 60), dataState); + runDelayed( + finishCheckIfServerIsOkay, const Duration(seconds: 60), dataState); } } @@ -280,7 +285,7 @@ class ServerInstallationCubit extends Cubit { void submitDomainForAccessRecovery(String domain) async { var serverDomain = ServerDomain( domainName: domain, - provider: DnsProvider.Unknown, + provider: DnsProvider.unknown, zoneId: '', ); final recoveryCapabilities = @@ -291,12 +296,12 @@ class ServerInstallationCubit extends Cubit { emit(ServerInstallationRecovery( serverDomain: serverDomain, recoveryCapabilities: recoveryCapabilities, - currentStep: RecoveryStep.Selecting, + currentStep: RecoveryStep.selecting, )); } void tryToRecover(String token, ServerRecoveryMethods method) async { - final dataState = this.state as ServerInstallationRecovery; + final dataState = state as ServerInstallationRecovery; final serverDomain = dataState.serverDomain; if (serverDomain == null) { return; @@ -324,7 +329,7 @@ class ServerInstallationCubit extends Cubit { await repository.saveServerDetails(serverDetails); emit(dataState.copyWith( serverDetails: serverDetails, - currentStep: RecoveryStep.HetznerToken, + currentStep: RecoveryStep.hetznerToken, )); } on ServerAuthorizationException { getIt() @@ -338,23 +343,23 @@ class ServerInstallationCubit extends Cubit { } void revertRecoveryStep() { - final dataState = this.state as ServerInstallationRecovery; + final dataState = state as ServerInstallationRecovery; switch (dataState.currentStep) { - case RecoveryStep.Selecting: + case RecoveryStep.selecting: repository.deleteDomain(); - emit(ServerInstallationEmpty()); + emit(const ServerInstallationEmpty()); break; - case RecoveryStep.RecoveryKey: - case RecoveryStep.NewDeviceKey: - case RecoveryStep.OldToken: + case RecoveryStep.recoveryKey: + case RecoveryStep.newDeviceKey: + case RecoveryStep.oldToken: emit(dataState.copyWith( - currentStep: RecoveryStep.Selecting, + currentStep: RecoveryStep.selecting, )); break; - case RecoveryStep.ServerSelection: + case RecoveryStep.serverSelection: repository.deleteHetznerKey(); emit(dataState.copyWith( - currentStep: RecoveryStep.HetznerToken, + currentStep: RecoveryStep.hetznerToken, )); break; // We won't revert steps after client is authorized @@ -364,21 +369,21 @@ class ServerInstallationCubit extends Cubit { } void selectRecoveryMethod(ServerRecoveryMethods method) { - final dataState = this.state as ServerInstallationRecovery; + final dataState = state as ServerInstallationRecovery; switch (method) { case ServerRecoveryMethods.newDeviceKey: emit(dataState.copyWith( - currentStep: RecoveryStep.NewDeviceKey, + currentStep: RecoveryStep.newDeviceKey, )); break; case ServerRecoveryMethods.recoveryKey: emit(dataState.copyWith( - currentStep: RecoveryStep.RecoveryKey, + currentStep: RecoveryStep.recoveryKey, )); break; case ServerRecoveryMethods.oldToken: emit(dataState.copyWith( - currentStep: RecoveryStep.OldToken, + currentStep: RecoveryStep.oldToken, )); break; } @@ -386,7 +391,7 @@ class ServerInstallationCubit extends Cubit { Future> getServersOnHetznerAccount() async { - final dataState = this.state as ServerInstallationRecovery; + final dataState = state as ServerInstallationRecovery; final servers = await repository.getServersOnHetznerAccount(); final validated = servers .map((server) => ServerBasicInfoWithValidators.fromServerBasicInfo( @@ -399,7 +404,7 @@ class ServerInstallationCubit extends Cubit { } Future setServerId(ServerBasicInfo server) async { - final dataState = this.state as ServerInstallationRecovery; + final dataState = state as ServerInstallationRecovery; final serverDomain = dataState.serverDomain; if (serverDomain == null) { return; @@ -410,21 +415,21 @@ class ServerInstallationCubit extends Cubit { createTime: server.created, volume: ServerVolume( id: server.volumeId, - name: "recovered_volume", + name: 'recovered_volume', ), apiToken: dataState.serverDetails!.apiToken, - provider: ServerProvider.Hetzner, + provider: ServerProvider.hetzner, ); await repository.saveDomain(serverDomain); await repository.saveServerDetails(serverDetails); emit(dataState.copyWith( serverDetails: serverDetails, - currentStep: RecoveryStep.CloudflareToken, + currentStep: RecoveryStep.cloudflareToken, )); } Future setAndValidateCloudflareToken(String token) async { - final dataState = this.state as ServerInstallationRecovery; + final dataState = state as ServerInstallationRecovery; final serverDomain = dataState.serverDomain; if (serverDomain == null) { return; @@ -438,17 +443,17 @@ class ServerInstallationCubit extends Cubit { await repository.saveDomain(ServerDomain( domainName: serverDomain.domainName, zoneId: zoneId, - provider: DnsProvider.Cloudflare, + provider: DnsProvider.cloudflare, )); await repository.saveCloudFlareKey(token); emit(dataState.copyWith( serverDomain: ServerDomain( domainName: serverDomain.domainName, zoneId: zoneId, - provider: DnsProvider.Cloudflare, + provider: DnsProvider.cloudflare, ), cloudFlareKey: token, - currentStep: RecoveryStep.BackblazeToken, + currentStep: RecoveryStep.backblazeToken, )); } @@ -474,7 +479,7 @@ class ServerInstallationCubit extends Cubit { closeTimer(); repository.clearAppConfig(); - emit(ServerInstallationEmpty()); + emit(const ServerInstallationEmpty()); } Future serverDelete() async { @@ -499,6 +504,7 @@ class ServerInstallationCubit extends Cubit { )); } + @override close() { closeTimer(); return super.close(); diff --git a/lib/logic/cubit/server_installation/server_installation_repository.dart b/lib/logic/cubit/server_installation/server_installation_repository.dart index bbeb5700..e4e07804 100644 --- a/lib/logic/cubit/server_installation/server_installation_repository.dart +++ b/lib/logic/cubit/server_installation/server_installation_repository.dart @@ -62,7 +62,7 @@ class ServerInstallationRepository { ); } - if (serverDomain != null && serverDomain.provider == DnsProvider.Unknown) { + if (serverDomain != null && serverDomain.provider == DnsProvider.unknown) { return ServerInstallationRecovery( hetznerKey: hetznerToken, cloudFlareKey: cloudflareToken, @@ -101,17 +101,17 @@ class ServerInstallationRepository { ) { if (serverDetails != null) { if (hetznerToken != null) { - if (serverDetails.provider != ServerProvider.Unknown) { - if (serverDomain.provider != DnsProvider.Unknown) { - return RecoveryStep.BackblazeToken; + if (serverDetails.provider != ServerProvider.unknown) { + if (serverDomain.provider != DnsProvider.unknown) { + return RecoveryStep.backblazeToken; } - return RecoveryStep.CloudflareToken; + return RecoveryStep.cloudflareToken; } - return RecoveryStep.ServerSelection; + return RecoveryStep.serverSelection; } - return RecoveryStep.HetznerToken; + return RecoveryStep.hetznerToken; } - return RecoveryStep.Selecting; + return RecoveryStep.selecting; } void clearAppConfig() { @@ -271,7 +271,7 @@ class ServerInstallationRepository { var nav = getIt.get(); nav.showPopUpDialog( BrandAlert( - title: e.response!.data["errors"][0]["code"] == 1038 + title: e.response!.data['errors'][0]['code'] == 1038 ? 'modals.10'.tr() : 'providers.domain.states.error'.tr(), contentText: 'modals.6'.tr(), @@ -309,7 +309,7 @@ class ServerInstallationRepository { var dkimRecordString = await api.getDkim(); - await cloudflareApi.setDkim(dkimRecordString ?? "", cloudFlareDomain); + await cloudflareApi.setDkim(dkimRecordString ?? '', cloudFlareDomain); } Future isHttpServerWorking() async { @@ -408,7 +408,7 @@ class ServerInstallationRepository { id: 0, name: '', ), - provider: ServerProvider.Unknown, + provider: ServerProvider.unknown, id: 0, ip4: serverIp, startTime: null, @@ -439,7 +439,7 @@ class ServerInstallationRepository { id: 0, name: '', ), - provider: ServerProvider.Unknown, + provider: ServerProvider.unknown, id: 0, ip4: '', startTime: null, @@ -472,7 +472,7 @@ class ServerInstallationRepository { id: 0, name: '', ), - provider: ServerProvider.Unknown, + provider: ServerProvider.unknown, id: 0, ip4: '', startTime: null, @@ -487,7 +487,7 @@ class ServerInstallationRepository { Future getMainUser() async { var serverApi = ServerApi(); - final fallbackUser = User( + const fallbackUser = User( isFoundOnServer: false, note: 'Couldn\'t find main user on server, API is outdated', login: 'UNKNOWN', diff --git a/lib/logic/cubit/server_installation/server_installation_state.dart b/lib/logic/cubit/server_installation/server_installation_state.dart index 605a26ff..e6979ff4 100644 --- a/lib/logic/cubit/server_installation/server_installation_state.dart +++ b/lib/logic/cubit/server_installation/server_installation_state.dart @@ -122,7 +122,7 @@ class ServerInstallationNotFinished extends ServerInstallationState { final bool isLoading; final Map? dnsMatches; - ServerInstallationNotFinished({ + const ServerInstallationNotFinished({ String? hetznerKey, String? cloudFlareKey, BackblazeCredential? backblazeCredential, @@ -203,7 +203,7 @@ class ServerInstallationNotFinished extends ServerInstallationState { } class ServerInstallationEmpty extends ServerInstallationNotFinished { - ServerInstallationEmpty() + const ServerInstallationEmpty() : super( hetznerKey: null, cloudFlareKey: null, @@ -256,14 +256,14 @@ class ServerInstallationFinished extends ServerInstallationState { } enum RecoveryStep { - Selecting, - RecoveryKey, - NewDeviceKey, - OldToken, - HetznerToken, - ServerSelection, - CloudflareToken, - BackblazeToken, + selecting, + recoveryKey, + newDeviceKey, + oldToken, + hetznerToken, + serverSelection, + cloudflareToken, + backblazeToken, } enum ServerRecoveryCapabilities { @@ -289,8 +289,8 @@ class ServerInstallationRecovery extends ServerInstallationState { ServerDomain? serverDomain, User? rootUser, ServerHostingDetails? serverDetails, - required RecoveryStep this.currentStep, - required ServerRecoveryCapabilities this.recoveryCapabilities, + required this.currentStep, + required this.recoveryCapabilities, }) : super( hetznerKey: hetznerKey, cloudFlareKey: cloudFlareKey, diff --git a/lib/logic/cubit/services/services_cubit.dart b/lib/logic/cubit/services/services_cubit.dart index 5caa5a42..83b22086 100644 --- a/lib/logic/cubit/services/services_cubit.dart +++ b/lib/logic/cubit/services/services_cubit.dart @@ -1,7 +1,6 @@ import 'package:selfprivacy/logic/api_maps/server.dart'; import 'package:selfprivacy/logic/common_enum/common_enum.dart'; import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart'; -import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; part 'services_state.dart'; @@ -9,6 +8,7 @@ class ServicesCubit extends ServerInstallationDependendCubit { ServicesCubit(ServerInstallationCubit serverInstallationCubit) : super(serverInstallationCubit, ServicesState.allOff()); final api = ServerApi(); + @override Future load() async { if (serverInstallationCubit.state is ServerInstallationFinished) { var statuses = await api.servicesPowerCheck(); diff --git a/lib/logic/cubit/services/services_state.dart b/lib/logic/cubit/services/services_state.dart index 2503a4c7..dba7accc 100644 --- a/lib/logic/cubit/services/services_state.dart +++ b/lib/logic/cubit/services/services_state.dart @@ -15,14 +15,14 @@ class ServicesState extends ServerInstallationDependendState { final bool isSocialNetworkEnable; final bool isVpnEnable; - factory ServicesState.allOff() => ServicesState( + factory ServicesState.allOff() => const ServicesState( isPasswordManagerEnable: false, isCloudEnable: false, isGitEnable: false, isSocialNetworkEnable: false, isVpnEnable: false, ); - factory ServicesState.allOn() => ServicesState( + factory ServicesState.allOn() => const ServicesState( isPasswordManagerEnable: true, isCloudEnable: true, isGitEnable: true, diff --git a/lib/logic/cubit/users/users_cubit.dart b/lib/logic/cubit/users/users_cubit.dart index f7ce18bb..61d0a789 100644 --- a/lib/logic/cubit/users/users_cubit.dart +++ b/lib/logic/cubit/users/users_cubit.dart @@ -1,7 +1,6 @@ import 'package:hive/hive.dart'; import 'package:selfprivacy/config/hive_config.dart'; import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart'; -import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; import 'package:selfprivacy/logic/models/hive/user.dart'; import '../../api_maps/server.dart'; @@ -14,18 +13,19 @@ class UsersCubit extends ServerInstallationDependendCubit { UsersCubit(ServerInstallationCubit serverInstallationCubit) : super( serverInstallationCubit, - UsersState( + const UsersState( [], User(login: 'root'), User(login: 'loading...'))); Box box = Hive.box(BNames.usersBox); Box serverInstallationBox = Hive.box(BNames.serverInstallationBox); final api = ServerApi(); + @override Future load() async { if (serverInstallationCubit.state is ServerInstallationFinished) { var loadedUsers = box.values.toList(); final primaryUser = serverInstallationBox.get(BNames.rootUser, - defaultValue: User(login: 'loading...')); + defaultValue: const User(login: 'loading...')); List rootKeys = [ ...serverInstallationBox.get(BNames.rootKeys, defaultValue: []) ]; @@ -308,6 +308,7 @@ class UsersCubit extends ServerInstallationDependendCubit { @override void clear() async { - emit(UsersState([], User(login: 'root'), User(login: 'loading...'))); + emit(const UsersState( + [], User(login: 'root'), User(login: 'loading...'))); } } diff --git a/lib/logic/get_it/api_config.dart b/lib/logic/get_it/api_config.dart index 1d6f2610..3e7ea4b3 100644 --- a/lib/logic/get_it/api_config.dart +++ b/lib/logic/get_it/api_config.dart @@ -6,7 +6,7 @@ import 'package:selfprivacy/logic/models/hive/server_details.dart'; import 'package:selfprivacy/logic/models/hive/server_domain.dart'; class ApiConfigModel { - Box _box = Hive.box(BNames.serverInstallationBox); + final Box _box = Hive.box(BNames.serverInstallationBox); ServerHostingDetails? get serverDetails => _serverDetails; String? get hetznerKey => _hetznerKey; diff --git a/lib/logic/get_it/console.dart b/lib/logic/get_it/console.dart index 80dc5a37..979895a1 100644 --- a/lib/logic/get_it/console.dart +++ b/lib/logic/get_it/console.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:selfprivacy/logic/models/message.dart'; class ConsoleModel extends ChangeNotifier { - List _messages = []; + final List _messages = []; List get messages => _messages; diff --git a/lib/logic/models/hive/backblaze_bucket.dart b/lib/logic/models/hive/backblaze_bucket.dart index e1e0dd3f..140f2122 100644 --- a/lib/logic/models/hive/backblaze_bucket.dart +++ b/lib/logic/models/hive/backblaze_bucket.dart @@ -24,6 +24,6 @@ class BackblazeBucket { @override String toString() { - return '$bucketName'; + return bucketName; } } diff --git a/lib/logic/models/hive/backblaze_credential.dart b/lib/logic/models/hive/backblaze_credential.dart index b9c06364..3f0f3ea6 100644 --- a/lib/logic/models/hive/backblaze_credential.dart +++ b/lib/logic/models/hive/backblaze_credential.dart @@ -23,7 +23,7 @@ class BackblazeCredential { } String encodedBackblazeKey(String? keyId, String? applicationKey) { - String _apiKey = '$keyId:$applicationKey'; - String encodedApiKey = base64.encode(utf8.encode(_apiKey)); + String apiKey = '$keyId:$applicationKey'; + String encodedApiKey = base64.encode(utf8.encode(apiKey)); return encodedApiKey; } diff --git a/lib/logic/models/hive/server_details.dart b/lib/logic/models/hive/server_details.dart index e9bb5f38..a5e5a575 100644 --- a/lib/logic/models/hive/server_details.dart +++ b/lib/logic/models/hive/server_details.dart @@ -32,7 +32,7 @@ class ServerHostingDetails { @HiveField(5) final String apiToken; - @HiveField(6, defaultValue: ServerProvider.Hetzner) + @HiveField(6, defaultValue: ServerProvider.hetzner) final ServerProvider provider; ServerHostingDetails copyWith({DateTime? startTime}) { @@ -47,6 +47,7 @@ class ServerHostingDetails { ); } + @override String toString() => id.toString(); } @@ -66,7 +67,7 @@ class ServerVolume { @HiveType(typeId: 101) enum ServerProvider { @HiveField(0) - Unknown, + unknown, @HiveField(1) - Hetzner, + hetzner, } diff --git a/lib/logic/models/hive/server_details.g.dart b/lib/logic/models/hive/server_details.g.dart index f52e6b37..f10628e7 100644 --- a/lib/logic/models/hive/server_details.g.dart +++ b/lib/logic/models/hive/server_details.g.dart @@ -23,7 +23,7 @@ class ServerHostingDetailsAdapter extends TypeAdapter { volume: fields[4] as ServerVolume, apiToken: fields[5] as String, provider: fields[6] == null - ? ServerProvider.Hetzner + ? ServerProvider.hetzner : fields[6] as ServerProvider, startTime: fields[2] as DateTime?, ); @@ -105,21 +105,21 @@ class ServerProviderAdapter extends TypeAdapter { ServerProvider read(BinaryReader reader) { switch (reader.readByte()) { case 0: - return ServerProvider.Unknown; + return ServerProvider.unknown; case 1: - return ServerProvider.Hetzner; + return ServerProvider.hetzner; default: - return ServerProvider.Unknown; + return ServerProvider.unknown; } } @override void write(BinaryWriter writer, ServerProvider obj) { switch (obj) { - case ServerProvider.Unknown: + case ServerProvider.unknown: writer.writeByte(0); break; - case ServerProvider.Hetzner: + case ServerProvider.hetzner: writer.writeByte(1); break; } diff --git a/lib/logic/models/hive/server_domain.dart b/lib/logic/models/hive/server_domain.dart index ab60019e..4fdd52c3 100644 --- a/lib/logic/models/hive/server_domain.dart +++ b/lib/logic/models/hive/server_domain.dart @@ -16,7 +16,7 @@ class ServerDomain { @HiveField(1) final String zoneId; - @HiveField(2, defaultValue: DnsProvider.Cloudflare) + @HiveField(2, defaultValue: DnsProvider.cloudflare) final DnsProvider provider; @override @@ -28,7 +28,7 @@ class ServerDomain { @HiveType(typeId: 100) enum DnsProvider { @HiveField(0) - Unknown, + unknown, @HiveField(1) - Cloudflare, + cloudflare, } diff --git a/lib/logic/models/hive/server_domain.g.dart b/lib/logic/models/hive/server_domain.g.dart index 7a3eb4de..3265db6b 100644 --- a/lib/logic/models/hive/server_domain.g.dart +++ b/lib/logic/models/hive/server_domain.g.dart @@ -20,7 +20,7 @@ class ServerDomainAdapter extends TypeAdapter { domainName: fields[0] as String, zoneId: fields[1] as String, provider: - fields[2] == null ? DnsProvider.Cloudflare : fields[2] as DnsProvider, + fields[2] == null ? DnsProvider.cloudflare : fields[2] as DnsProvider, ); } @@ -55,21 +55,21 @@ class DnsProviderAdapter extends TypeAdapter { DnsProvider read(BinaryReader reader) { switch (reader.readByte()) { case 0: - return DnsProvider.Unknown; + return DnsProvider.unknown; case 1: - return DnsProvider.Cloudflare; + return DnsProvider.cloudflare; default: - return DnsProvider.Unknown; + return DnsProvider.unknown; } } @override void write(BinaryWriter writer, DnsProvider obj) { switch (obj) { - case DnsProvider.Unknown: + case DnsProvider.unknown: writer.writeByte(0); break; - case DnsProvider.Cloudflare: + case DnsProvider.cloudflare: writer.writeByte(1); break; } diff --git a/lib/logic/models/hive/user.dart b/lib/logic/models/hive/user.dart index 8161f61b..94ea993f 100644 --- a/lib/logic/models/hive/user.dart +++ b/lib/logic/models/hive/user.dart @@ -8,7 +8,7 @@ part 'user.g.dart'; @HiveType(typeId: 1) class User extends Equatable { - User({ + const User({ required this.login, this.password, this.sshKeys = const [], @@ -22,7 +22,7 @@ class User extends Equatable { @HiveField(1) final String? password; - @HiveField(2, defaultValue: const []) + @HiveField(2, defaultValue: []) final List sshKeys; @HiveField(3, defaultValue: true) @@ -36,6 +36,7 @@ class User extends Equatable { Color get color => stringToColor(login); + @override String toString() { return '$login, ${isFoundOnServer ? 'found' : 'not found'}, ${sshKeys.length} ssh keys, note: $note'; } diff --git a/lib/logic/models/job.dart b/lib/logic/models/job.dart index 38ef2022..7ee8e418 100644 --- a/lib/logic/models/job.dart +++ b/lib/logic/models/job.dart @@ -71,7 +71,7 @@ class CreateSSHKeyJob extends Job { CreateSSHKeyJob({ required this.user, required this.publicKey, - }) : super(title: '${"jobs.create_ssh_key".tr(args: [user.login])}'); + }) : super(title: 'jobs.create_ssh_key'.tr(args: [user.login])); final User user; final String publicKey; @@ -84,7 +84,7 @@ class DeleteSSHKeyJob extends Job { DeleteSSHKeyJob({ required this.user, required this.publicKey, - }) : super(title: '${"jobs.delete_ssh_key".tr(args: [user.login])}'); + }) : super(title: 'jobs.delete_ssh_key'.tr(args: [user.login])); final User user; final String publicKey; diff --git a/lib/logic/models/json/api_token.dart b/lib/logic/models/json/api_token.dart index 60801889..867e11d5 100644 --- a/lib/logic/models/json/api_token.dart +++ b/lib/logic/models/json/api_token.dart @@ -7,12 +7,13 @@ class ApiToken { ApiToken({ required this.name, required this.date, - required this.is_caller, + required this.isCaller, }); final String name; final DateTime date; - final bool is_caller; + @JsonKey(name: 'is_caller') + final bool isCaller; factory ApiToken.fromJson(Map json) => _$ApiTokenFromJson(json); diff --git a/lib/logic/models/json/api_token.g.dart b/lib/logic/models/json/api_token.g.dart index c009f58b..b6c8b8db 100644 --- a/lib/logic/models/json/api_token.g.dart +++ b/lib/logic/models/json/api_token.g.dart @@ -9,5 +9,5 @@ part of 'api_token.dart'; ApiToken _$ApiTokenFromJson(Map json) => ApiToken( name: json['name'] as String, date: DateTime.parse(json['date'] as String), - is_caller: json['is_caller'] as bool, + isCaller: json['is_caller'] as bool, ); diff --git a/lib/logic/models/json/auto_upgrade_settings.dart b/lib/logic/models/json/auto_upgrade_settings.dart index 6007e622..77c8905d 100644 --- a/lib/logic/models/json/auto_upgrade_settings.dart +++ b/lib/logic/models/json/auto_upgrade_settings.dart @@ -8,7 +8,7 @@ class AutoUpgradeSettings extends Equatable { final bool enable; final bool allowReboot; - AutoUpgradeSettings({ + const AutoUpgradeSettings({ required this.enable, required this.allowReboot, }); diff --git a/lib/logic/models/json/recovery_token_status.dart b/lib/logic/models/json/recovery_token_status.dart index 8904f84f..6b27b028 100644 --- a/lib/logic/models/json/recovery_token_status.dart +++ b/lib/logic/models/json/recovery_token_status.dart @@ -5,7 +5,7 @@ part 'recovery_token_status.g.dart'; @JsonSerializable() class RecoveryKeyStatus extends Equatable { - RecoveryKeyStatus({ + const RecoveryKeyStatus({ required this.exists, required this.valid, this.date, diff --git a/lib/logic/models/message.dart b/lib/logic/models/message.dart index 79f21a54..176f2846 100644 --- a/lib/logic/models/message.dart +++ b/lib/logic/models/message.dart @@ -1,6 +1,6 @@ import 'package:intl/intl.dart'; -final formatter = new DateFormat('hh:mm'); +final formatter = DateFormat('hh:mm'); class Message { Message({this.text, this.type = MessageType.normal}) : time = DateTime.now(); diff --git a/lib/main.dart b/lib/main.dart index 1889f65a..6e3d181b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -85,9 +85,9 @@ class MyApp extends StatelessWidget { appSettings.isDarkModeOn ? ThemeMode.dark : ThemeMode.light, home: appSettings.isOnboardingShowing ? OnboardingPage(nextPage: InitializingPage()) - : RootPage(), + : const RootPage(), builder: (BuildContext context, Widget? widget) { - Widget error = Text('...rendering error...'); + Widget error = const Text('...rendering error...'); if (widget is Scaffold || widget is Navigator) { error = Scaffold(body: Center(child: error)); } diff --git a/lib/theming/factory/app_theme_factory.dart b/lib/theming/factory/app_theme_factory.dart index 860dd0b2..1d99c480 100644 --- a/lib/theming/factory/app_theme_factory.dart +++ b/lib/theming/factory/app_theme_factory.dart @@ -18,7 +18,7 @@ abstract class AppThemeFactory { } static Future _createAppTheme({ - bool isDark: false, + bool isDark = false, required Color fallbackColor, }) async { ColorScheme? gtkColorsScheme; @@ -39,12 +39,12 @@ abstract class AppThemeFactory { ); } - final accentColor = await SystemAccentColor(fallbackColor); + final accentColor = SystemAccentColor(fallbackColor); try { await accentColor.load(); } on MissingPluginException catch (e) { - print("_createAppTheme: ${e.message}"); + print('_createAppTheme: ${e.message}'); } final fallbackColorScheme = ColorScheme.fromSeed( diff --git a/lib/ui/components/action_button/action_button.dart b/lib/ui/components/action_button/action_button.dart index bc0393e7..e507fa0c 100644 --- a/lib/ui/components/action_button/action_button.dart +++ b/lib/ui/components/action_button/action_button.dart @@ -20,7 +20,7 @@ class ActionButton extends StatelessWidget { return TextButton( child: Text( text!, - style: isRed ? TextStyle(color: BrandColors.red1) : null, + style: isRed ? const TextStyle(color: BrandColors.red1) : null, ), onPressed: () { navigator.pop(); diff --git a/lib/ui/components/brand_bottom_sheet/brand_bottom_sheet.dart b/lib/ui/components/brand_bottom_sheet/brand_bottom_sheet.dart index d5718181..22430d06 100644 --- a/lib/ui/components/brand_bottom_sheet/brand_bottom_sheet.dart +++ b/lib/ui/components/brand_bottom_sheet/brand_bottom_sheet.dart @@ -44,9 +44,9 @@ class BrandBottomSheet extends StatelessWidget { ), ), ), - SizedBox(height: 6), + const SizedBox(height: 6), ClipRRect( - borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), child: ConstrainedBox( constraints: BoxConstraints(maxHeight: mainHeight), child: innerWidget, diff --git a/lib/ui/components/brand_button/brand_button.dart b/lib/ui/components/brand_button/brand_button.dart index b09b0442..398e9cc2 100644 --- a/lib/ui/components/brand_button/brand_button.dart +++ b/lib/ui/components/brand_button/brand_button.dart @@ -14,7 +14,7 @@ class BrandButton { assert(text == null || child == null, 'required title or child'); assert(text != null || child != null, 'required title or child'); return ConstrainedBox( - constraints: BoxConstraints( + constraints: const BoxConstraints( minHeight: 48, minWidth: double.infinity, ), @@ -33,7 +33,7 @@ class BrandButton { required String title, }) => ConstrainedBox( - constraints: BoxConstraints( + constraints: const BoxConstraints( minHeight: 48, minWidth: double.infinity, ), diff --git a/lib/ui/components/brand_button/FilledButton.dart b/lib/ui/components/brand_button/filled_button.dart similarity index 100% rename from lib/ui/components/brand_button/FilledButton.dart rename to lib/ui/components/brand_button/filled_button.dart index 0ac34a42..06822d4f 100644 --- a/lib/ui/components/brand_button/FilledButton.dart +++ b/lib/ui/components/brand_button/filled_button.dart @@ -16,11 +16,11 @@ class FilledButton extends StatelessWidget { Widget build(BuildContext context) { return ElevatedButton( onPressed: onPressed, - child: child ?? Text(title ?? ''), style: ElevatedButton.styleFrom( onPrimary: Theme.of(context).colorScheme.onPrimary, primary: Theme.of(context).colorScheme.primary, ).copyWith(elevation: ButtonStyleButton.allOrNull(0.0)), + child: child ?? Text(title ?? ''), ); } } diff --git a/lib/ui/components/brand_cards/brand_cards.dart b/lib/ui/components/brand_cards/brand_cards.dart index 777663cf..b290c00e 100644 --- a/lib/ui/components/brand_cards/brand_cards.dart +++ b/lib/ui/components/brand_cards/brand_cards.dart @@ -3,22 +3,22 @@ import 'package:selfprivacy/config/brand_colors.dart'; class BrandCards { static Widget big({required Widget child}) => _BrandCard( - child: child, - padding: EdgeInsets.symmetric( + padding: const EdgeInsets.symmetric( horizontal: 20, vertical: 15, ), shadow: bigShadow, borderRadius: BorderRadius.circular(20), + child: child, ); static Widget small({required Widget child}) => _BrandCard( - child: child, - padding: EdgeInsets.symmetric( + padding: const EdgeInsets.symmetric( horizontal: 15, vertical: 10, ), shadow: bigShadow, borderRadius: BorderRadius.circular(10), + child: child, ); static Widget outlined({required Widget child}) => _OutlinedCard( child: child, @@ -70,7 +70,7 @@ class _OutlinedCard extends StatelessWidget { return Card( elevation: 0.0, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(12)), + borderRadius: const BorderRadius.all(Radius.circular(12)), side: BorderSide( color: Theme.of(context).colorScheme.outline, ), @@ -92,19 +92,19 @@ class _FilledCard extends StatelessWidget { Widget build(BuildContext context) { return Card( elevation: 0.0, - shape: RoundedRectangleBorder( + shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(12)), ), clipBehavior: Clip.antiAlias, - child: child, color: Theme.of(context).colorScheme.surfaceVariant, + child: child, ); } } final bigShadow = [ BoxShadow( - offset: Offset(0, 4), + 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 ca36305e..9f136466 100644 --- a/lib/ui/components/brand_header/brand_header.dart +++ b/lib/ui/components/brand_header/brand_header.dart @@ -6,7 +6,7 @@ import 'package:selfprivacy/ui/components/pre_styled_buttons/pre_styled_buttons. class BrandHeader extends StatelessWidget { const BrandHeader({ Key? key, - this.title = "", + this.title = '', this.hasBackButton = false, this.hasFlashButton = false, this.onBackButtonPressed, @@ -25,22 +25,20 @@ class BrandHeader extends StatelessWidget { padding: EdgeInsets.only( left: hasBackButton ? 1 : 15, ), - child: Container( - child: Row( - children: [ - if (hasBackButton) ...[ - IconButton( - icon: Icon(BrandIcons.arrow_left), - onPressed: - onBackButtonPressed ?? () => Navigator.of(context).pop(), - ), - SizedBox(width: 10), - ], - BrandText.h4(title), - Spacer(), - if (hasFlashButton) PreStyledButtons.flash(), + child: Row( + children: [ + if (hasBackButton) ...[ + IconButton( + icon: const Icon(BrandIcons.arrowLeft), + onPressed: + onBackButtonPressed ?? () => Navigator.of(context).pop(), + ), + const SizedBox(width: 10), ], - ), + BrandText.h4(title), + const Spacer(), + if (hasFlashButton) PreStyledButtons.flash(), + ], ), ); } diff --git a/lib/ui/components/brand_hero_screen/brand_hero_screen.dart b/lib/ui/components/brand_hero_screen/brand_hero_screen.dart index e6163cd8..105267a9 100644 --- a/lib/ui/components/brand_hero_screen/brand_hero_screen.dart +++ b/lib/ui/components/brand_hero_screen/brand_hero_screen.dart @@ -28,7 +28,7 @@ class BrandHeroScreen extends StatelessWidget { return SafeArea( child: Scaffold( appBar: PreferredSize( - preferredSize: Size.fromHeight(52.0), + preferredSize: const Size.fromHeight(52.0), child: BrandHeader( title: headerTitle, hasBackButton: hasBackButton, @@ -37,29 +37,29 @@ class BrandHeroScreen extends StatelessWidget { ), ), body: ListView( - padding: EdgeInsets.all(16.0), + padding: const EdgeInsets.all(16.0), children: [ if (heroIcon != null) Container( + alignment: Alignment.bottomLeft, child: Icon( heroIcon, size: 48.0, ), - alignment: Alignment.bottomLeft, ), - SizedBox(height: 8.0), + const SizedBox(height: 8.0), if (heroTitle != null) Text( heroTitle!, style: Theme.of(context).textTheme.headlineMedium, textAlign: TextAlign.start, ), - SizedBox(height: 8.0), + const SizedBox(height: 8.0), if (heroSubtitle != null) Text(heroSubtitle!, style: Theme.of(context).textTheme.bodyMedium, textAlign: TextAlign.start), - SizedBox(height: 16.0), + const SizedBox(height: 16.0), ...children, ], ), diff --git a/lib/ui/components/brand_icons/brand_icons.dart b/lib/ui/components/brand_icons/brand_icons.dart index b66f8bf6..ea8e51a0 100644 --- a/lib/ui/components/brand_icons/brand_icons.dart +++ b/lib/ui/components/brand_icons/brand_icons.dart @@ -67,7 +67,7 @@ class BrandIcons { IconData(0xe815, fontFamily: _kFontFam, fontPackage: _kFontPkg); static const IconData upload = IconData(0xe816, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData arrow_left = + static const IconData arrowLeft = IconData(0xe818, fontFamily: _kFontFam, fontPackage: _kFontPkg); static const IconData shape = IconData(0xe819, fontFamily: _kFontFam, fontPackage: _kFontPkg); diff --git a/lib/ui/components/brand_loader/brand_loader.dart b/lib/ui/components/brand_loader/brand_loader.dart index 9cd5b571..ea9f754b 100644 --- a/lib/ui/components/brand_loader/brand_loader.dart +++ b/lib/ui/components/brand_loader/brand_loader.dart @@ -12,8 +12,8 @@ class _HorizontalLoader extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ Text('basis.wait'.tr()), - SizedBox(height: 10), - LinearProgressIndicator(minHeight: 3), + const SizedBox(height: 10), + const LinearProgressIndicator(minHeight: 3), ], ); } diff --git a/lib/ui/components/brand_radio/brand_radio.dart b/lib/ui/components/brand_radio/brand_radio.dart index 3ae64bcf..21cc2779 100644 --- a/lib/ui/components/brand_radio/brand_radio.dart +++ b/lib/ui/components/brand_radio/brand_radio.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:selfprivacy/config/brand_colors.dart'; class BrandRadio extends StatelessWidget { - BrandRadio({ + const BrandRadio({ Key? key, required this.isChecked, }) : super(key: key); @@ -15,7 +15,7 @@ class BrandRadio extends StatelessWidget { height: 20, width: 20, alignment: Alignment.center, - padding: EdgeInsets.all(2), + padding: const EdgeInsets.all(2), decoration: BoxDecoration( shape: BoxShape.circle, border: _getBorder(), @@ -24,7 +24,7 @@ class BrandRadio extends StatelessWidget { ? Container( height: 10, width: 10, - decoration: BoxDecoration( + decoration: const BoxDecoration( shape: BoxShape.circle, color: BrandColors.primary, ), diff --git a/lib/ui/components/brand_radio_tile/brand_radio_tile.dart b/lib/ui/components/brand_radio_tile/brand_radio_tile.dart index 4f979a47..b2784799 100644 --- a/lib/ui/components/brand_radio_tile/brand_radio_tile.dart +++ b/lib/ui/components/brand_radio_tile/brand_radio_tile.dart @@ -21,13 +21,13 @@ class BrandRadioTile extends StatelessWidget { onTap: onPress, behavior: HitTestBehavior.translucent, child: Padding( - padding: EdgeInsets.all(2), + padding: const EdgeInsets.all(2), child: Row( children: [ BrandRadio( isChecked: isChecked, ), - SizedBox(width: 9), + const SizedBox(width: 9), BrandText.h5(text) ], ), diff --git a/lib/ui/components/brand_span_button/brand_span_button.dart b/lib/ui/components/brand_span_button/brand_span_button.dart index 6401a821..6fdef622 100644 --- a/lib/ui/components/brand_span_button/brand_span_button.dart +++ b/lib/ui/components/brand_span_button/brand_span_button.dart @@ -11,7 +11,7 @@ class BrandSpanButton extends TextSpan { }) : super( recognizer: TapGestureRecognizer()..onTap = onTap, text: text, - style: (style ?? TextStyle()).copyWith(color: BrandColors.blue), + style: (style ?? const TextStyle()).copyWith(color: BrandColors.blue), ); static link({ diff --git a/lib/ui/components/brand_tab_bar/brand_tab_bar.dart b/lib/ui/components/brand_tab_bar/brand_tab_bar.dart index 3975ee25..021c75e0 100644 --- a/lib/ui/components/brand_tab_bar/brand_tab_bar.dart +++ b/lib/ui/components/brand_tab_bar/brand_tab_bar.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart'; class BrandTabBar extends StatefulWidget { - BrandTabBar({Key? key, this.controller}) : super(key: key); + const BrandTabBar({Key? key, this.controller}) : super(key: key); final TabController? controller; @override diff --git a/lib/ui/components/brand_timer/brand_timer.dart b/lib/ui/components/brand_timer/brand_timer.dart index b82df99b..2e93d415 100644 --- a/lib/ui/components/brand_timer/brand_timer.dart +++ b/lib/ui/components/brand_timer/brand_timer.dart @@ -31,7 +31,7 @@ class _BrandTimerState extends State { _timerStart() { _timeString = differenceFromStart; - timer = Timer.periodic(Duration(seconds: 1), (Timer t) { + timer = Timer.periodic(const Duration(seconds: 1), (Timer t) { var timePassed = DateTime.now().difference(widget.startDateTime); if (timePassed > widget.duration) { t.cancel(); @@ -54,7 +54,7 @@ class _BrandTimerState extends State { Widget build(BuildContext context) { return BrandText.medium( _timeString, - style: TextStyle( + style: const TextStyle( fontWeight: NamedFontWeight.demiBold, ), ); @@ -71,10 +71,10 @@ class _BrandTimerState extends State { String _durationToString(Duration duration) { var timeLeft = widget.duration - duration; - String twoDigits(int n) => n.toString().padLeft(2, "0"); + String twoDigits(int n) => n.toString().padLeft(2, '0'); String twoDigitSeconds = twoDigits(timeLeft.inSeconds); - return "timer.sec".tr(args: [twoDigitSeconds]); + return 'timer.sec'.tr(args: [twoDigitSeconds]); } @override diff --git a/lib/ui/components/dots_indicator/dots_indicator.dart b/lib/ui/components/dots_indicator/dots_indicator.dart index ccf42aa5..e5c48bb4 100644 --- a/lib/ui/components/dots_indicator/dots_indicator.dart +++ b/lib/ui/components/dots_indicator/dots_indicator.dart @@ -16,7 +16,7 @@ class DotsIndicator extends StatelessWidget { var dots = List.generate( count, (index) => Container( - margin: EdgeInsets.symmetric(horizontal: 5, vertical: 10), + margin: const EdgeInsets.symmetric(horizontal: 5, vertical: 10), height: 10, width: 10, decoration: BoxDecoration( diff --git a/lib/ui/components/error/error.dart b/lib/ui/components/error/error.dart index ed46f547..64479cf7 100644 --- a/lib/ui/components/error/error.dart +++ b/lib/ui/components/error/error.dart @@ -16,7 +16,7 @@ class BrandError extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.center, children: [ Text(error.toString()), - Text('stackTrace: '), + const Text('stackTrace: '), Text(stackTrace.toString()), ], ), diff --git a/lib/ui/components/icon_status_mask/icon_status_mask.dart b/lib/ui/components/icon_status_mask/icon_status_mask.dart index cf9fd1d7..89426968 100644 --- a/lib/ui/components/icon_status_mask/icon_status_mask.dart +++ b/lib/ui/components/icon_status_mask/icon_status_mask.dart @@ -3,7 +3,7 @@ import 'package:selfprivacy/config/brand_colors.dart'; import 'package:selfprivacy/logic/models/state_types.dart'; class IconStatusMask extends StatelessWidget { - IconStatusMask({required this.child, required this.status}); + const IconStatusMask({required this.child, required this.status}); final Icon child; final StateType status; @@ -24,8 +24,8 @@ class IconStatusMask extends StatelessWidget { } return ShaderMask( shaderCallback: (bounds) => LinearGradient( - begin: Alignment(-1, -0.8), - end: Alignment(0.9, 0.9), + begin: const Alignment(-1, -0.8), + end: const Alignment(0.9, 0.9), colors: colors, tileMode: TileMode.mirror, ).createShader(bounds), diff --git a/lib/ui/components/jobs_content/jobs_content.dart b/lib/ui/components/jobs_content/jobs_content.dart index 1859cd70..cad2d7c4 100644 --- a/lib/ui/components/jobs_content/jobs_content.dart +++ b/lib/ui/components/jobs_content/jobs_content.dart @@ -24,19 +24,19 @@ class JobsContent extends StatelessWidget { var installationState = context.read().state; if (state is JobsStateEmpty) { widgets = [ - SizedBox(height: 80), + const SizedBox(height: 80), Center(child: BrandText.body1('jobs.empty'.tr())), ]; if (installationState is ServerInstallationFinished) { widgets = [ ...widgets, - SizedBox(height: 80), + const SizedBox(height: 80), BrandButton.rised( onPressed: () => context.read().upgradeServer(), text: 'jobs.upgradeServer'.tr(), ), - SizedBox(height: 10), + const SizedBox(height: 10), BrandButton.text( onPressed: () { var nav = getIt(); @@ -61,7 +61,7 @@ class JobsContent extends StatelessWidget { } } else if (state is JobsStateLoading) { widgets = [ - SizedBox(height: 80), + const SizedBox(height: 80), BrandLoader.horizontal(), ]; } else if (state is JobsStateWithJobs) { @@ -75,7 +75,7 @@ class JobsContent extends StatelessWidget { child: Text(j.title), ), ), - SizedBox(width: 10), + const SizedBox(width: 10), ElevatedButton( style: ElevatedButton.styleFrom( primary: BrandColors.red1, @@ -91,7 +91,7 @@ class JobsContent extends StatelessWidget { ), ) .toList(), - SizedBox(height: 20), + const SizedBox(height: 20), BrandButton.rised( onPressed: () => context.read().applyAll(), text: 'jobs.start'.tr(), @@ -101,13 +101,13 @@ class JobsContent extends StatelessWidget { return ListView( padding: paddingH15V0, children: [ - SizedBox(height: 15), + const SizedBox(height: 15), Center( child: BrandText.h2( 'jobs.title'.tr(), ), ), - SizedBox(height: 20), + const SizedBox(height: 20), ...widgets ], ); 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 a2eac28c..8bc3ec32 100644 --- a/lib/ui/components/not_ready_card/not_ready_card.dart +++ b/lib/ui/components/not_ready_card/not_ready_card.dart @@ -11,7 +11,7 @@ class NotReadyCard extends StatelessWidget { @override Widget build(BuildContext context) { return Container( - padding: EdgeInsets.symmetric(horizontal: 15, vertical: 10), + padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 10), decoration: BoxDecoration( borderRadius: BorderRadius.circular(15), color: BrandColors.gray6), child: RichText( @@ -19,7 +19,7 @@ class NotReadyCard extends StatelessWidget { children: [ TextSpan( text: 'not_ready_card.1'.tr(), - style: TextStyle(color: BrandColors.white), + style: const TextStyle(color: BrandColors.white), ), WidgetSpan( child: Padding( @@ -44,7 +44,7 @@ class NotReadyCard extends StatelessWidget { ), TextSpan( text: 'not_ready_card.3'.tr(), - style: TextStyle(color: BrandColors.white), + style: const TextStyle(color: BrandColors.white), ), ], ), diff --git a/lib/ui/components/one_page/one_page.dart b/lib/ui/components/one_page/one_page.dart index 9cb1afe6..30707766 100644 --- a/lib/ui/components/one_page/one_page.dart +++ b/lib/ui/components/one_page/one_page.dart @@ -18,18 +18,18 @@ class OnePage extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( appBar: PreferredSize( + preferredSize: const Size.fromHeight(52), child: Column( children: [ Container( height: 51, alignment: Alignment.center, - padding: EdgeInsets.symmetric(horizontal: 15), + padding: const EdgeInsets.symmetric(horizontal: 15), child: BrandText.h4('basis.details'.tr()), ), - BrandDivider(), + const BrandDivider(), ], ), - preferredSize: Size.fromHeight(52), ), body: child, bottomNavigationBar: SafeArea( diff --git a/lib/ui/components/pre_styled_buttons/close.dart b/lib/ui/components/pre_styled_buttons/close.dart index 13f99bce..5a9e6241 100644 --- a/lib/ui/components/pre_styled_buttons/close.dart +++ b/lib/ui/components/pre_styled_buttons/close.dart @@ -13,7 +13,7 @@ class _CloseButton extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ BrandText.h4('basis.close'.tr()), - Icon(Icons.close), + const Icon(Icons.close), ], ), ); diff --git a/lib/ui/components/pre_styled_buttons/flash.dart b/lib/ui/components/pre_styled_buttons/flash.dart index 3888af29..8a0f4b98 100644 --- a/lib/ui/components/pre_styled_buttons/flash.dart +++ b/lib/ui/components/pre_styled_buttons/flash.dart @@ -1,7 +1,7 @@ part of 'pre_styled_buttons.dart'; class _BrandFlashButton extends StatefulWidget { - _BrandFlashButton({Key? key}) : super(key: key); + const _BrandFlashButton({Key? key}) : super(key: key); @override _BrandFlashButtonState createState() => _BrandFlashButtonState(); @@ -14,8 +14,8 @@ class _BrandFlashButtonState extends State<_BrandFlashButton> @override void initState() { - _animationController = - AnimationController(vsync: this, duration: Duration(milliseconds: 800)); + _animationController = AnimationController( + vsync: this, duration: const Duration(milliseconds: 800)); _colorTween = ColorTween( begin: BrandColors.black, end: BrandColors.primary, @@ -61,7 +61,7 @@ class _BrandFlashButtonState extends State<_BrandFlashButton> onPressed: () { showBrandBottomSheet( context: context, - builder: (context) => BrandBottomSheet( + builder: (context) => const BrandBottomSheet( isExpended: true, child: JobsContent(), ), diff --git a/lib/ui/components/pre_styled_buttons/flashFab.dart b/lib/ui/components/pre_styled_buttons/flash_fab.dart similarity index 92% rename from lib/ui/components/pre_styled_buttons/flashFab.dart rename to lib/ui/components/pre_styled_buttons/flash_fab.dart index 0af5bcab..fac36a37 100644 --- a/lib/ui/components/pre_styled_buttons/flashFab.dart +++ b/lib/ui/components/pre_styled_buttons/flash_fab.dart @@ -8,7 +8,7 @@ import 'package:selfprivacy/ui/components/jobs_content/jobs_content.dart'; import 'package:selfprivacy/ui/helpers/modals.dart'; class BrandFab extends StatefulWidget { - BrandFab({Key? key}) : super(key: key); + const BrandFab({Key? key}) : super(key: key); @override _BrandFabState createState() => _BrandFabState(); @@ -21,8 +21,8 @@ class _BrandFabState extends State @override void initState() { - _animationController = - AnimationController(vsync: this, duration: Duration(milliseconds: 800)); + _animationController = AnimationController( + vsync: this, duration: const Duration(milliseconds: 800)); _colorTween = ColorTween( begin: BrandColors.black, end: BrandColors.primary, @@ -68,7 +68,7 @@ class _BrandFabState extends State onPressed: () { showBrandBottomSheet( context: context, - builder: (context) => BrandBottomSheet( + builder: (context) => const BrandBottomSheet( isExpended: true, child: JobsContent(), ), diff --git a/lib/ui/components/pre_styled_buttons/pre_styled_buttons.dart b/lib/ui/components/pre_styled_buttons/pre_styled_buttons.dart index b3be1ee8..860a83a3 100644 --- a/lib/ui/components/pre_styled_buttons/pre_styled_buttons.dart +++ b/lib/ui/components/pre_styled_buttons/pre_styled_buttons.dart @@ -18,5 +18,5 @@ class PreStyledButtons { }) => _CloseButton(onPress: onPress); - static Widget flash() => _BrandFlashButton(); + static Widget flash() => const _BrandFlashButton(); } diff --git a/lib/ui/components/progress_bar/progress_bar.dart b/lib/ui/components/progress_bar/progress_bar.dart index 41b23c86..393fcc70 100644 --- a/lib/ui/components/progress_bar/progress_bar.dart +++ b/lib/ui/components/progress_bar/progress_bar.dart @@ -6,7 +6,7 @@ import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart'; import 'package:selfprivacy/ui/components/brand_text/brand_text.dart'; class ProgressBar extends StatefulWidget { - ProgressBar({ + const ProgressBar({ Key? key, required this.steps, required this.activeIndex, @@ -49,12 +49,12 @@ class _ProgressBarState extends State { odd.insert( 0, - SizedBox( + const SizedBox( width: 10, ), ); odd.add( - SizedBox( + const SizedBox( width: 20, ), ); @@ -63,12 +63,12 @@ class _ProgressBarState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ BrandText.h2('Progress'), - SizedBox(height: 10), + const SizedBox(height: 10), Row( - children: even, mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: even, ), - SizedBox(height: 7), + const SizedBox(height: 7), Container( alignment: Alignment.centerLeft, decoration: BoxDecoration( @@ -82,23 +82,23 @@ class _ProgressBarState extends State { height: 5, decoration: BoxDecoration( borderRadius: BorderRadius.circular(5), - gradient: LinearGradient( + gradient: const LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: BrandColors.stableGradientColors, ), ), - duration: Duration( + duration: const Duration( milliseconds: 300, ), ); }, ), ), - SizedBox(height: 5), + const SizedBox(height: 5), Row( - children: odd, mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: odd, ), ], ); @@ -114,7 +114,7 @@ class _ProgressBarState extends State { style = isActive ? style!.copyWith(fontWeight: FontWeight.w700) : style; return Container( - padding: EdgeInsets.only(left: 10), + padding: const EdgeInsets.only(left: 10), height: 20, alignment: Alignment.center, child: RichText( @@ -123,9 +123,9 @@ class _ProgressBarState extends State { style: progressTextStyleLight, children: [ checked - ? WidgetSpan( + ? const WidgetSpan( child: Padding( - padding: const EdgeInsets.only(bottom: 2, right: 2), + padding: EdgeInsets.only(bottom: 2, right: 2), child: Icon(BrandIcons.check, size: 11), )) : TextSpan(text: '${index + 1}.', style: style), diff --git a/lib/ui/components/switch_block/switch_bloc.dart b/lib/ui/components/switch_block/switch_bloc.dart index cddb2859..3b5531cd 100644 --- a/lib/ui/components/switch_block/switch_bloc.dart +++ b/lib/ui/components/switch_block/switch_bloc.dart @@ -16,8 +16,8 @@ class SwitcherBlock extends StatelessWidget { @override Widget build(BuildContext context) { return Container( - padding: EdgeInsets.only(top: 20, bottom: 5), - decoration: BoxDecoration( + padding: const EdgeInsets.only(top: 20, bottom: 5), + decoration: const BoxDecoration( border: Border( bottom: BorderSide(width: 1, color: BrandColors.dividerColor), )), @@ -26,7 +26,7 @@ class SwitcherBlock extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Flexible(child: child), - SizedBox(width: 5), + const SizedBox(width: 5), Switch( activeColor: BrandColors.green1, activeTrackColor: BrandColors.green2, diff --git a/lib/ui/helpers/modals.dart b/lib/ui/helpers/modals.dart index 69f6b6d8..540b11ec 100644 --- a/lib/ui/helpers/modals.dart +++ b/lib/ui/helpers/modals.dart @@ -9,6 +9,6 @@ Future showBrandBottomSheet({ builder: builder, barrierColor: Colors.black45, context: context, - shadow: BoxShadow(color: Colors.transparent), + shadow: const BoxShadow(color: Colors.transparent), backgroundColor: Colors.transparent, ); diff --git a/lib/ui/pages/backup_details/backup_details.dart b/lib/ui/pages/backup_details/backup_details.dart index 8a5eb688..78d45a5b 100644 --- a/lib/ui/pages/backup_details/backup_details.dart +++ b/lib/ui/pages/backup_details/backup_details.dart @@ -72,7 +72,7 @@ class _BackupDetailsState extends State : () async { await context.read().createBackup(); }, - leading: Icon( + leading: const Icon( Icons.add_circle_outline_rounded, ), title: Text( @@ -105,7 +105,7 @@ class _BackupDetailsState extends State ), if (backupStatus == BackupStatusEnum.error) ListTile( - leading: Icon( + leading: const Icon( Icons.error_outline, color: BrandColors.red1, ), @@ -117,7 +117,7 @@ class _BackupDetailsState extends State ], ), ), - SizedBox(height: 16), + const SizedBox(height: 16), // Card with a list of existing backups // Each list item has a date // When clicked, starts the restore action @@ -128,7 +128,7 @@ class _BackupDetailsState extends State crossAxisAlignment: CrossAxisAlignment.start, children: [ ListTile( - leading: Icon( + leading: const Icon( Icons.refresh, ), title: Text( @@ -136,12 +136,12 @@ class _BackupDetailsState extends State style: Theme.of(context).textTheme.headline6, ), ), - Divider( + const Divider( height: 1.0, ), if (backups.isEmpty) ListTile( - leading: Icon( + leading: const Icon( Icons.error_outline, ), title: Text('providers.backup.no_backups'.tr()), @@ -174,11 +174,7 @@ class _BackupDetailsState extends State )); }, title: Text( - MaterialLocalizations.of(context) - .formatShortDate(backup.time) + - ' ' + - TimeOfDay.fromDateTime(backup.time) - .format(context), + '${MaterialLocalizations.of(context).formatShortDate(backup.time)} ${TimeOfDay.fromDateTime(backup.time).format(context)}', ), ); }).toList(), @@ -186,7 +182,7 @@ class _BackupDetailsState extends State ], ), ), - SizedBox(height: 16), + const SizedBox(height: 16), BrandCards.outlined( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -203,7 +199,7 @@ class _BackupDetailsState extends State if (providerState != StateType.uninitialized) Column( children: [ - Divider( + const Divider( height: 1.0, ), ListTile( @@ -218,7 +214,7 @@ class _BackupDetailsState extends State .forceUpdateBackups() }, ), - Divider( + const Divider( height: 1.0, ), ListTile( diff --git a/lib/ui/pages/dns_details/dns_details.dart b/lib/ui/pages/dns_details/dns_details.dart index 3b5fd555..b93de2f9 100644 --- a/lib/ui/pages/dns_details/dns_details.dart +++ b/lib/ui/pages/dns_details/dns_details.dart @@ -16,26 +16,26 @@ class _DnsDetailsPageState extends State { Widget _getStateCard(DnsRecordsStatus dnsState, Function fixCallback) { var description = ''; var subtitle = ''; - var icon = Icon( + var icon = const Icon( Icons.check, color: Colors.green, ); switch (dnsState) { case DnsRecordsStatus.uninitialized: description = 'providers.domain.states.uninitialized'.tr(); - icon = Icon( + icon = const Icon( Icons.refresh, ); break; case DnsRecordsStatus.refreshing: description = 'providers.domain.states.refreshing'.tr(); - icon = Icon( + icon = const Icon( Icons.refresh, ); break; case DnsRecordsStatus.good: description = 'providers.domain.states.ok'.tr(); - icon = Icon( + icon = const Icon( Icons.check, color: Colors.green, ); @@ -43,7 +43,7 @@ class _DnsDetailsPageState extends State { case DnsRecordsStatus.error: description = 'providers.domain.states.error'.tr(); subtitle = 'providers.domain.states.error_subtitle'.tr(); - icon = Icon( + icon = const Icon( Icons.error, color: Colors.red, ); @@ -104,7 +104,7 @@ class _DnsDetailsPageState extends State { ), ), - SizedBox(height: 16.0), + const SizedBox(height: 16.0), // Outlined card with a list of A records and their // status. BrandCards.outlined( @@ -128,7 +128,7 @@ class _DnsDetailsPageState extends State { .map( (dnsRecord) => Column( children: [ - Divider( + const Divider( height: 1.0, ), ListTile( @@ -162,7 +162,7 @@ class _DnsDetailsPageState extends State { ], ), ), - SizedBox(height: 16.0), + const SizedBox(height: 16.0), BrandCards.outlined( child: Column( children: [ @@ -184,7 +184,7 @@ class _DnsDetailsPageState extends State { .map( (dnsRecord) => Column( children: [ - Divider( + const Divider( height: 1.0, ), ListTile( diff --git a/lib/ui/pages/more/about/about.dart b/lib/ui/pages/more/about/about.dart index b927c785..53faa191 100644 --- a/lib/ui/pages/more/about/about.dart +++ b/lib/ui/pages/more/about/about.dart @@ -11,14 +11,12 @@ class AboutPage extends StatelessWidget { return SafeArea( child: Scaffold( appBar: PreferredSize( + preferredSize: const Size.fromHeight(52), child: BrandHeader( title: 'more.about_project'.tr(), hasBackButton: true), - preferredSize: Size.fromHeight(52), ), - body: Container( - child: BrandMarkdown( - fileName: 'about', - ), + body: const BrandMarkdown( + fileName: 'about', ), ), ); diff --git a/lib/ui/pages/more/app_settings/app_setting.dart b/lib/ui/pages/more/app_settings/app_setting.dart index 37bbe83e..f490bc11 100644 --- a/lib/ui/pages/more/app_settings/app_setting.dart +++ b/lib/ui/pages/more/app_settings/app_setting.dart @@ -28,17 +28,17 @@ class _AppSettingsPageState extends State { child: Builder(builder: (context) { return Scaffold( appBar: PreferredSize( + preferredSize: const Size.fromHeight(52), child: BrandHeader( title: 'more.settings.title'.tr(), hasBackButton: true), - preferredSize: Size.fromHeight(52), ), body: ListView( padding: paddingH15V0, children: [ - BrandDivider(), + const BrandDivider(), Container( - padding: EdgeInsets.only(top: 20, bottom: 5), - decoration: BoxDecoration( + padding: const EdgeInsets.only(top: 20, bottom: 5), + decoration: const BoxDecoration( border: Border( bottom: BorderSide(width: 1, color: BrandColors.dividerColor), )), @@ -52,7 +52,7 @@ class _AppSettingsPageState extends State { value: 'more.settings.2'.tr(), ), ), - SizedBox(width: 5), + const SizedBox(width: 5), BrandSwitch( value: Theme.of(context).brightness == Brightness.dark, onChanged: (value) => context @@ -63,8 +63,8 @@ class _AppSettingsPageState extends State { ), ), Container( - padding: EdgeInsets.only(top: 20, bottom: 5), - decoration: BoxDecoration( + padding: const EdgeInsets.only(top: 20, bottom: 5), + decoration: const BoxDecoration( border: Border( bottom: BorderSide(width: 1, color: BrandColors.dividerColor), )), @@ -78,14 +78,14 @@ class _AppSettingsPageState extends State { value: 'more.settings.4'.tr(), ), ), - SizedBox(width: 5), + const SizedBox(width: 5), ElevatedButton( style: ElevatedButton.styleFrom( primary: BrandColors.red1, ), child: Text( 'basis.reset'.tr(), - style: TextStyle( + style: const TextStyle( color: BrandColors.white, fontWeight: NamedFontWeight.demiBold, ), @@ -131,8 +131,8 @@ class _AppSettingsPageState extends State { var isDisabled = context.watch().state.serverDetails == null; return Container( - padding: EdgeInsets.only(top: 20, bottom: 5), - decoration: BoxDecoration( + padding: const EdgeInsets.only(top: 20, bottom: 5), + decoration: const BoxDecoration( border: Border( bottom: BorderSide(width: 1, color: BrandColors.dividerColor), )), @@ -146,18 +146,11 @@ class _AppSettingsPageState extends State { value: 'more.settings.6'.tr(), ), ), - SizedBox(width: 5), + const SizedBox(width: 5), ElevatedButton( style: ElevatedButton.styleFrom( primary: BrandColors.red1, ), - child: Text( - 'basis.delete'.tr(), - style: TextStyle( - color: BrandColors.white, - fontWeight: NamedFontWeight.demiBold, - ), - ), onPressed: isDisabled ? null : () { @@ -177,7 +170,8 @@ class _AppSettingsPageState extends State { builder: (context) { return Container( alignment: Alignment.center, - child: CircularProgressIndicator(), + child: + const CircularProgressIndicator(), ); }); await context @@ -193,6 +187,13 @@ class _AppSettingsPageState extends State { }, ); }, + child: Text( + 'basis.delete'.tr(), + style: const TextStyle( + color: BrandColors.white, + fontWeight: NamedFontWeight.demiBold, + ), + ), ), ], ), @@ -220,9 +221,9 @@ class _TextColumn extends StatelessWidget { title, style: TextStyle(color: hasWarning ? BrandColors.warning : null), ), - SizedBox(height: 5), + const SizedBox(height: 5), BrandText.body1(value, - style: TextStyle( + style: const TextStyle( fontSize: 13, height: 1.53, color: BrandColors.gray1, diff --git a/lib/ui/pages/more/console/console.dart b/lib/ui/pages/more/console/console.dart index 2e129f79..984f20b3 100644 --- a/lib/ui/pages/more/console/console.dart +++ b/lib/ui/pages/more/console/console.dart @@ -35,13 +35,13 @@ class _ConsoleState extends State { return SafeArea( child: Scaffold( appBar: PreferredSize( + preferredSize: const Size.fromHeight(53), child: Column( - children: [ + children: const [ BrandHeader(title: 'Console', hasBackButton: true), BrandDivider(), ], ), - preferredSize: Size.fromHeight(53), ), body: FutureBuilder( future: getIt.allReady(), @@ -53,7 +53,7 @@ class _ConsoleState extends State { reverse: true, shrinkWrap: true, children: [ - SizedBox(height: 20), + const SizedBox(height: 20), ...UnmodifiableListView(messages .map((message) { var isError = message.type == MessageType.warning; @@ -84,7 +84,7 @@ class _ConsoleState extends State { return Column( mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, - children: [ + children: const [ Text('Waiting for initialisation'), SizedBox( height: 16, diff --git a/lib/ui/pages/more/info/info.dart b/lib/ui/pages/more/info/info.dart index ef0628a6..639fab4e 100644 --- a/lib/ui/pages/more/info/info.dart +++ b/lib/ui/pages/more/info/info.dart @@ -14,14 +14,14 @@ class InfoPage extends StatelessWidget { return SafeArea( child: Scaffold( appBar: PreferredSize( + preferredSize: const Size.fromHeight(52), child: BrandHeader(title: 'more.about_app'.tr(), hasBackButton: true), - preferredSize: Size.fromHeight(52), ), body: ListView( padding: paddingH15V0, children: [ - BrandDivider(), - SizedBox(height: 10), + const BrandDivider(), + const SizedBox(height: 10), FutureBuilder( future: _version(), builder: (context, snapshot) { diff --git a/lib/ui/pages/more/more.dart b/lib/ui/pages/more/more.dart index d91ee769..790170c5 100644 --- a/lib/ui/pages/more/more.dart +++ b/lib/ui/pages/more/more.dart @@ -27,11 +27,11 @@ class MorePage extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( appBar: PreferredSize( + preferredSize: const Size.fromHeight(52), child: BrandHeader( title: 'basis.more'.tr(), hasFlashButton: true, ), - preferredSize: Size.fromHeight(52), ), body: ListView( children: [ @@ -39,7 +39,7 @@ class MorePage extends StatelessWidget { padding: paddingH15V0, child: Column( children: [ - BrandDivider(), + const BrandDivider(), _NavItem( title: 'more.configuration_wizard'.tr(), iconData: BrandIcons.triangle, @@ -48,27 +48,27 @@ class MorePage extends StatelessWidget { _NavItem( title: 'more.settings.title'.tr(), iconData: BrandIcons.settings, - goTo: AppSettingsPage(), + goTo: const AppSettingsPage(), ), _NavItem( title: 'more.about_project'.tr(), iconData: BrandIcons.engineer, - goTo: AboutPage(), + goTo: const AboutPage(), ), _NavItem( title: 'more.about_app'.tr(), iconData: BrandIcons.fire, - goTo: InfoPage(), + goTo: const InfoPage(), ), _NavItem( title: 'more.onboarding'.tr(), iconData: BrandIcons.start, - goTo: OnboardingPage(nextPage: RootPage()), + goTo: const OnboardingPage(nextPage: RootPage()), ), _NavItem( title: 'more.console'.tr(), iconData: BrandIcons.terminal, - goTo: Console(), + goTo: const Console(), ), _NavItem( isEnabled: context.read().state @@ -131,8 +131,8 @@ class _MoreMenuItem extends StatelessWidget { @override Widget build(BuildContext context) { return Container( - padding: EdgeInsets.symmetric(vertical: 24), - decoration: BoxDecoration( + padding: const EdgeInsets.symmetric(vertical: 24), + decoration: const BoxDecoration( border: Border( bottom: BorderSide( width: 1.0, @@ -148,7 +148,7 @@ class _MoreMenuItem extends StatelessWidget { color: isActive ? null : Colors.grey, ), ), - Spacer(), + const Spacer(), SizedBox( width: 56, child: Icon( diff --git a/lib/ui/pages/onboarding/onboarding.dart b/lib/ui/pages/onboarding/onboarding.dart index 6416a5b0..eecd55c8 100644 --- a/lib/ui/pages/onboarding/onboarding.dart +++ b/lib/ui/pages/onboarding/onboarding.dart @@ -53,11 +53,11 @@ class _OnboardingPageState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox(height: 30), + const SizedBox(height: 30), BrandText.h2( 'onboarding.page1_title'.tr(), ), - SizedBox(height: 20), + const SizedBox(height: 20), BrandText.body2('onboarding.page1_text'.tr()), Flexible( child: Center( @@ -75,13 +75,13 @@ class _OnboardingPageState extends State { onPressed: () { pageController.animateToPage( 1, - duration: Duration(milliseconds: 300), + duration: const Duration(milliseconds: 300), curve: Curves.easeIn, ); }, text: 'basis.next'.tr(), ), - SizedBox(height: 30), + const SizedBox(height: 30), ], ), ); @@ -94,11 +94,11 @@ class _OnboardingPageState extends State { ), child: Column( children: [ - SizedBox(height: 30), + const SizedBox(height: 30), BrandText.h2('onboarding.page2_title'.tr()), - SizedBox(height: 20), + const SizedBox(height: 20), BrandText.body2('onboarding.page2_text'.tr()), - SizedBox(height: 20), + const SizedBox(height: 20), Center( child: Image.asset( _fileName( @@ -131,7 +131,7 @@ class _OnboardingPageState extends State { }, text: 'basis.got_it'.tr(), ), - SizedBox(height: 30), + const SizedBox(height: 30), ], ), ); diff --git a/lib/ui/pages/providers/providers.dart b/lib/ui/pages/providers/providers.dart index 333ee66b..754e7866 100644 --- a/lib/ui/pages/providers/providers.dart +++ b/lib/ui/pages/providers/providers.dart @@ -21,7 +21,7 @@ import 'package:selfprivacy/utils/route_transitions/basic.dart'; var navigatorKey = GlobalKey(); class ProvidersPage extends StatefulWidget { - ProvidersPage({Key? key}) : super(key: key); + const ProvidersPage({Key? key}) : super(key: key); @override _ProvidersPageState createState() => _ProvidersPageState(); @@ -49,7 +49,7 @@ class _ProvidersPageState extends State { final cards = ProviderType.values .map( (type) => Padding( - padding: EdgeInsets.only(bottom: 30), + padding: const EdgeInsets.only(bottom: 30), child: _Card( provider: ProviderModel( state: isReady @@ -67,18 +67,18 @@ class _ProvidersPageState extends State { .toList(); return Scaffold( appBar: PreferredSize( + preferredSize: const Size.fromHeight(52), child: BrandHeader( title: 'providers.page_title'.tr(), hasFlashButton: true, ), - preferredSize: Size.fromHeight(52), ), body: ListView( padding: paddingH15V0, children: [ if (!isReady) ...[ - NotReadyCard(), - SizedBox(height: 24), + const NotReadyCard(), + const SizedBox(height: 24), ], ...cards, ], @@ -111,7 +111,7 @@ class _Card extends StatelessWidget { stableText = 'providers.server.status'.tr(); onTap = () => showBrandBottomSheet( context: context, - builder: (context) => BrandBottomSheet( + builder: (context) => const BrandBottomSheet( isExpended: true, child: ServerDetailsScreen(), ), @@ -132,7 +132,7 @@ class _Card extends StatelessWidget { stableText = 'providers.backup.status'.tr(); onTap = () => Navigator.of(context).push(materialRoute( - BackupDetails(), + const BackupDetails(), )); break; } @@ -146,12 +146,12 @@ class _Card extends StatelessWidget { status: provider.state, child: Icon(provider.icon, size: 30, color: Colors.white), ), - SizedBox(height: 10), + const SizedBox(height: 10), BrandText.h2(title), - SizedBox(height: 10), + const SizedBox(height: 10), if (message != null) ...[ BrandText.body2(message), - SizedBox(height: 10), + const SizedBox(height: 10), ], if (provider.state == StateType.stable) BrandText.body2(stableText), ], diff --git a/lib/ui/pages/rootRoute.dart b/lib/ui/pages/root_route.dart similarity index 92% rename from lib/ui/pages/rootRoute.dart rename to lib/ui/pages/root_route.dart index eabb0a10..d66d73c3 100644 --- a/lib/ui/pages/rootRoute.dart +++ b/lib/ui/pages/root_route.dart @@ -7,7 +7,7 @@ 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 '../components/pre_styled_buttons/flashFab.dart'; +import '../components/pre_styled_buttons/flash_fab.dart'; class RootPage extends StatefulWidget { const RootPage({Key? key}) : super(key: key); @@ -42,7 +42,7 @@ class _RootPageState extends State create: (_) => ChangeTab(tabController.animateTo), child: TabBarView( controller: tabController, - children: [ + children: const [ ProvidersPage(), ServicesPage(), UsersPage(), @@ -53,7 +53,7 @@ class _RootPageState extends State bottomNavigationBar: BrandTabBar( controller: tabController, ), - floatingActionButton: BrandFab(), + floatingActionButton: const BrandFab(), ), ); } diff --git a/lib/ui/pages/server_details/chart.dart b/lib/ui/pages/server_details/chart.dart index 242df725..7829234b 100644 --- a/lib/ui/pages/server_details/chart.dart +++ b/lib/ui/pages/server_details/chart.dart @@ -19,33 +19,33 @@ class _Chart extends StatelessWidget { ]; } else if (state is HetznerMetricsLoaded) { charts = [ - Legend(color: Colors.red, text: 'CPU %'), - SizedBox(height: 20), + const Legend(color: Colors.red, text: 'CPU %'), + const SizedBox(height: 20), getCpuChart(state), Row( mainAxisAlignment: MainAxisAlignment.end, children: [ BrandText.small('Public Network interface packets per sec'), - SizedBox(width: 10), - Legend(color: Colors.red, text: 'IN'), - SizedBox(width: 5), - Legend(color: Colors.green, text: 'OUT'), + const SizedBox(width: 10), + const Legend(color: Colors.red, text: 'IN'), + const SizedBox(width: 5), + const Legend(color: Colors.green, text: 'OUT'), ], ), - SizedBox(height: 20), + const SizedBox(height: 20), getPpsChart(state), - SizedBox(height: 1), + const SizedBox(height: 1), Row( mainAxisAlignment: MainAxisAlignment.end, children: [ BrandText.small('Public Network interface bytes per sec'), - SizedBox(width: 10), - Legend(color: Colors.red, text: 'IN'), - SizedBox(width: 5), - Legend(color: Colors.green, text: 'OUT'), + const SizedBox(width: 10), + const Legend(color: Colors.red, text: 'IN'), + const SizedBox(width: 5), + const Legend(color: Colors.green, text: 'OUT'), ], ), - SizedBox(height: 20), + const SizedBox(height: 20), getBandwidthChart(state), ]; } else { @@ -57,7 +57,7 @@ class _Chart extends StatelessWidget { child: Column( children: [ Padding( - padding: EdgeInsets.symmetric(horizontal: 20.0, vertical: 10), + padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 10), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -88,7 +88,7 @@ class _Chart extends StatelessWidget { Widget getCpuChart(HetznerMetricsLoaded state) { var data = state.cpu; - return Container( + return SizedBox( height: 200, child: CpuChart(data, state.period, state.start), ); @@ -98,7 +98,7 @@ class _Chart extends StatelessWidget { var ppsIn = state.ppsIn; var ppsOut = state.ppsOut; - return Container( + return SizedBox( height: 200, child: NetworkChart( [ppsIn, ppsOut], @@ -112,7 +112,7 @@ class _Chart extends StatelessWidget { var ppsIn = state.bandwidthIn; var ppsOut = state.bandwidthOut; - return Container( + return SizedBox( height: 200, child: NetworkChart( [ppsIn, ppsOut], @@ -138,7 +138,7 @@ class Legend extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.end, children: [ _ColoredBox(color: color), - SizedBox(width: 5), + const SizedBox(width: 5), BrandText.small(text), ], ); diff --git a/lib/ui/pages/server_details/cpu_chart.dart b/lib/ui/pages/server_details/cpu_chart.dart index fd131e61..113c797b 100644 --- a/lib/ui/pages/server_details/cpu_chart.dart +++ b/lib/ui/pages/server_details/cpu_chart.dart @@ -7,7 +7,7 @@ import 'package:selfprivacy/logic/models/hetzner_metrics.dart'; import 'package:intl/intl.dart'; class CpuChart extends StatelessWidget { - CpuChart(this.data, this.period, this.start); + const CpuChart(this.data, this.period, this.start); final List data; final Period period; @@ -54,13 +54,13 @@ class CpuChart extends StatelessWidget { return Padding( padding: const EdgeInsets.all(8.0), child: RotatedBox( + quarterTurns: 1, child: Text(bottomTitle(value.toInt()), - style: TextStyle( + style: const TextStyle( fontSize: 10, color: Colors.purple, fontWeight: FontWeight.bold, )), - quarterTurns: 1, ), ); }, @@ -71,7 +71,7 @@ class CpuChart extends StatelessWidget { sideTitles: SideTitles( getTitlesWidget: (value, titleMeta) { return Padding( - padding: EdgeInsets.only(right: 15), + padding: const EdgeInsets.only(right: 15), child: Text( value.toInt().toString(), style: progressTextStyleLight.copyWith( diff --git a/lib/ui/pages/server_details/header.dart b/lib/ui/pages/server_details/header.dart index 92abf3f8..b28a5efc 100644 --- a/lib/ui/pages/server_details/header.dart +++ b/lib/ui/pages/server_details/header.dart @@ -16,17 +16,17 @@ class _Header extends StatelessWidget { children: [ IconStatusMask( status: providerState, - child: Icon( + child: const Icon( BrandIcons.server, size: 40, color: Colors.white, ), ), - SizedBox(width: 10), + const SizedBox(width: 10), BrandText.h2('providers.server.card_title'.tr()), - Spacer(), + const Spacer(), Padding( - padding: EdgeInsets.symmetric( + padding: const EdgeInsets.symmetric( vertical: 4, horizontal: 2, ), @@ -41,12 +41,12 @@ class _Header extends StatelessWidget { break; } }, - icon: Icon(Icons.more_vert), + icon: const Icon(Icons.more_vert), itemBuilder: (BuildContext context) => [ PopupMenuItem<_PopupMenuItemType>( value: _PopupMenuItemType.setting, child: Container( - padding: EdgeInsets.only(left: 5), + padding: const EdgeInsets.only(left: 5), child: Text('basis.settings'.tr()), ), ), diff --git a/lib/ui/pages/server_details/network_charts.dart b/lib/ui/pages/server_details/network_charts.dart index 8827eb9f..4e0b385e 100644 --- a/lib/ui/pages/server_details/network_charts.dart +++ b/lib/ui/pages/server_details/network_charts.dart @@ -9,7 +9,7 @@ import 'package:selfprivacy/logic/models/hetzner_metrics.dart'; import 'package:intl/intl.dart'; class NetworkChart extends StatelessWidget { - NetworkChart( + const NetworkChart( this.listData, this.period, this.start, @@ -76,13 +76,13 @@ class NetworkChart extends StatelessWidget { return Padding( padding: const EdgeInsets.all(8.0), child: RotatedBox( + quarterTurns: 1, child: Text(bottomTitle(value.toInt()), - style: TextStyle( + style: const TextStyle( fontSize: 10, color: Colors.purple, fontWeight: FontWeight.bold, )), - quarterTurns: 1, ), ); }, @@ -94,7 +94,7 @@ class NetworkChart extends StatelessWidget { reservedSize: 50, getTitlesWidget: (value, titleMeta) { return Padding( - padding: EdgeInsets.only(right: 5), + padding: const EdgeInsets.only(right: 5), child: Text( value.toInt().toString(), style: progressTextStyleLight.copyWith( diff --git a/lib/ui/pages/server_details/server_details_screen.dart b/lib/ui/pages/server_details/server_details_screen.dart index 56e7e18d..8fe4b04e 100644 --- a/lib/ui/pages/server_details/server_details_screen.dart +++ b/lib/ui/pages/server_details/server_details_screen.dart @@ -69,25 +69,25 @@ class _ServerDetailsScreenState extends State create: (context) => ServerDetailsCubit()..check(), child: Scaffold( appBar: PreferredSize( + preferredSize: const Size.fromHeight(52), child: Column( children: [ Container( height: 51, alignment: Alignment.center, - padding: EdgeInsets.symmetric(horizontal: 15), + padding: const EdgeInsets.symmetric(horizontal: 15), child: BrandText.h4('basis.details'.tr()), ), - BrandDivider(), + const BrandDivider(), ], ), - preferredSize: Size.fromHeight(52), ), body: TabBarView( - physics: NeverScrollableScrollPhysics(), + physics: const NeverScrollableScrollPhysics(), controller: tabController, children: [ SingleChildScrollView( - physics: ClampingScrollPhysics(), + physics: const ClampingScrollPhysics(), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -103,13 +103,13 @@ class _ServerDetailsScreenState extends State ], ), ), - SizedBox(height: 10), + const SizedBox(height: 10), BlocProvider( create: (context) => HetznerMetricsCubit()..restart(), - child: _Chart(), + child: const _Chart(), ), - SizedBox(height: 20), - _TextDetails(), + const SizedBox(height: 20), + const _TextDetails(), ], ), ), diff --git a/lib/ui/pages/server_details/server_settings.dart b/lib/ui/pages/server_details/server_settings.dart index cca5fcea..f5047d6c 100644 --- a/lib/ui/pages/server_details/server_settings.dart +++ b/lib/ui/pages/server_details/server_settings.dart @@ -12,47 +12,45 @@ class _ServerSettings extends StatelessWidget { Widget build(BuildContext context) { var serverDetailsState = context.watch().state; if (serverDetailsState is ServerDetailsNotReady) { - return Text('not ready'); + return const Text('not ready'); } else if (serverDetailsState is! Loaded) { return BrandLoader.horizontal(); } return ListView( padding: paddingH15V0, children: [ - SizedBox(height: 10), + const SizedBox(height: 10), Container( height: 52, alignment: Alignment.centerLeft, - padding: EdgeInsets.only(left: 1), - child: Container( - child: Row( - children: [ - IconButton( - icon: Icon(BrandIcons.arrow_left), - onPressed: () => tabController.animateTo(0), - ), - SizedBox(width: 10), - BrandText.h4('basis.settings'.tr()), - ], - ), + padding: const EdgeInsets.only(left: 1), + child: Row( + children: [ + IconButton( + icon: const Icon(BrandIcons.arrowLeft), + onPressed: () => tabController.animateTo(0), + ), + const SizedBox(width: 10), + BrandText.h4('basis.settings'.tr()), + ], ), ), - BrandDivider(), + const BrandDivider(), SwitcherBlock( onChange: (_) {}, - child: _TextColumn( + isActive: serverDetailsState.autoUpgradeSettings.enable, + child: const _TextColumn( title: 'Allow Auto-upgrade', value: 'Wether to allow automatic packages upgrades', ), - isActive: serverDetailsState.autoUpgradeSettings.enable, ), SwitcherBlock( onChange: (_) {}, - child: _TextColumn( + isActive: serverDetailsState.autoUpgradeSettings.allowReboot, + child: const _TextColumn( title: 'Reboot after upgrade', value: 'Reboot without prompt after applying updates', ), - isActive: serverDetailsState.autoUpgradeSettings.allowReboot, ), _Button( onTap: () { @@ -83,8 +81,8 @@ class _Button extends StatelessWidget { return InkWell( onTap: onTap, child: Container( - padding: EdgeInsets.only(top: 20, bottom: 5), - decoration: BoxDecoration( + padding: const EdgeInsets.only(top: 20, bottom: 5), + decoration: const BoxDecoration( border: Border( bottom: BorderSide(width: 1, color: BrandColors.dividerColor), )), @@ -114,7 +112,7 @@ class _TextColumn extends StatelessWidget { title, style: TextStyle(color: hasWarning ? BrandColors.warning : null), ), - SizedBox(height: 5), + const SizedBox(height: 5), BrandText.body1( value, style: TextStyle( diff --git a/lib/ui/pages/server_details/text_details.dart b/lib/ui/pages/server_details/text_details.dart index 3d1c751d..397f7da1 100644 --- a/lib/ui/pages/server_details/text_details.dart +++ b/lib/ui/pages/server_details/text_details.dart @@ -17,9 +17,9 @@ class _TextDetails extends StatelessWidget { return Column( children: [ Center(child: BrandText.h3('providers.server.bottom_sheet.2'.tr())), - SizedBox(height: 10), + const SizedBox(height: 10), Table( - columnWidths: { + columnWidths: const { 0: FractionColumnWidth(.5), 1: FractionColumnWidth(.5), }, @@ -41,7 +41,7 @@ class _TextDetails extends StatelessWidget { children: [ getRowTitle('Status:'), getRowValue( - '${data.status.toString().split('.')[1].toUpperCase()}', + data.status.toString().split('.')[1].toUpperCase(), isBold: true, ), ], @@ -74,7 +74,7 @@ class _TextDetails extends StatelessWidget { children: [ getRowTitle('Price monthly:'), getRowValue( - '${data.serverType.prices[1].monthly.toString()}', + data.serverType.prices[1].monthly.toString(), ), ], ), @@ -82,17 +82,17 @@ class _TextDetails extends StatelessWidget { children: [ getRowTitle('Price hourly:'), getRowValue( - '${data.serverType.prices[1].hourly.toString()}', + data.serverType.prices[1].hourly.toString(), ), ], ), ], ), - SizedBox(height: 30), + const SizedBox(height: 30), Center(child: BrandText.h3('providers.server.bottom_sheet.3'.tr())), - SizedBox(height: 10), + const SizedBox(height: 10), Table( - columnWidths: { + columnWidths: const { 0: FractionColumnWidth(.5), 1: FractionColumnWidth(.5), }, @@ -102,7 +102,7 @@ class _TextDetails extends StatelessWidget { children: [ getRowTitle('Country:'), getRowValue( - '${data.location.country}', + data.location.country, ), ], ), @@ -120,7 +120,7 @@ class _TextDetails extends StatelessWidget { ), ], ), - SizedBox(height: 20), + const SizedBox(height: 20), ], ); } else { @@ -142,7 +142,7 @@ class _TextDetails extends StatelessWidget { return BrandText.body1( title, style: isBold - ? TextStyle( + ? const TextStyle( fontWeight: NamedFontWeight.demiBold, ) : null, diff --git a/lib/ui/pages/server_details/time_zone/lang.dart b/lib/ui/pages/server_details/time_zone/lang.dart index 4ea55019..b46fa721 100644 --- a/lib/ui/pages/server_details/time_zone/lang.dart +++ b/lib/ui/pages/server_details/time_zone/lang.dart @@ -1,431 +1,431 @@ final russian = { - "Pacific/Midway": "Мидуэй", - "Pacific/Niue": "Ниуэ", - "Pacific/Pago_Pago": "Паго-Паго", - "America/Adak": "Адак", - "Pacific/Honolulu": "Гонолулу", - "Pacific/Johnston": "Джонстон", - "Pacific/Rarotonga": "Раротонга", - "Pacific/Tahiti": "Таити", - "US/Hawaii": "Гавайи", - "Pacific/Marquesas": "Маркизские острова", - "America/Sitka": "Ситка", - "America/Anchorage": "Анкоридж", - "America/Metlakatla": "Метлакатла", - "America/Juneau": "Джуно", - "US/Alaska": "Аляска", - "America/Nome": "Ном", - "America/Yakutat": "Якутат", - "Pacific/Gambier": "Гамбье", - "America/Tijuana": "Тихуана", - "Pacific/Pitcairn": "Питкэрн", - "US/Pacific": "США/Тихий океан", - "Canada/Pacific": "США/Тихий океан", - "America/Los_Angeles": "Лос-Анджелес", - "America/Vancouver": "Ванкувер", - "America/Santa_Isabel": "Санта-Изабель", - "America/Chihuahua": "Чихуахуа", - "America/Cambridge_Bay": "Кембридж-Бэй", - "America/Inuvik": "Инувик", - "America/Boise": "Бойсе", - "America/Dawson": "Доусон", - "America/Mazatlan": "Масатлан", - "America/Dawson_Creek": "Доусон-Крик", - "US/Arizona": "Аризона", - "America/Denver": "Денвер", - "US/Mountain": "гора", - "America/Edmonton": "Эдмонтон", - "America/Yellowknife": "Йеллоунайф", - "America/Ojinaga": "Охинага", - "America/Phoenix": "Феникс", - "America/Whitehorse": "Белая лошадь", - "Canada/Mountain": "гора", - "America/Hermosillo": "Эрмосильо", - "America/Creston": "Крестон", - "America/Swift_Current": "Свифт Керрент", - "America/Tegucigalpa": "Тегусигальпа", - "America/Regina": "Регина", - "America/Rankin_Inlet": "Ранкин-Инлет", - "America/Rainy_River": "Райни-Ривер", - "America/Winnipeg": "Виннипег", - "America/North_Dakota/Center": "Северная Дакота/Центр", - "America/North_Dakota/Beulah": "Северная Дакота/Беула", - "America/Monterrey": "Монтеррей", - "America/Mexico_City": "Мехико", - "US/Central": "Центральный", - "America/Merida": "Мерида", - "America/Menominee": "Меномини", - "America/Matamoros": "Матаморос", - "America/Managua": "Манагуа", - "America/North_Dakota/New_Salem": "Северная Дакота/Нью-Салем", - "Pacific/Galapagos": "Галапагосские острова", - "America/Indiana/Tell_City": "Индиана/Телл-Сити", - "America/Indiana/Knox": "Индиана/Нокс", - "Canada/Central": "Центральный", - "America/Guatemala": "Гватемала", - "America/El_Salvador": "Сальвадор", - "America/Costa_Rica": "Коста-Рика", - "America/Chicago": "Чикаго", - "America/Belize": "Белиз", - "America/Bahia_Banderas": "Баия де Бандерас", - "America/Resolute": "Резольют", - "America/Atikokan": "Атикокан", - "America/Lima": "Лима", - "America/Bogota": "Богота", - "America/Cancun": "Канкун", - "America/Cayman": "Кайман", - "America/Detroit": "Детройт", - "America/Indiana/Indianapolis": "Индиана/Индианаполис", - "America/Eirunepe": "Эйрунепе", - "America/Grand_Turk": "Гранд-Терк", - "America/Guayaquil": "Гуаякиль", - "America/Havana": "Гавана", - "America/Indiana/Marengo": "Индиана/Маренго", - "America/Indiana/Petersburg": "Индиана/Петербург", - "America/Indiana/Vevay": "Индиана/Вева", - "America/Indiana/Vincennes": "Индиана/Винсеннес", - "America/Indiana/Winamac": "Индиана/Винамак", - "America/Iqaluit": "Икалуит", - "America/Jamaica": "Ямайка", - "America/Kentucky/Louisville": "Кентукки/Луисвилл", - "America/Nassau": "Нассау", - "America/Toronto": "Торонто", - "America/Montreal": "Монреаль", - "America/Pangnirtung": "Пангниртунг", - "America/Port-au-Prince": "Порт-о-Пренс", - "America/Kentucky/Monticello": "Кентукки/Монтичелло", - "Canada/Eastern": "Канада/Восточное", - "US/Eastern": "США/Восточное", - "America/Thunder_Bay": "Тандер-Бей", - "Pacific/Easter": "Пасха", - "America/Panama": "Панама", - "America/Nipigon": "Нипигон", - "America/Rio_Branco": "Рио-Бранко", - "America/New_York": "Нью-Йорк", - "Canada/Atlantic": "Атлантика", - "America/Kralendijk": "Кралендейк", - "America/La_Paz": "Ла-Пас", - "America/Halifax": "Галифакс", - "America/Lower_Princes": "Лоуэр-Принс-Куотер", - "America/Manaus": "Манаус", - "America/Marigot": "Мариго", - "America/Martinique": "Мартиника", - "America/Moncton": "Монктон", - "America/Guyana": "Гайана", - "America/Montserrat": "Монтсеррат", - "America/Guadeloupe": "Гваделупа", - "America/Grenada": "Гренада", - "America/Goose_Bay": "Гуз-Бей", - "America/Glace_Bay": "Глас Бэй", - "America/Curacao": "Кюрасао", - "America/Cuiaba": "Куяба", - "America/Port_of_Spain": "Порт-оф-Спейн", - "America/Porto_Velho": "Порту-Велью", - "America/Puerto_Rico": "Пуэрто-Рико", - "America/Caracas": "Каракас", - "America/Santo_Domingo": "Санто-Доминго", - "America/St_Barthelemy": "Святой Бартелеми", - "Atlantic/Bermuda": "Бермуды", - "America/St_Kitts": "Сент-Китс", - "America/St_Lucia": "Святая Люсия", - "America/St_Thomas": "Сент-Томас", - "America/St_Vincent": "Сент-Винсент", - "America/Thule": "Туле", - "America/Campo_Grande": "Кампу-Гранди", - "America/Boa_Vista": "Боа-Виста", - "America/Tortola": "Тортола", - "America/Aruba": "Аруба", - "America/Blanc-Sablon": "Блан-Саблон", - "America/Barbados": "Барбадос", - "America/Anguilla": "Ангилья", - "America/Antigua": "Антигуа", - "America/Dominica": "Доминика", - "Canada/Newfoundland": "Ньюфаундленд", - "America/St_Johns": "Сент-Джонс", - "America/Sao_Paulo": "Сан-Паулу", - "Atlantic/Stanley": "Стэнли", - "America/Miquelon": "Микелон", - "America/Argentina/Salta": "Аргентина/Сальта", - "America/Montevideo": "Монтевидео", - "America/Argentina/Rio_Gallegos": "Аргентина/Рио-Гальегос", - "America/Argentina/Mendoza": "Аргентина/Мендоса", - "America/Argentina/La_Rioja": "Аргентина/Ла-Риоха", - "America/Argentina/Jujuy": "Аргентина/Жужуй", - "Antarctica/Rothera": "Ротера", - "America/Argentina/Cordoba": "Аргентина/Кордова", - "America/Argentina/Catamarca": "Аргентина/Катамарка", - "America/Argentina/Ushuaia": "Аргентина/Ушуая", - "America/Argentina/Tucuman": "Аргентина/Тукуман", - "America/Paramaribo": "Парамарибо", - "America/Argentina/San_Luis": "Аргентина/Сан-Луис", - "America/Recife": "Ресифи", - "America/Argentina/Buenos_Aires": "Аргентина/Буэнос-Айрес", - "America/Asuncion": "Асунсьон", - "America/Maceio": "Масейо", - "America/Santarem": "Сантарен", - "America/Santiago": "Сантьяго", - "Antarctica/Palmer": "Палмер", - "America/Argentina/San_Juan": "Аргентина/Сан-Хуан", - "America/Fortaleza": "Форталеза", - "America/Cayenne": "Кайенна", - "America/Godthab": "Годтаб", - "America/Belem": "Белен", - "America/Araguaina": "Арагуайна", - "America/Bahia": "Баия", - "Atlantic/South_Georgia": "Южная_Грузия", - "America/Noronha": "Норонья", - "Atlantic/Azores": "Азорские острова", - "Atlantic/Cape_Verde": "Кабо-Верде", - "America/Scoresbysund": "Скорсбисунд", - "Africa/Accra": "Аккра", - "Atlantic/Faroe": "Фарерские острова", - "Europe/Guernsey": "Гернси", - "Africa/Dakar": "Дакар", - "Europe/Isle_of_Man": "Остров Мэн", - "Africa/Conakry": "Конакри", - "Africa/Abidjan": "Абиджан", - "Atlantic/Canary": "канарейка", - "Africa/Banjul": "Банжул", - "Europe/Jersey": "Джерси", - "Atlantic/St_Helena": "Остров Святой Елены", - "Africa/Bissau": "Бисау", - "Europe/London": "Лондон", - "Africa/Nouakchott": "Нуакшот", - "Africa/Lome": "Ломе", - "America/Danmarkshavn": "Данмарксхавн", - "Africa/Ouagadougou": "Уагадугу", - "Europe/Lisbon": "Лиссабон", - "Africa/Sao_Tome": "Сан-Томе", - "Africa/Monrovia": "Монровия", - "Atlantic/Reykjavik": "Рейкьявик", - "Antarctica/Troll": "Тролль", - "Atlantic/Madeira": "Мадейра", - "Africa/Bamako": "Бамако", - "Europe/Dublin": "Дублин", - "Africa/Freetown": "Фритаун", - "Europe/Monaco": "Монако", - "Europe/Skopje": "Скопье", - "Europe/Amsterdam": "Амстердам", - "Africa/Tunis": "Тунис", - "Arctic/Longyearbyen": "Лонгйир", - "Africa/Bangui": "Банги", - "Africa/Lagos": "Лагос", - "Africa/Douala": "Дуала", - "Africa/Libreville": "Либревиль", - "Europe/Belgrade": "Белград", - "Europe/Stockholm": "Стокгольм", - "Europe/Berlin": "Берлин", - "Europe/Zurich": "Цюрих", - "Europe/Zagreb": "Загреб", - "Europe/Warsaw": "Варшава", - "Africa/Luanda": "Луанда", - "Africa/Porto-Novo": "Порто-Ново", - "Africa/Brazzaville": "Браззавиль", - "Europe/Vienna": "Вена", - "Europe/Vatican": "Ватикан", - "Europe/Vaduz": "Вадуц", - "Europe/Tirane": "Тиран", - "Europe/Bratislava": "Братислава", - "Europe/Brussels": "Брюссель", - "Europe/Paris": "Париж", - "Europe/Sarajevo": "Сараево", - "Europe/San_Marino": "Сан-Марино", - "Europe/Rome": "Рим", - "Africa/El_Aaiun": "Эль-Аайун", - "Africa/Casablanca": "Касабланка", - "Europe/Malta": "Мальта", - "Africa/Ceuta": "Сеута", - "Europe/Gibraltar": "Гибралтар", - "Africa/Malabo": "Малабо", - "Europe/Busingen": "Бузинген", - "Africa/Ndjamena": "Нджамена", - "Europe/Andorra": "Андорра", - "Europe/Oslo": "Осло", - "Europe/Luxembourg": "Люксембург", - "Africa/Niamey": "Ниамей", - "Europe/Copenhagen": "Копенгаген", - "Europe/Madrid": "Мадрид", - "Europe/Budapest": "Будапешт", - "Africa/Algiers": "Алжир", - "Europe/Ljubljana": "Любляна", - "Europe/Podgorica": "Подгорица", - "Africa/Kinshasa": "Киншаса", - "Europe/Prague": "Прага", - "Europe/Riga": "Рига", - "Africa/Bujumbura": "Бужумбура", - "Africa/Lubumbashi": "Лубумбаши", - "Europe/Bucharest": "Бухарест", - "Africa/Blantyre": "Блантайр", - "Asia/Nicosia": "Никосия", - "Europe/Sofia": "София", - "Asia/Jerusalem": "Иерусалим", - "Europe/Tallinn": "Таллинн", - "Europe/Uzhgorod": "Ужгород", - "Africa/Lusaka": "Лусака", - "Europe/Mariehamn": "Мариехамн", - "Asia/Hebron": "Хеврон", - "Asia/Gaza": "Газа", - "Asia/Damascus": "Дамаск", - "Europe/Zaporozhye": "Запорожье", - "Asia/Beirut": "Бейрут", - "Africa/Juba": "Джуба", - "Africa/Harare": "Хараре", - "Europe/Athens": "Афины", - "Europe/Kiev": "Киев", - "Europe/Kaliningrad": "Калининград", - "Africa/Khartoum": "Хартум", - "Africa/Cairo": "Каир", - "Africa/Kigali": "Кигали", - "Asia/Amman": "Амман", - "Africa/Maputo": "Мапуту", - "Africa/Gaborone": "Габороне", - "Africa/Tripoli": "Триполи", - "Africa/Maseru": "Масеру", - "Africa/Windhoek": "Виндхук", - "Africa/Johannesburg": "Йоханнесбург", - "Europe/Chisinau": "Кишинев", - "Africa/Mbabane": "Мбабане", - "Europe/Vilnius": "Вильнюс", - "Europe/Helsinki": "Хельсинки", - "Europe/Moscow": "Москва", - "Africa/Kampala": "Кампала", - "Africa/Nairobi": "Найроби", - "Africa/Asmara": "Асмэра", - "Europe/Istanbul": "Стамбул", - "Asia/Riyadh": "Эр-Рияд", - "Asia/Qatar": "Катар", - "Europe/Minsk": "Минск", - "Indian/Comoro": "Коморо", - "Asia/Kuwait": "Кувейт", - "Africa/Addis_Ababa": "Аддис-Абеба", - "Africa/Dar_es_Salaam": "Дар-эс-Салам", - "Europe/Volgograd": "Волгоград", - "Indian/Antananarivo": "Антананариву", - "Asia/Bahrain": "Бахрейн", - "Asia/Baghdad": "Багдад", - "Indian/Mayotte": "Майотта", - "Africa/Djibouti": "Джибути", - "Europe/Simferopol": "Симферополь", - "Asia/Aden": "Аден", - "Antarctica/Syowa": "Сёва", - "Africa/Mogadishu": "Могадишо", - "Asia/Tehran": "Тегеран", - "Asia/Yerevan": "Ереван", - "Asia/Tbilisi": "Тбилиси", - "Asia/Muscat": "Мускат", - "Europe/Samara": "Самара", - "Indian/Mahe": "Маэ", - "Asia/Baku": "Баку", - "Indian/Mauritius": "Маврикий", - "Indian/Reunion": "Воссоединение", - "Asia/Dubai": "Дубай", - "Asia/Kabul": "Кабул", - "Asia/Ashgabat": "Ашхабад", - "Antarctica/Mawson": "Моусон", - "Asia/Aqtau": "Актау", - "Asia/Yekaterinburg": "Екатеринбург", - "Asia/Aqtobe": "Актобе", - "Asia/Dushanbe": "Душанбе", - "Asia/Tashkent": "Ташкент", - "Asia/Samarkand": "Самарканд", - "Asia/Qyzylorda": "Кызылорда", - "Asia/Oral": "Оральный", - "Asia/Karachi": "Карачи", - "Indian/Kerguelen": "Кергелен", - "Indian/Maldives": "Мальдивы", - "Asia/Kolkata": "Калькутта", - "Asia/Colombo": "Коломбо", - "Asia/Kathmandu": "Катманду", - "Antarctica/Vostok": "Восток", - "Asia/Almaty": "Алматы", - "Asia/Urumqi": "Урумчи", - "Asia/Thimphu": "Тхимпху", - "Asia/Omsk": "Омск", - "Asia/Dhaka": "Дакка", - "Indian/Chagos": "Чагос", - "Asia/Bishkek": "Бишкек", - "Asia/Rangoon": "Рангун", - "Indian/Cocos": "кокосы", - "Asia/Bangkok": "Бангкок", - "Asia/Hovd": "Ховд", - "Asia/Novokuznetsk": "Новокузнецк", - "Asia/Vientiane": "Вьентьян", - "Asia/Krasnoyarsk": "Красноярск", - "Antarctica/Davis": "Дэвис", - "Asia/Novosibirsk": "Новосибирск", - "Asia/Phnom_Penh": "Пномпень", - "Asia/Pontianak": "Понтианак", - "Asia/Jakarta": "Джакарта", - "Asia/Ho_Chi_Minh": "Хо Ши Мин", - "Indian/Christmas": "Рождество", - "Asia/Manila": "Манила", - "Asia/Makassar": "Макассар", - "Asia/Macau": "Макао", - "Asia/Kuala_Lumpur": "Куала-Лумпур", - "Asia/Singapore": "Сингапур", - "Asia/Shanghai": "Шанхай", - "Asia/Irkutsk": "Иркутск", - "Asia/Kuching": "Кучинг", - "Asia/Hong_Kong": "Гонконг", - "Australia/Perth": "Перт", - "Asia/Taipei": "Тайбэй", - "Asia/Brunei": "Бруней", - "Asia/Choibalsan": "Чойбалсан", - "Asia/Ulaanbaatar": "Улан-Батор", - "Australia/Eucla": "Евкла", - "Asia/Yakutsk": "Якутск", - "Asia/Dili": "Дили", - "Pacific/Palau": "Палау", - "Asia/Jayapura": "Джаяпура", - "Asia/Seoul": "Сеул", - "Asia/Pyongyang": "Пхеньян", - "Asia/Khandyga": "Хандыга", - "Asia/Chita": "Чита", - "Asia/Tokyo": "Токио", - "Australia/Darwin": "Дарвин", - "Pacific/Saipan": "Сайпан", - "Australia/Brisbane": "Брисбен", - "Pacific/Port_Moresby": "Порт-Морсби", - "Pacific/Chuuk": "Чуук", - "Antarctica/DumontDUrville": "Дюмон-д'Юрвиль", - "Pacific/Guam": "Гуам", - "Australia/Lindeman": "Линдеман", - "Asia/Ust-Nera": "Усть-Нера", - "Asia/Vladivostok": "Владивосток", - "Australia/Broken_Hill": "Брокен-Хилл", - "Australia/Adelaide": "Аделаида", - "Asia/Sakhalin": "Сахалин", - "Pacific/Guadalcanal": "Гуадалканал", - "Pacific/Efate": "Эфате", - "Antarctica/Casey": "Кейси", - "Antarctica/Macquarie": "Маккуори", - "Pacific/Kosrae": "Косрае", - "Australia/Sydney": "Сидней", - "Pacific/Noumea": "Нумеа", - "Australia/Melbourne": "Мельбурн", - "Australia/Lord_Howe": "Остров Лорд-Хау", - "Australia/Hobart": "Хобарт", - "Pacific/Pohnpei": "Понпеи", - "Australia/Currie": "Карри", - "Asia/Srednekolymsk": "Среднеколымск", - "Asia/Magadan": "Магадан", - "Pacific/Kwajalein": "Кваджалейн", - "Pacific/Majuro": "Маджуро", - "Pacific/Funafuti": "Фунафути", - "Asia/Anadyr": "Анадырь", - "Pacific/Nauru": "Науру", - "Asia/Kamchatka": "Камчатка", - "Pacific/Fiji": "Фиджи", - "Pacific/Norfolk": "Норфолк", - "Pacific/Tarawa": "Тарава", - "Pacific/Wallis": "Уоллис", - "Pacific/Wake": "Будить", - "Pacific/Tongatapu": "Тонгатапу", - "Antarctica/McMurdo": "МакМердо", - "Pacific/Enderbury": "Эндербери", - "Pacific/Fakaofo": "Факаофо", - "Pacific/Auckland": "Окленд", - "Pacific/Chatham": "Чатем", - "Pacific/Kiritimati": "Киритимати", - "Pacific/Apia": "Апиа", + 'Pacific/Midway': 'Мидуэй', + 'Pacific/Niue': 'Ниуэ', + 'Pacific/Pago_Pago': 'Паго-Паго', + 'America/Adak': 'Адак', + 'Pacific/Honolulu': 'Гонолулу', + 'Pacific/Johnston': 'Джонстон', + 'Pacific/Rarotonga': 'Раротонга', + 'Pacific/Tahiti': 'Таити', + 'US/Hawaii': 'Гавайи', + 'Pacific/Marquesas': 'Маркизские острова', + 'America/Sitka': 'Ситка', + 'America/Anchorage': 'Анкоридж', + 'America/Metlakatla': 'Метлакатла', + 'America/Juneau': 'Джуно', + 'US/Alaska': 'Аляска', + 'America/Nome': 'Ном', + 'America/Yakutat': 'Якутат', + 'Pacific/Gambier': 'Гамбье', + 'America/Tijuana': 'Тихуана', + 'Pacific/Pitcairn': 'Питкэрн', + 'US/Pacific': 'США/Тихий океан', + 'Canada/Pacific': 'США/Тихий океан', + 'America/Los_Angeles': 'Лос-Анджелес', + 'America/Vancouver': 'Ванкувер', + 'America/Santa_Isabel': 'Санта-Изабель', + 'America/Chihuahua': 'Чихуахуа', + 'America/Cambridge_Bay': 'Кембридж-Бэй', + 'America/Inuvik': 'Инувик', + 'America/Boise': 'Бойсе', + 'America/Dawson': 'Доусон', + 'America/Mazatlan': 'Масатлан', + 'America/Dawson_Creek': 'Доусон-Крик', + 'US/Arizona': 'Аризона', + 'America/Denver': 'Денвер', + 'US/Mountain': 'гора', + 'America/Edmonton': 'Эдмонтон', + 'America/Yellowknife': 'Йеллоунайф', + 'America/Ojinaga': 'Охинага', + 'America/Phoenix': 'Феникс', + 'America/Whitehorse': 'Белая лошадь', + 'Canada/Mountain': 'гора', + 'America/Hermosillo': 'Эрмосильо', + 'America/Creston': 'Крестон', + 'America/Swift_Current': 'Свифт Керрент', + 'America/Tegucigalpa': 'Тегусигальпа', + 'America/Regina': 'Регина', + 'America/Rankin_Inlet': 'Ранкин-Инлет', + 'America/Rainy_River': 'Райни-Ривер', + 'America/Winnipeg': 'Виннипег', + 'America/North_Dakota/Center': 'Северная Дакота/Центр', + 'America/North_Dakota/Beulah': 'Северная Дакота/Беула', + 'America/Monterrey': 'Монтеррей', + 'America/Mexico_City': 'Мехико', + 'US/Central': 'Центральный', + 'America/Merida': 'Мерида', + 'America/Menominee': 'Меномини', + 'America/Matamoros': 'Матаморос', + 'America/Managua': 'Манагуа', + 'America/North_Dakota/New_Salem': 'Северная Дакота/Нью-Салем', + 'Pacific/Galapagos': 'Галапагосские острова', + 'America/Indiana/Tell_City': 'Индиана/Телл-Сити', + 'America/Indiana/Knox': 'Индиана/Нокс', + 'Canada/Central': 'Центральный', + 'America/Guatemala': 'Гватемала', + 'America/El_Salvador': 'Сальвадор', + 'America/Costa_Rica': 'Коста-Рика', + 'America/Chicago': 'Чикаго', + 'America/Belize': 'Белиз', + 'America/Bahia_Banderas': 'Баия де Бандерас', + 'America/Resolute': 'Резольют', + 'America/Atikokan': 'Атикокан', + 'America/Lima': 'Лима', + 'America/Bogota': 'Богота', + 'America/Cancun': 'Канкун', + 'America/Cayman': 'Кайман', + 'America/Detroit': 'Детройт', + 'America/Indiana/Indianapolis': 'Индиана/Индианаполис', + 'America/Eirunepe': 'Эйрунепе', + 'America/Grand_Turk': 'Гранд-Терк', + 'America/Guayaquil': 'Гуаякиль', + 'America/Havana': 'Гавана', + 'America/Indiana/Marengo': 'Индиана/Маренго', + 'America/Indiana/Petersburg': 'Индиана/Петербург', + 'America/Indiana/Vevay': 'Индиана/Вева', + 'America/Indiana/Vincennes': 'Индиана/Винсеннес', + 'America/Indiana/Winamac': 'Индиана/Винамак', + 'America/Iqaluit': 'Икалуит', + 'America/Jamaica': 'Ямайка', + 'America/Kentucky/Louisville': 'Кентукки/Луисвилл', + 'America/Nassau': 'Нассау', + 'America/Toronto': 'Торонто', + 'America/Montreal': 'Монреаль', + 'America/Pangnirtung': 'Пангниртунг', + 'America/Port-au-Prince': 'Порт-о-Пренс', + 'America/Kentucky/Monticello': 'Кентукки/Монтичелло', + 'Canada/Eastern': 'Канада/Восточное', + 'US/Eastern': 'США/Восточное', + 'America/Thunder_Bay': 'Тандер-Бей', + 'Pacific/Easter': 'Пасха', + 'America/Panama': 'Панама', + 'America/Nipigon': 'Нипигон', + 'America/Rio_Branco': 'Рио-Бранко', + 'America/New_York': 'Нью-Йорк', + 'Canada/Atlantic': 'Атлантика', + 'America/Kralendijk': 'Кралендейк', + 'America/La_Paz': 'Ла-Пас', + 'America/Halifax': 'Галифакс', + 'America/Lower_Princes': 'Лоуэр-Принс-Куотер', + 'America/Manaus': 'Манаус', + 'America/Marigot': 'Мариго', + 'America/Martinique': 'Мартиника', + 'America/Moncton': 'Монктон', + 'America/Guyana': 'Гайана', + 'America/Montserrat': 'Монтсеррат', + 'America/Guadeloupe': 'Гваделупа', + 'America/Grenada': 'Гренада', + 'America/Goose_Bay': 'Гуз-Бей', + 'America/Glace_Bay': 'Глас Бэй', + 'America/Curacao': 'Кюрасао', + 'America/Cuiaba': 'Куяба', + 'America/Port_of_Spain': 'Порт-оф-Спейн', + 'America/Porto_Velho': 'Порту-Велью', + 'America/Puerto_Rico': 'Пуэрто-Рико', + 'America/Caracas': 'Каракас', + 'America/Santo_Domingo': 'Санто-Доминго', + 'America/St_Barthelemy': 'Святой Бартелеми', + 'Atlantic/Bermuda': 'Бермуды', + 'America/St_Kitts': 'Сент-Китс', + 'America/St_Lucia': 'Святая Люсия', + 'America/St_Thomas': 'Сент-Томас', + 'America/St_Vincent': 'Сент-Винсент', + 'America/Thule': 'Туле', + 'America/Campo_Grande': 'Кампу-Гранди', + 'America/Boa_Vista': 'Боа-Виста', + 'America/Tortola': 'Тортола', + 'America/Aruba': 'Аруба', + 'America/Blanc-Sablon': 'Блан-Саблон', + 'America/Barbados': 'Барбадос', + 'America/Anguilla': 'Ангилья', + 'America/Antigua': 'Антигуа', + 'America/Dominica': 'Доминика', + 'Canada/Newfoundland': 'Ньюфаундленд', + 'America/St_Johns': 'Сент-Джонс', + 'America/Sao_Paulo': 'Сан-Паулу', + 'Atlantic/Stanley': 'Стэнли', + 'America/Miquelon': 'Микелон', + 'America/Argentina/Salta': 'Аргентина/Сальта', + 'America/Montevideo': 'Монтевидео', + 'America/Argentina/Rio_Gallegos': 'Аргентина/Рио-Гальегос', + 'America/Argentina/Mendoza': 'Аргентина/Мендоса', + 'America/Argentina/La_Rioja': 'Аргентина/Ла-Риоха', + 'America/Argentina/Jujuy': 'Аргентина/Жужуй', + 'Antarctica/Rothera': 'Ротера', + 'America/Argentina/Cordoba': 'Аргентина/Кордова', + 'America/Argentina/Catamarca': 'Аргентина/Катамарка', + 'America/Argentina/Ushuaia': 'Аргентина/Ушуая', + 'America/Argentina/Tucuman': 'Аргентина/Тукуман', + 'America/Paramaribo': 'Парамарибо', + 'America/Argentina/San_Luis': 'Аргентина/Сан-Луис', + 'America/Recife': 'Ресифи', + 'America/Argentina/Buenos_Aires': 'Аргентина/Буэнос-Айрес', + 'America/Asuncion': 'Асунсьон', + 'America/Maceio': 'Масейо', + 'America/Santarem': 'Сантарен', + 'America/Santiago': 'Сантьяго', + 'Antarctica/Palmer': 'Палмер', + 'America/Argentina/San_Juan': 'Аргентина/Сан-Хуан', + 'America/Fortaleza': 'Форталеза', + 'America/Cayenne': 'Кайенна', + 'America/Godthab': 'Годтаб', + 'America/Belem': 'Белен', + 'America/Araguaina': 'Арагуайна', + 'America/Bahia': 'Баия', + 'Atlantic/South_Georgia': 'Южная_Грузия', + 'America/Noronha': 'Норонья', + 'Atlantic/Azores': 'Азорские острова', + 'Atlantic/Cape_Verde': 'Кабо-Верде', + 'America/Scoresbysund': 'Скорсбисунд', + 'Africa/Accra': 'Аккра', + 'Atlantic/Faroe': 'Фарерские острова', + 'Europe/Guernsey': 'Гернси', + 'Africa/Dakar': 'Дакар', + 'Europe/Isle_of_Man': 'Остров Мэн', + 'Africa/Conakry': 'Конакри', + 'Africa/Abidjan': 'Абиджан', + 'Atlantic/Canary': 'канарейка', + 'Africa/Banjul': 'Банжул', + 'Europe/Jersey': 'Джерси', + 'Atlantic/St_Helena': 'Остров Святой Елены', + 'Africa/Bissau': 'Бисау', + 'Europe/London': 'Лондон', + 'Africa/Nouakchott': 'Нуакшот', + 'Africa/Lome': 'Ломе', + 'America/Danmarkshavn': 'Данмарксхавн', + 'Africa/Ouagadougou': 'Уагадугу', + 'Europe/Lisbon': 'Лиссабон', + 'Africa/Sao_Tome': 'Сан-Томе', + 'Africa/Monrovia': 'Монровия', + 'Atlantic/Reykjavik': 'Рейкьявик', + 'Antarctica/Troll': 'Тролль', + 'Atlantic/Madeira': 'Мадейра', + 'Africa/Bamako': 'Бамако', + 'Europe/Dublin': 'Дублин', + 'Africa/Freetown': 'Фритаун', + 'Europe/Monaco': 'Монако', + 'Europe/Skopje': 'Скопье', + 'Europe/Amsterdam': 'Амстердам', + 'Africa/Tunis': 'Тунис', + 'Arctic/Longyearbyen': 'Лонгйир', + 'Africa/Bangui': 'Банги', + 'Africa/Lagos': 'Лагос', + 'Africa/Douala': 'Дуала', + 'Africa/Libreville': 'Либревиль', + 'Europe/Belgrade': 'Белград', + 'Europe/Stockholm': 'Стокгольм', + 'Europe/Berlin': 'Берлин', + 'Europe/Zurich': 'Цюрих', + 'Europe/Zagreb': 'Загреб', + 'Europe/Warsaw': 'Варшава', + 'Africa/Luanda': 'Луанда', + 'Africa/Porto-Novo': 'Порто-Ново', + 'Africa/Brazzaville': 'Браззавиль', + 'Europe/Vienna': 'Вена', + 'Europe/Vatican': 'Ватикан', + 'Europe/Vaduz': 'Вадуц', + 'Europe/Tirane': 'Тиран', + 'Europe/Bratislava': 'Братислава', + 'Europe/Brussels': 'Брюссель', + 'Europe/Paris': 'Париж', + 'Europe/Sarajevo': 'Сараево', + 'Europe/San_Marino': 'Сан-Марино', + 'Europe/Rome': 'Рим', + 'Africa/El_Aaiun': 'Эль-Аайун', + 'Africa/Casablanca': 'Касабланка', + 'Europe/Malta': 'Мальта', + 'Africa/Ceuta': 'Сеута', + 'Europe/Gibraltar': 'Гибралтар', + 'Africa/Malabo': 'Малабо', + 'Europe/Busingen': 'Бузинген', + 'Africa/Ndjamena': 'Нджамена', + 'Europe/Andorra': 'Андорра', + 'Europe/Oslo': 'Осло', + 'Europe/Luxembourg': 'Люксембург', + 'Africa/Niamey': 'Ниамей', + 'Europe/Copenhagen': 'Копенгаген', + 'Europe/Madrid': 'Мадрид', + 'Europe/Budapest': 'Будапешт', + 'Africa/Algiers': 'Алжир', + 'Europe/Ljubljana': 'Любляна', + 'Europe/Podgorica': 'Подгорица', + 'Africa/Kinshasa': 'Киншаса', + 'Europe/Prague': 'Прага', + 'Europe/Riga': 'Рига', + 'Africa/Bujumbura': 'Бужумбура', + 'Africa/Lubumbashi': 'Лубумбаши', + 'Europe/Bucharest': 'Бухарест', + 'Africa/Blantyre': 'Блантайр', + 'Asia/Nicosia': 'Никосия', + 'Europe/Sofia': 'София', + 'Asia/Jerusalem': 'Иерусалим', + 'Europe/Tallinn': 'Таллинн', + 'Europe/Uzhgorod': 'Ужгород', + 'Africa/Lusaka': 'Лусака', + 'Europe/Mariehamn': 'Мариехамн', + 'Asia/Hebron': 'Хеврон', + 'Asia/Gaza': 'Газа', + 'Asia/Damascus': 'Дамаск', + 'Europe/Zaporozhye': 'Запорожье', + 'Asia/Beirut': 'Бейрут', + 'Africa/Juba': 'Джуба', + 'Africa/Harare': 'Хараре', + 'Europe/Athens': 'Афины', + 'Europe/Kiev': 'Киев', + 'Europe/Kaliningrad': 'Калининград', + 'Africa/Khartoum': 'Хартум', + 'Africa/Cairo': 'Каир', + 'Africa/Kigali': 'Кигали', + 'Asia/Amman': 'Амман', + 'Africa/Maputo': 'Мапуту', + 'Africa/Gaborone': 'Габороне', + 'Africa/Tripoli': 'Триполи', + 'Africa/Maseru': 'Масеру', + 'Africa/Windhoek': 'Виндхук', + 'Africa/Johannesburg': 'Йоханнесбург', + 'Europe/Chisinau': 'Кишинев', + 'Africa/Mbabane': 'Мбабане', + 'Europe/Vilnius': 'Вильнюс', + 'Europe/Helsinki': 'Хельсинки', + 'Europe/Moscow': 'Москва', + 'Africa/Kampala': 'Кампала', + 'Africa/Nairobi': 'Найроби', + 'Africa/Asmara': 'Асмэра', + 'Europe/Istanbul': 'Стамбул', + 'Asia/Riyadh': 'Эр-Рияд', + 'Asia/Qatar': 'Катар', + 'Europe/Minsk': 'Минск', + 'Indian/Comoro': 'Коморо', + 'Asia/Kuwait': 'Кувейт', + 'Africa/Addis_Ababa': 'Аддис-Абеба', + 'Africa/Dar_es_Salaam': 'Дар-эс-Салам', + 'Europe/Volgograd': 'Волгоград', + 'Indian/Antananarivo': 'Антананариву', + 'Asia/Bahrain': 'Бахрейн', + 'Asia/Baghdad': 'Багдад', + 'Indian/Mayotte': 'Майотта', + 'Africa/Djibouti': 'Джибути', + 'Europe/Simferopol': 'Симферополь', + 'Asia/Aden': 'Аден', + 'Antarctica/Syowa': 'Сёва', + 'Africa/Mogadishu': 'Могадишо', + 'Asia/Tehran': 'Тегеран', + 'Asia/Yerevan': 'Ереван', + 'Asia/Tbilisi': 'Тбилиси', + 'Asia/Muscat': 'Мускат', + 'Europe/Samara': 'Самара', + 'Indian/Mahe': 'Маэ', + 'Asia/Baku': 'Баку', + 'Indian/Mauritius': 'Маврикий', + 'Indian/Reunion': 'Воссоединение', + 'Asia/Dubai': 'Дубай', + 'Asia/Kabul': 'Кабул', + 'Asia/Ashgabat': 'Ашхабад', + 'Antarctica/Mawson': 'Моусон', + 'Asia/Aqtau': 'Актау', + 'Asia/Yekaterinburg': 'Екатеринбург', + 'Asia/Aqtobe': 'Актобе', + 'Asia/Dushanbe': 'Душанбе', + 'Asia/Tashkent': 'Ташкент', + 'Asia/Samarkand': 'Самарканд', + 'Asia/Qyzylorda': 'Кызылорда', + 'Asia/Oral': 'Оральный', + 'Asia/Karachi': 'Карачи', + 'Indian/Kerguelen': 'Кергелен', + 'Indian/Maldives': 'Мальдивы', + 'Asia/Kolkata': 'Калькутта', + 'Asia/Colombo': 'Коломбо', + 'Asia/Kathmandu': 'Катманду', + 'Antarctica/Vostok': 'Восток', + 'Asia/Almaty': 'Алматы', + 'Asia/Urumqi': 'Урумчи', + 'Asia/Thimphu': 'Тхимпху', + 'Asia/Omsk': 'Омск', + 'Asia/Dhaka': 'Дакка', + 'Indian/Chagos': 'Чагос', + 'Asia/Bishkek': 'Бишкек', + 'Asia/Rangoon': 'Рангун', + 'Indian/Cocos': 'кокосы', + 'Asia/Bangkok': 'Бангкок', + 'Asia/Hovd': 'Ховд', + 'Asia/Novokuznetsk': 'Новокузнецк', + 'Asia/Vientiane': 'Вьентьян', + 'Asia/Krasnoyarsk': 'Красноярск', + 'Antarctica/Davis': 'Дэвис', + 'Asia/Novosibirsk': 'Новосибирск', + 'Asia/Phnom_Penh': 'Пномпень', + 'Asia/Pontianak': 'Понтианак', + 'Asia/Jakarta': 'Джакарта', + 'Asia/Ho_Chi_Minh': 'Хо Ши Мин', + 'Indian/Christmas': 'Рождество', + 'Asia/Manila': 'Манила', + 'Asia/Makassar': 'Макассар', + 'Asia/Macau': 'Макао', + 'Asia/Kuala_Lumpur': 'Куала-Лумпур', + 'Asia/Singapore': 'Сингапур', + 'Asia/Shanghai': 'Шанхай', + 'Asia/Irkutsk': 'Иркутск', + 'Asia/Kuching': 'Кучинг', + 'Asia/Hong_Kong': 'Гонконг', + 'Australia/Perth': 'Перт', + 'Asia/Taipei': 'Тайбэй', + 'Asia/Brunei': 'Бруней', + 'Asia/Choibalsan': 'Чойбалсан', + 'Asia/Ulaanbaatar': 'Улан-Батор', + 'Australia/Eucla': 'Евкла', + 'Asia/Yakutsk': 'Якутск', + 'Asia/Dili': 'Дили', + 'Pacific/Palau': 'Палау', + 'Asia/Jayapura': 'Джаяпура', + 'Asia/Seoul': 'Сеул', + 'Asia/Pyongyang': 'Пхеньян', + 'Asia/Khandyga': 'Хандыга', + 'Asia/Chita': 'Чита', + 'Asia/Tokyo': 'Токио', + 'Australia/Darwin': 'Дарвин', + 'Pacific/Saipan': 'Сайпан', + 'Australia/Brisbane': 'Брисбен', + 'Pacific/Port_Moresby': 'Порт-Морсби', + 'Pacific/Chuuk': 'Чуук', + 'Antarctica/DumontDUrville': "Дюмон-д'Юрвиль", + 'Pacific/Guam': 'Гуам', + 'Australia/Lindeman': 'Линдеман', + 'Asia/Ust-Nera': 'Усть-Нера', + 'Asia/Vladivostok': 'Владивосток', + 'Australia/Broken_Hill': 'Брокен-Хилл', + 'Australia/Adelaide': 'Аделаида', + 'Asia/Sakhalin': 'Сахалин', + 'Pacific/Guadalcanal': 'Гуадалканал', + 'Pacific/Efate': 'Эфате', + 'Antarctica/Casey': 'Кейси', + 'Antarctica/Macquarie': 'Маккуори', + 'Pacific/Kosrae': 'Косрае', + 'Australia/Sydney': 'Сидней', + 'Pacific/Noumea': 'Нумеа', + 'Australia/Melbourne': 'Мельбурн', + 'Australia/Lord_Howe': 'Остров Лорд-Хау', + 'Australia/Hobart': 'Хобарт', + 'Pacific/Pohnpei': 'Понпеи', + 'Australia/Currie': 'Карри', + 'Asia/Srednekolymsk': 'Среднеколымск', + 'Asia/Magadan': 'Магадан', + 'Pacific/Kwajalein': 'Кваджалейн', + 'Pacific/Majuro': 'Маджуро', + 'Pacific/Funafuti': 'Фунафути', + 'Asia/Anadyr': 'Анадырь', + 'Pacific/Nauru': 'Науру', + 'Asia/Kamchatka': 'Камчатка', + 'Pacific/Fiji': 'Фиджи', + 'Pacific/Norfolk': 'Норфолк', + 'Pacific/Tarawa': 'Тарава', + 'Pacific/Wallis': 'Уоллис', + 'Pacific/Wake': 'Будить', + 'Pacific/Tongatapu': 'Тонгатапу', + 'Antarctica/McMurdo': 'МакМердо', + 'Pacific/Enderbury': 'Эндербери', + 'Pacific/Fakaofo': 'Факаофо', + 'Pacific/Auckland': 'Окленд', + 'Pacific/Chatham': 'Чатем', + 'Pacific/Kiritimati': 'Киритимати', + 'Pacific/Apia': 'Апиа', }; 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 cdc0fa65..0ae610cf 100644 --- a/lib/ui/pages/server_details/time_zone/time_zone.dart +++ b/lib/ui/pages/server_details/time_zone/time_zone.dart @@ -5,7 +5,7 @@ final List locations = timeZoneDatabase.locations.values.toList() l1.currentTimeZone.offset.compareTo(l2.currentTimeZone.offset)); class SelectTimezone extends StatefulWidget { - SelectTimezone({Key? key}) : super(key: key); + const SelectTimezone({Key? key}) : super(key: key); @override _SelectTimezoneState createState() => _SelectTimezoneState(); @@ -28,7 +28,7 @@ class _SelectTimezoneState extends State { if (index >= 0) { controller.animateTo(60.0 * index, - duration: Duration(milliseconds: 300), curve: Curves.easeIn); + duration: const Duration(milliseconds: 300), curve: Curves.easeIn); } } @@ -41,12 +41,12 @@ class _SelectTimezoneState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: PreferredSize( + appBar: const PreferredSize( + preferredSize: Size.fromHeight(52), child: BrandHeader( title: 'select timezone', hasBackButton: true, ), - preferredSize: Size.fromHeight(52), ), body: ListView( controller: controller, @@ -71,31 +71,31 @@ class _SelectTimezoneState extends State { key, Container( height: 60, - padding: EdgeInsets.symmetric(horizontal: 20), + padding: const EdgeInsets.symmetric(horizontal: 20), + decoration: const BoxDecoration( + border: Border( + bottom: BorderSide( + color: BrandColors.dividerColor, + )), + ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, children: [ BrandText.body1( timezoneName, - style: TextStyle( + style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ), ), BrandText.small( 'GMT ${duration.toDayHourMinuteFormat()} ${area.isNotEmpty ? '($area)' : ''}', - style: TextStyle( + style: const TextStyle( fontSize: 13, )), ], ), - decoration: BoxDecoration( - border: Border( - bottom: BorderSide( - color: BrandColors.dividerColor, - )), - ), ), ); }) diff --git a/lib/ui/pages/services/services.dart b/lib/ui/pages/services/services.dart index d2619420..77134124 100644 --- a/lib/ui/pages/services/services.dart +++ b/lib/ui/pages/services/services.dart @@ -21,7 +21,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:selfprivacy/utils/ui_helpers.dart'; import 'package:url_launcher/url_launcher_string.dart'; -import '../rootRoute.dart'; +import '../root_route.dart'; const switchableServices = [ ServiceTypes.passwordManager, @@ -32,7 +32,7 @@ const switchableServices = [ ]; class ServicesPage extends StatefulWidget { - ServicesPage({Key? key}) : super(key: key); + const ServicesPage({Key? key}) : super(key: key); @override _ServicesPageState createState() => _ServicesPageState(); @@ -62,21 +62,21 @@ class _ServicesPageState extends State { return Scaffold( appBar: PreferredSize( + preferredSize: const Size.fromHeight(52), child: BrandHeader( title: 'basis.services'.tr(), hasFlashButton: true, ), - preferredSize: Size.fromHeight(52), ), body: ListView( padding: paddingH15V0, children: [ BrandText.body1('services.title'.tr()), - SizedBox(height: 24), - if (!isReady) ...[NotReadyCard(), SizedBox(height: 24)], + const SizedBox(height: 24), + if (!isReady) ...[const NotReadyCard(), const SizedBox(height: 24)], ...ServiceTypes.values .map((t) => Padding( - padding: EdgeInsets.only( + padding: const EdgeInsets.only( bottom: 30, ), child: _Card(serviceType: t), @@ -145,7 +145,7 @@ class _Card extends StatelessWidget { child: Icon(serviceType.icon, size: 30, color: Colors.white), ), if (isReady && switchableService) ...[ - Spacer(), + const Spacer(), Builder( builder: (context) { late bool isActive; @@ -179,9 +179,9 @@ class _Card extends StatelessWidget { Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox(height: 10), + const SizedBox(height: 10), BrandText.h2(serviceType.title), - SizedBox(height: 10), + const SizedBox(height: 10), if (serviceType.subdomain != '') Column( children: [ @@ -193,7 +193,7 @@ class _Card extends StatelessWidget { style: linkStyle, ), ), - SizedBox(height: 10), + const SizedBox(height: 10), ], ), if (serviceType == ServiceTypes.mail) @@ -202,12 +202,12 @@ class _Card extends StatelessWidget { domainName, style: linkStyle, ), - SizedBox(height: 10), + const SizedBox(height: 10), ]), BrandText.body2(serviceType.loginInfo), - SizedBox(height: 10), + const SizedBox(height: 10), BrandText.body2(serviceType.subtitle), - SizedBox(height: 10), + const SizedBox(height: 10), ], ), if (hasSwitchJob) @@ -282,10 +282,10 @@ class _ServiceDetails extends StatelessWidget { text: 'services.mail.bottom_sheet.1'.tr(args: [domainName]), style: textStyle, ), - WidgetSpan(child: SizedBox(width: 5)), + const WidgetSpan(child: SizedBox(width: 5)), WidgetSpan( child: Padding( - padding: EdgeInsets.only(bottom: 0.8), + padding: const EdgeInsets.only(bottom: 0.8), child: GestureDetector( child: Text( 'services.mail.bottom_sheet.2'.tr(), @@ -321,10 +321,10 @@ class _ServiceDetails extends StatelessWidget { .tr(args: [domainName]), style: textStyle, ), - WidgetSpan(child: SizedBox(width: 5)), + const WidgetSpan(child: SizedBox(width: 5)), WidgetSpan( child: Padding( - padding: EdgeInsets.only(bottom: 0.8), + padding: const EdgeInsets.only(bottom: 0.8), child: GestureDetector( onTap: () => _launchURL('https://password.$domainName'), child: Text( @@ -345,10 +345,10 @@ class _ServiceDetails extends StatelessWidget { text: 'services.video.bottom_sheet.1'.tr(args: [domainName]), style: textStyle, ), - WidgetSpan(child: SizedBox(width: 5)), + const WidgetSpan(child: SizedBox(width: 5)), WidgetSpan( child: Padding( - padding: EdgeInsets.only(bottom: 0.8), + padding: const EdgeInsets.only(bottom: 0.8), child: GestureDetector( onTap: () => _launchURL('https://meet.$domainName'), child: Text( @@ -369,10 +369,10 @@ class _ServiceDetails extends StatelessWidget { text: 'services.cloud.bottom_sheet.1'.tr(args: [domainName]), style: textStyle, ), - WidgetSpan(child: SizedBox(width: 5)), + const WidgetSpan(child: SizedBox(width: 5)), WidgetSpan( child: Padding( - padding: EdgeInsets.only(bottom: 0.8), + padding: const EdgeInsets.only(bottom: 0.8), child: GestureDetector( onTap: () => _launchURL('https://cloud.$domainName'), child: Text( @@ -394,10 +394,10 @@ class _ServiceDetails extends StatelessWidget { .tr(args: [domainName]), style: textStyle, ), - WidgetSpan(child: SizedBox(width: 5)), + const WidgetSpan(child: SizedBox(width: 5)), WidgetSpan( child: Padding( - padding: EdgeInsets.only(bottom: 0.8), + padding: const EdgeInsets.only(bottom: 0.8), child: GestureDetector( onTap: () => _launchURL('https://social.$domainName'), child: Text( @@ -418,10 +418,10 @@ class _ServiceDetails extends StatelessWidget { text: 'services.git.bottom_sheet.1'.tr(args: [domainName]), style: textStyle, ), - WidgetSpan(child: SizedBox(width: 5)), + const WidgetSpan(child: SizedBox(width: 5)), WidgetSpan( child: Padding( - padding: EdgeInsets.only(bottom: 0.8), + padding: const EdgeInsets.only(bottom: 0.8), child: GestureDetector( onTap: () => _launchURL('https://git.$domainName'), child: Text( @@ -445,7 +445,7 @@ class _ServiceDetails extends StatelessWidget { borderRadius: BorderRadius.circular(20), ), child: SingleChildScrollView( - child: Container( + child: SizedBox( width: 350, child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -459,11 +459,11 @@ class _ServiceDetails extends StatelessWidget { status: status, child: Icon(icon, size: 40, color: Colors.white), ), - SizedBox(height: 10), + const SizedBox(height: 10), BrandText.h2(title), - SizedBox(height: 10), + const SizedBox(height: 10), child, - SizedBox(height: 40), + const SizedBox(height: 40), Center( child: Container( child: BrandButton.rised( diff --git a/lib/ui/pages/setup/initializing.dart b/lib/ui/pages/setup/initializing.dart index 0b99c761..5121ac36 100644 --- a/lib/ui/pages/setup/initializing.dart +++ b/lib/ui/pages/setup/initializing.dart @@ -16,7 +16,7 @@ import 'package:selfprivacy/ui/components/brand_md/brand_md.dart'; import 'package:selfprivacy/ui/components/brand_text/brand_text.dart'; import 'package:selfprivacy/ui/components/brand_timer/brand_timer.dart'; import 'package:selfprivacy/ui/components/progress_bar/progress_bar.dart'; -import 'package:selfprivacy/ui/pages/rootRoute.dart'; +import 'package:selfprivacy/ui/pages/root_route.dart'; import 'package:selfprivacy/ui/pages/setup/recovering/recovery_routing.dart'; import 'package:selfprivacy/utils/route_transitions/basic.dart'; @@ -26,7 +26,7 @@ class InitializingPage extends StatelessWidget { var cubit = context.watch(); if (cubit.state is ServerInstallationRecovery) { - return RecoveryRouting(); + return const RecoveryRouting(); } else { var actualInitializingPage = [ () => _stepHetzner(cubit), @@ -38,13 +38,14 @@ class InitializingPage extends StatelessWidget { () => _stepCheck(cubit), () => _stepCheck(cubit), () => _stepCheck(cubit), - () => Container(child: Center(child: Text('initializing.finish'.tr()))) + () => Center(child: Text('initializing.finish'.tr())) ][cubit.state.progress.index](); return BlocListener( listener: (context, state) { if (cubit.state is ServerInstallationFinished) { - Navigator.of(context).pushReplacement(materialRoute(RootPage())); + Navigator.of(context) + .pushReplacement(materialRoute(const RootPage())); } }, child: SafeArea( @@ -56,11 +57,11 @@ class InitializingPage extends StatelessWidget { Padding( padding: paddingH15V0.copyWith(top: 10, bottom: 10), child: cubit.state.isFullyInitilized - ? SizedBox( + ? const SizedBox( height: 80, ) : ProgressBar( - steps: [ + steps: const [ 'Hetzner', 'CloudFlare', 'Backblaze', @@ -74,7 +75,7 @@ class InitializingPage extends StatelessWidget { ), _addCard( AnimatedSwitcher( - duration: Duration(milliseconds: 300), + duration: const Duration(milliseconds: 300), child: actualInitializingPage, ), ), @@ -96,7 +97,7 @@ class InitializingPage extends StatelessWidget { : 'basis.later'.tr(), onPressed: () { Navigator.of(context).pushAndRemoveUntil( - materialRoute(RootPage()), + materialRoute(const RootPage()), (predicate) => false, ); }, @@ -109,8 +110,8 @@ class InitializingPage extends StatelessWidget { child: BrandButton.text( title: 'basis.connect_to_existing'.tr(), onPressed: () { - Navigator.of(context).push( - materialRoute(RecoveryRouting())); + Navigator.of(context).push(materialRoute( + const RecoveryRouting())); }, ), ) @@ -137,30 +138,30 @@ class InitializingPage extends StatelessWidget { 'assets/images/logos/hetzner.png', width: 150, ), - SizedBox(height: 10), + const SizedBox(height: 10), BrandText.h2('initializing.1'.tr()), - SizedBox(height: 10), + const SizedBox(height: 10), BrandText.body2('initializing.2'.tr()), - Spacer(), + const Spacer(), CubitFormTextField( formFieldCubit: context.read().apiKey, textAlign: TextAlign.center, - scrollPadding: EdgeInsets.only(bottom: 70), - decoration: InputDecoration( + scrollPadding: const EdgeInsets.only(bottom: 70), + decoration: const InputDecoration( hintText: 'Hetzner API Token', ), ), - Spacer(), + const Spacer(), BrandButton.rised( onPressed: formCubitState.isSubmitting ? null : () => context.read().trySubmit(), text: 'basis.connect'.tr(), ), - SizedBox(height: 10), + const SizedBox(height: 10), BrandButton.text( onPressed: () => - _showModal(context, _HowTo(fileName: 'how_hetzner')), + _showModal(context, const _HowTo(fileName: 'how_hetzner')), title: 'initializing.how'.tr(), ), ], @@ -193,31 +194,31 @@ class InitializingPage extends StatelessWidget { 'assets/images/logos/cloudflare.png', width: 150, ), - SizedBox(height: 10), + const SizedBox(height: 10), BrandText.h2('initializing.3'.tr()), - SizedBox(height: 10), + const SizedBox(height: 10), BrandText.body2('initializing.4'.tr()), - Spacer(), + const Spacer(), CubitFormTextField( formFieldCubit: context.read().apiKey, textAlign: TextAlign.center, - scrollPadding: EdgeInsets.only(bottom: 70), + scrollPadding: const EdgeInsets.only(bottom: 70), decoration: InputDecoration( hintText: 'initializing.5'.tr(), ), ), - Spacer(), + const Spacer(), BrandButton.rised( onPressed: formCubitState.isSubmitting ? null : () => context.read().trySubmit(), text: 'basis.connect'.tr(), ), - SizedBox(height: 10), + const SizedBox(height: 10), BrandButton.text( onPressed: () => _showModal( context, - _HowTo( + const _HowTo( fileName: 'how_cloudflare', )), title: 'initializing.how'.tr(), @@ -240,39 +241,39 @@ class InitializingPage extends StatelessWidget { 'assets/images/logos/backblaze.png', height: 50, ), - SizedBox(height: 10), + const SizedBox(height: 10), BrandText.h2('initializing.6'.tr()), - SizedBox(height: 10), - Spacer(), + const SizedBox(height: 10), + const Spacer(), CubitFormTextField( formFieldCubit: context.read().keyId, textAlign: TextAlign.center, - scrollPadding: EdgeInsets.only(bottom: 70), - decoration: InputDecoration( + scrollPadding: const EdgeInsets.only(bottom: 70), + decoration: const InputDecoration( hintText: 'KeyID', ), ), - Spacer(), + const Spacer(), CubitFormTextField( formFieldCubit: context.read().applicationKey, textAlign: TextAlign.center, - scrollPadding: EdgeInsets.only(bottom: 70), - decoration: InputDecoration( + scrollPadding: const EdgeInsets.only(bottom: 70), + decoration: const InputDecoration( hintText: 'Master Application Key', ), ), - Spacer(), + const Spacer(), BrandButton.rised( onPressed: formCubitState.isSubmitting ? null : () => context.read().trySubmit(), text: 'basis.connect'.tr(), ), - SizedBox(height: 10), + const SizedBox(height: 10), BrandButton.text( onPressed: () => _showModal( context, - _HowTo( + const _HowTo( fileName: 'how_backblaze', )), title: 'initializing.how'.tr(), @@ -295,9 +296,9 @@ class InitializingPage extends StatelessWidget { 'assets/images/logos/cloudflare.png', width: 150, ), - SizedBox(height: 30), + const SizedBox(height: 30), BrandText.h2('basis.domain'.tr()), - SizedBox(height: 10), + const SizedBox(height: 10), if (state is Empty) BrandText.body2('initializing.7'.tr()), if (state is Loading) BrandText.body2( @@ -310,7 +311,7 @@ class InitializingPage extends StatelessWidget { 'initializing.9'.tr(), ), if (state is Loaded) ...[ - SizedBox(height: 10), + const SizedBox(height: 10), Row( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.end, @@ -318,18 +319,18 @@ class InitializingPage extends StatelessWidget { children: [ Expanded( child: BrandText.h3( - '${state.domain}', + state.domain, textAlign: TextAlign.center, ), ), - Container( + SizedBox( width: 50, child: BrandButton.rised( onPressed: () => context.read().load(), child: Row( mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, - children: [ + children: const [ Icon( Icons.refresh, color: Colors.white, @@ -342,30 +343,30 @@ class InitializingPage extends StatelessWidget { ) ], if (state is Empty) ...[ - SizedBox(height: 30), + const SizedBox(height: 30), BrandButton.rised( onPressed: () => context.read().load(), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon( + const Icon( Icons.refresh, color: Colors.white, ), - SizedBox(width: 10), + const SizedBox(width: 10), BrandText.buttonTitleText('Обновить cписок'), ], ), ), ], if (state is Loaded) ...[ - SizedBox(height: 30), + const SizedBox(height: 30), BrandButton.rised( onPressed: () => context.read().saveDomain(), text: 'initializing.10'.tr(), ), ], - SizedBox( + const SizedBox( height: 10, width: double.infinity, ), @@ -386,18 +387,18 @@ class InitializingPage extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ BrandText.h2('initializing.22'.tr()), - SizedBox(height: 10), + const SizedBox(height: 10), BrandText.body2('initializing.23'.tr()), - Spacer(), + const Spacer(), CubitFormTextField( formFieldCubit: context.read().userName, textAlign: TextAlign.center, - scrollPadding: EdgeInsets.only(bottom: 70), + scrollPadding: const EdgeInsets.only(bottom: 70), decoration: InputDecoration( hintText: 'basis.nickname'.tr(), ), ), - SizedBox(height: 10), + const SizedBox(height: 10), BlocBuilder, FieldCubitState>( bloc: context.read().isVisible, builder: (context, state) { @@ -406,7 +407,7 @@ class InitializingPage extends StatelessWidget { obscureText: !isVisible, formFieldCubit: context.read().password, textAlign: TextAlign.center, - scrollPadding: EdgeInsets.only(bottom: 70), + scrollPadding: const EdgeInsets.only(bottom: 70), decoration: InputDecoration( hintText: 'basis.password'.tr(), suffixIcon: IconButton( @@ -418,14 +419,14 @@ class InitializingPage extends StatelessWidget { .isVisible .setValue(!isVisible), ), - suffixIconConstraints: BoxConstraints(minWidth: 60), - prefixIconConstraints: BoxConstraints(maxWidth: 85), + suffixIconConstraints: const BoxConstraints(minWidth: 60), + prefixIconConstraints: const BoxConstraints(maxWidth: 85), prefixIcon: Container(), ), ); }, ), - Spacer(), + const Spacer(), BrandButton.rised( onPressed: formCubitState.isSubmitting ? null @@ -445,11 +446,11 @@ class InitializingPage extends StatelessWidget { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Spacer(flex: 2), + const Spacer(flex: 2), BrandText.h2('initializing.final'.tr()), - SizedBox(height: 10), + const SizedBox(height: 10), BrandText.body2('initializing.11'.tr()), - Spacer(), + const Spacer(), BrandButton.rised( onPressed: isLoading ? null @@ -484,14 +485,14 @@ class InitializingPage extends StatelessWidget { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox(height: 15), + const SizedBox(height: 15), BrandText.h4( - 'initializing.checks'.tr(args: [doneCount.toString(), "4"]), + 'initializing.checks'.tr(args: [doneCount.toString(), '4']), ), - Spacer(flex: 2), - SizedBox(height: 10), + const Spacer(flex: 2), + const SizedBox(height: 10), BrandText.body2(text), - SizedBox(height: 10), + const SizedBox(height: 10), if (doneCount == 0 && state.dnsMatches != null) Column( children: state.dnsMatches!.entries.map((entry) { @@ -499,15 +500,16 @@ class InitializingPage extends StatelessWidget { var isCorrect = entry.value; return Row( children: [ - if (isCorrect) Icon(Icons.check, color: Colors.green), - if (!isCorrect) Icon(Icons.schedule, color: Colors.amber), - SizedBox(width: 10), + 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(), ), - SizedBox(height: 10), + const SizedBox(height: 10), if (!state.isLoading) Row( children: [ 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 de5112e2..7496ef37 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,7 +1,7 @@ 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/FilledButton.dart'; +import 'package:selfprivacy/ui/components/brand_button/filled_button.dart'; import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; import 'package:selfprivacy/utils/route_transitions/basic.dart'; import 'package:cubit_form/cubit_form.dart'; @@ -9,20 +9,22 @@ import 'package:selfprivacy/logic/cubit/server_installation/server_installation_ import 'package:selfprivacy/logic/cubit/forms/factories/field_cubit_factory.dart'; class RecoverByNewDeviceKeyInstruction extends StatelessWidget { + const RecoverByNewDeviceKeyInstruction({Key? key}) : super(key: key); + @override Widget build(BuildContext context) { return BrandHeroScreen( - heroTitle: "recovering.recovery_main_header".tr(), - heroSubtitle: "recovering.method_device_description".tr(), + heroTitle: 'recovering.recovery_main_header'.tr(), + heroSubtitle: 'recovering.method_device_description'.tr(), hasBackButton: true, hasFlashButton: false, onBackButtonPressed: () => context.read().revertRecoveryStep(), children: [ FilledButton( - title: "recovering.method_device_button".tr(), + title: 'recovering.method_device_button'.tr(), onPressed: () => Navigator.of(context) - .push(materialRoute(RecoverByNewDeviceKeyInput())), + .push(materialRoute(const RecoverByNewDeviceKeyInput())), ) ], ); @@ -30,6 +32,8 @@ class RecoverByNewDeviceKeyInstruction extends StatelessWidget { } class RecoverByNewDeviceKeyInput extends StatelessWidget { + const RecoverByNewDeviceKeyInput({Key? key}) : super(key: key); + @override Widget build(BuildContext context) { var appConfig = context.watch(); @@ -43,7 +47,7 @@ class RecoverByNewDeviceKeyInput extends StatelessWidget { child: BlocListener( listener: (context, state) { if (state is ServerInstallationRecovery && - state.currentStep != RecoveryStep.NewDeviceKey) { + state.currentStep != RecoveryStep.newDeviceKey) { Navigator.of(context).pop(); } }, @@ -52,8 +56,8 @@ class RecoverByNewDeviceKeyInput extends StatelessWidget { var formCubitState = context.watch().state; return BrandHeroScreen( - heroTitle: "recovering.recovery_main_header".tr(), - heroSubtitle: "recovering.method_device_input_description".tr(), + heroTitle: 'recovering.recovery_main_header'.tr(), + heroSubtitle: 'recovering.method_device_input_description'.tr(), hasBackButton: true, hasFlashButton: false, children: [ @@ -61,14 +65,14 @@ class RecoverByNewDeviceKeyInput extends StatelessWidget { formFieldCubit: context.read().tokenField, decoration: InputDecoration( - border: OutlineInputBorder(), + border: const OutlineInputBorder(), labelText: - "recovering.method_device_input_placeholder".tr(), + 'recovering.method_device_input_placeholder'.tr(), ), ), - SizedBox(height: 16), + const SizedBox(height: 16), FilledButton( - title: "more.continue".tr(), + title: 'more.continue'.tr(), onPressed: formCubitState.isSubmitting ? null : () => 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 363519c9..ea267a39 100644 --- a/lib/ui/pages/setup/recovering/recover_by_old_token.dart +++ b/lib/ui/pages/setup/recovering/recover_by_old_token.dart @@ -1,7 +1,7 @@ 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/FilledButton.dart'; +import 'package:selfprivacy/ui/components/brand_button/filled_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:cubit_form/cubit_form.dart'; @@ -10,19 +10,20 @@ import 'package:selfprivacy/logic/cubit/forms/factories/field_cubit_factory.dart class RecoverByOldTokenInstruction extends StatelessWidget { @override - RecoverByOldTokenInstruction({required this.instructionFilename}); + const RecoverByOldTokenInstruction({required this.instructionFilename}); + @override Widget build(BuildContext context) { return BlocListener( listener: (context, state) { if (state is ServerInstallationRecovery && - state.currentStep != RecoveryStep.Selecting) { + state.currentStep != RecoveryStep.selecting) { Navigator.of(context).pop(); Navigator.of(context).pop(); } }, child: BrandHeroScreen( - heroTitle: "recovering.recovery_main_header".tr(), + heroTitle: 'recovering.recovery_main_header'.tr(), hasBackButton: true, hasFlashButton: false, onBackButtonPressed: () => @@ -31,9 +32,9 @@ class RecoverByOldTokenInstruction extends StatelessWidget { BrandMarkdown( fileName: instructionFilename, ), - SizedBox(height: 18), + const SizedBox(height: 18), FilledButton( - title: "recovering.method_device_button".tr(), + title: 'recovering.method_device_button'.tr(), onPressed: () => context .read() .selectRecoveryMethod(ServerRecoveryMethods.oldToken), @@ -47,6 +48,8 @@ class RecoverByOldTokenInstruction extends StatelessWidget { } class RecoverByOldToken extends StatelessWidget { + const RecoverByOldToken({Key? key}) : super(key: key); + @override Widget build(BuildContext context) { var appConfig = context.watch(); @@ -62,8 +65,8 @@ class RecoverByOldToken extends StatelessWidget { var formCubitState = context.watch().state; return BrandHeroScreen( - heroTitle: "recovering.recovery_main_header".tr(), - heroSubtitle: "recovering.method_device_input_description".tr(), + heroTitle: 'recovering.recovery_main_header'.tr(), + heroSubtitle: 'recovering.method_device_input_description'.tr(), hasBackButton: true, hasFlashButton: false, children: [ @@ -71,13 +74,13 @@ class RecoverByOldToken extends StatelessWidget { formFieldCubit: context.read().tokenField, decoration: InputDecoration( - border: OutlineInputBorder(), - labelText: "recovering.method_device_input_placeholder".tr(), + border: const OutlineInputBorder(), + labelText: 'recovering.method_device_input_placeholder'.tr(), ), ), - SizedBox(height: 18), + const SizedBox(height: 18), FilledButton( - title: "more.continue".tr(), + title: 'more.continue'.tr(), onPressed: formCubitState.isSubmitting ? null : () => context.read().trySubmit(), 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 34969b25..c9bd2439 100644 --- a/lib/ui/pages/setup/recovering/recover_by_recovery_key.dart +++ b/lib/ui/pages/setup/recovering/recover_by_recovery_key.dart @@ -4,10 +4,12 @@ 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/FilledButton.dart'; +import 'package:selfprivacy/ui/components/brand_button/filled_button.dart'; import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; class RecoverByRecoveryKey extends StatelessWidget { + const RecoverByRecoveryKey({Key? key}) : super(key: key); + @override Widget build(BuildContext context) { var appConfig = context.watch(); @@ -23,8 +25,8 @@ class RecoverByRecoveryKey extends StatelessWidget { var formCubitState = context.watch().state; return BrandHeroScreen( - heroTitle: "recovering.recovery_main_header".tr(), - heroSubtitle: "recovering.method_recovery_input_description".tr(), + heroTitle: 'recovering.recovery_main_header'.tr(), + heroSubtitle: 'recovering.method_recovery_input_description'.tr(), hasBackButton: true, hasFlashButton: false, onBackButtonPressed: () => @@ -34,13 +36,13 @@ class RecoverByRecoveryKey extends StatelessWidget { formFieldCubit: context.read().tokenField, decoration: InputDecoration( - border: OutlineInputBorder(), - labelText: "recovering.method_device_input_placeholder".tr(), + border: const OutlineInputBorder(), + labelText: 'recovering.method_device_input_placeholder'.tr(), ), ), - SizedBox(height: 16), + const SizedBox(height: 16), FilledButton( - title: "more.continue".tr(), + title: 'more.continue'.tr(), onPressed: formCubitState.isSubmitting ? null : () => context.read().trySubmit(), diff --git a/lib/ui/pages/setup/recovering/recovery_confirm_backblaze.dart b/lib/ui/pages/setup/recovering/recovery_confirm_backblaze.dart index 6c1779e1..63e3a019 100644 --- a/lib/ui/pages/setup/recovering/recovery_confirm_backblaze.dart +++ b/lib/ui/pages/setup/recovering/recovery_confirm_backblaze.dart @@ -10,6 +10,8 @@ import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.da import 'package:selfprivacy/ui/components/brand_md/brand_md.dart'; class RecoveryConfirmBackblaze extends StatelessWidget { + const RecoveryConfirmBackblaze({Key? key}) : super(key: key); + @override Widget build(BuildContext context) { var appConfig = context.watch(); @@ -20,17 +22,17 @@ class RecoveryConfirmBackblaze extends StatelessWidget { var formCubitState = context.watch().state; return BrandHeroScreen( - heroTitle: "recovering.confirm_backblaze".tr(), - heroSubtitle: "recovering.confirm_backblaze_description".tr(), + heroTitle: 'recovering.confirm_backblaze'.tr(), + heroSubtitle: 'recovering.confirm_backblaze_description'.tr(), hasBackButton: true, hasFlashButton: false, children: [ CubitFormTextField( formFieldCubit: context.read().keyId, textAlign: TextAlign.center, - scrollPadding: EdgeInsets.only(bottom: 70), - decoration: InputDecoration( - border: const OutlineInputBorder(), + scrollPadding: const EdgeInsets.only(bottom: 70), + decoration: const InputDecoration( + border: OutlineInputBorder(), hintText: 'KeyID', ), ), @@ -38,9 +40,9 @@ class RecoveryConfirmBackblaze extends StatelessWidget { CubitFormTextField( formFieldCubit: context.read().applicationKey, textAlign: TextAlign.center, - scrollPadding: EdgeInsets.only(bottom: 70), - decoration: InputDecoration( - border: const OutlineInputBorder(), + scrollPadding: const EdgeInsets.only(bottom: 70), + decoration: const InputDecoration( + border: OutlineInputBorder(), hintText: 'Master Application Key', ), ), @@ -58,7 +60,7 @@ class RecoveryConfirmBackblaze extends StatelessWidget { isScrollControlled: true, backgroundColor: Colors.transparent, builder: (BuildContext context) { - return BrandBottomSheet( + return const BrandBottomSheet( isExpended: true, child: Padding( padding: paddingH15V0, diff --git a/lib/ui/pages/setup/recovering/recovery_confirm_cloudflare.dart b/lib/ui/pages/setup/recovering/recovery_confirm_cloudflare.dart index a0966f4f..19dce048 100644 --- a/lib/ui/pages/setup/recovering/recovery_confirm_cloudflare.dart +++ b/lib/ui/pages/setup/recovering/recovery_confirm_cloudflare.dart @@ -10,6 +10,8 @@ import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.da import 'package:selfprivacy/ui/components/brand_md/brand_md.dart'; class RecoveryConfirmCloudflare extends StatelessWidget { + const RecoveryConfirmCloudflare({Key? key}) : super(key: key); + @override Widget build(BuildContext context) { var appConfig = context.watch(); @@ -20,9 +22,9 @@ class RecoveryConfirmCloudflare extends StatelessWidget { var formCubitState = context.watch().state; return BrandHeroScreen( - heroTitle: "recovering.confirm_cloudflare".tr(), - heroSubtitle: "recovering.confirm_cloudflare_description".tr(args: [ - appConfig.state.serverDomain?.domainName ?? "your domain" + heroTitle: 'recovering.confirm_cloudflare'.tr(), + heroSubtitle: 'recovering.confirm_cloudflare_description'.tr(args: [ + appConfig.state.serverDomain?.domainName ?? 'your domain' ]), hasBackButton: true, hasFlashButton: false, @@ -30,7 +32,7 @@ class RecoveryConfirmCloudflare extends StatelessWidget { CubitFormTextField( formFieldCubit: context.read().apiKey, textAlign: TextAlign.center, - scrollPadding: EdgeInsets.only(bottom: 70), + scrollPadding: const EdgeInsets.only(bottom: 70), decoration: InputDecoration( border: const OutlineInputBorder(), hintText: 'initializing.5'.tr(), @@ -50,7 +52,7 @@ class RecoveryConfirmCloudflare extends StatelessWidget { isScrollControlled: true, backgroundColor: Colors.transparent, builder: (BuildContext context) { - return BrandBottomSheet( + return const BrandBottomSheet( isExpended: true, child: Padding( padding: paddingH15V0, diff --git a/lib/ui/pages/setup/recovering/recovery_confirm_server.dart b/lib/ui/pages/setup/recovering/recovery_confirm_server.dart index 8a5c45c3..0f03090e 100644 --- a/lib/ui/pages/setup/recovering/recovery_confirm_server.dart +++ b/lib/ui/pages/setup/recovering/recovery_confirm_server.dart @@ -1,9 +1,8 @@ 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/json/hetzner_server_info.dart'; import 'package:selfprivacy/logic/models/server_basic_info.dart'; -import 'package:selfprivacy/ui/components/brand_button/FilledButton.dart'; +import 'package:selfprivacy/ui/components/brand_button/filled_button.dart'; import 'package:selfprivacy/ui/components/brand_button/brand_button.dart'; import 'package:selfprivacy/ui/components/brand_cards/brand_cards.dart'; import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; @@ -75,7 +74,7 @@ class _RecoveryConfirmServerState extends State { ], ); } else { - return Center( + return const Center( child: CircularProgressIndicator(), ); } @@ -90,26 +89,24 @@ class _RecoveryConfirmServerState extends State { ServerBasicInfoWithValidators server, bool showMoreServersButton, ) { - return Container( - child: Column( - children: [ - _ServerCard( - context: context, - server: server, + return Column( + children: [ + _ServerCard( + context: context, + server: server, + ), + const SizedBox(height: 16), + FilledButton( + title: 'recovering.confirm_server_accept'.tr(), + onPressed: () => _showConfirmationDialog(context, server), + ), + const SizedBox(height: 16), + if (showMoreServersButton) + BrandButton.text( + title: 'recovering.confirm_server_decline'.tr(), + onPressed: () => setState(() => _isExtended = true), ), - SizedBox(height: 16), - FilledButton( - title: 'recovering.confirm_server_accept'.tr(), - onPressed: () => _showConfirmationDialog(context, server), - ), - SizedBox(height: 16), - if (showMoreServersButton) - BrandButton.text( - title: 'recovering.confirm_server_decline'.tr(), - onPressed: () => setState(() => _isExtended = true), - ), - ], - ), + ], ); } @@ -138,7 +135,7 @@ class _RecoveryConfirmServerState extends State { child: ListTile( onTap: onTap, title: Text(server.name), - leading: Icon(Icons.dns), + leading: const Icon(Icons.dns), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ diff --git a/lib/ui/pages/setup/recovering/recovery_hentzner_connected.dart b/lib/ui/pages/setup/recovering/recovery_hentzner_connected.dart index f49eb982..04093aed 100644 --- a/lib/ui/pages/setup/recovering/recovery_hentzner_connected.dart +++ b/lib/ui/pages/setup/recovering/recovery_hentzner_connected.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import 'package:selfprivacy/config/brand_theme.dart'; import 'package:selfprivacy/logic/cubit/forms/setup/initializing/hetzner_form_cubit.dart'; import 'package:selfprivacy/ui/components/brand_bottom_sheet/brand_bottom_sheet.dart'; -import 'package:selfprivacy/ui/components/brand_button/FilledButton.dart'; +import 'package:selfprivacy/ui/components/brand_button/filled_button.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:cubit_form/cubit_form.dart'; @@ -11,6 +11,8 @@ import 'package:selfprivacy/logic/cubit/server_installation/server_installation_ import 'package:selfprivacy/ui/components/brand_md/brand_md.dart'; class RecoveryHetznerConnected extends StatelessWidget { + const RecoveryHetznerConnected({Key? key}) : super(key: key); + @override Widget build(BuildContext context) { var appConfig = context.watch(); @@ -22,9 +24,9 @@ class RecoveryHetznerConnected extends StatelessWidget { var formCubitState = context.watch().state; return BrandHeroScreen( - heroTitle: "recovering.hetzner_connected".tr(), - heroSubtitle: "recovering.hetzner_connected_description".tr(args: [ - appConfig.state.serverDomain?.domainName ?? "your domain" + heroTitle: 'recovering.hetzner_connected'.tr(), + heroSubtitle: 'recovering.hetzner_connected_description'.tr(args: [ + appConfig.state.serverDomain?.domainName ?? 'your domain' ]), hasBackButton: true, hasFlashButton: false, @@ -32,18 +34,18 @@ class RecoveryHetznerConnected extends StatelessWidget { CubitFormTextField( formFieldCubit: context.read().apiKey, decoration: InputDecoration( - border: OutlineInputBorder(), - labelText: "recovering.hetzner_connected_placeholder".tr(), + border: const OutlineInputBorder(), + labelText: 'recovering.hetzner_connected_placeholder'.tr(), ), ), - SizedBox(height: 16), + const SizedBox(height: 16), FilledButton( - title: "more.continue".tr(), + title: 'more.continue'.tr(), onPressed: formCubitState.isSubmitting ? null : () => context.read().trySubmit(), ), - SizedBox(height: 16), + const SizedBox(height: 16), BrandButton.text( title: 'initializing.how'.tr(), onPressed: () => showModalBottomSheet( @@ -51,7 +53,7 @@ class RecoveryHetznerConnected extends StatelessWidget { isScrollControlled: true, backgroundColor: Colors.transparent, builder: (BuildContext context) { - return BrandBottomSheet( + return const BrandBottomSheet( isExpended: true, child: Padding( padding: paddingH15V0, diff --git a/lib/ui/pages/setup/recovering/recovery_method_select.dart b/lib/ui/pages/setup/recovering/recovery_method_select.dart index 57c336b8..ed768c5a 100644 --- a/lib/ui/pages/setup/recovering/recovery_method_select.dart +++ b/lib/ui/pages/setup/recovering/recovery_method_select.dart @@ -8,42 +8,44 @@ import 'package:selfprivacy/ui/pages/setup/recovering/recover_by_old_token.dart' import 'package:selfprivacy/utils/route_transitions/basic.dart'; class RecoveryMethodSelect extends StatelessWidget { + const RecoveryMethodSelect({Key? key}) : super(key: key); + @override Widget build(BuildContext context) { return BrandHeroScreen( - heroTitle: "recovering.recovery_main_header".tr(), - heroSubtitle: "recovering.method_select_description".tr(), + heroTitle: 'recovering.recovery_main_header'.tr(), + heroSubtitle: 'recovering.method_select_description'.tr(), hasBackButton: true, hasFlashButton: false, children: [ BrandCards.outlined( child: ListTile( title: Text( - "recovering.method_select_other_device".tr(), + 'recovering.method_select_other_device'.tr(), style: Theme.of(context).textTheme.titleMedium, ), - leading: Icon(Icons.offline_share_outlined), + leading: const Icon(Icons.offline_share_outlined), onTap: () => context .read() .selectRecoveryMethod(ServerRecoveryMethods.newDeviceKey), ), ), - SizedBox(height: 16), + const SizedBox(height: 16), BrandCards.outlined( child: ListTile( title: Text( - "recovering.method_select_recovery_key".tr(), + 'recovering.method_select_recovery_key'.tr(), style: Theme.of(context).textTheme.titleMedium, ), - leading: Icon(Icons.password_outlined), + leading: const Icon(Icons.password_outlined), onTap: () => context .read() .selectRecoveryMethod(ServerRecoveryMethods.recoveryKey), ), ), - SizedBox(height: 16), + const SizedBox(height: 16), BrandButton.text( - title: "recovering.method_select_nothing".tr(), + title: 'recovering.method_select_nothing'.tr(), onPressed: () => Navigator.of(context) .push(materialRoute(RecoveryFallbackMethodSelect())), ) @@ -53,55 +55,57 @@ class RecoveryMethodSelect extends StatelessWidget { } class RecoveryFallbackMethodSelect extends StatelessWidget { + const RecoveryFallbackMethodSelect({Key? key}) : super(key: key); + @override Widget build(BuildContext context) { return BrandHeroScreen( - heroTitle: "recovering.recovery_main_header".tr(), - heroSubtitle: "recovering.fallback_select_description".tr(), + heroTitle: 'recovering.recovery_main_header'.tr(), + heroSubtitle: 'recovering.fallback_select_description'.tr(), hasBackButton: true, hasFlashButton: false, children: [ BrandCards.outlined( child: ListTile( title: Text( - "recovering.fallback_select_token_copy".tr(), + 'recovering.fallback_select_token_copy'.tr(), style: Theme.of(context).textTheme.titleMedium, ), - leading: Icon(Icons.vpn_key), + leading: const Icon(Icons.vpn_key), onTap: () => Navigator.of(context) - .push(materialRoute(RecoverByOldTokenInstruction( + .push(materialRoute(const RecoverByOldTokenInstruction( instructionFilename: 'how_fallback_old', ))), ), ), - SizedBox(height: 16), + const SizedBox(height: 16), BrandCards.outlined( child: ListTile( title: Text( - "recovering.fallback_select_root_ssh".tr(), + 'recovering.fallback_select_root_ssh'.tr(), style: Theme.of(context).textTheme.titleMedium, ), - leading: Icon(Icons.terminal), + leading: const Icon(Icons.terminal), onTap: () => Navigator.of(context) - .push(materialRoute(RecoverByOldTokenInstruction( + .push(materialRoute(const RecoverByOldTokenInstruction( instructionFilename: 'how_fallback_ssh', ))), ), ), - SizedBox(height: 16), + const SizedBox(height: 16), BrandCards.outlined( child: ListTile( title: Text( - "recovering.fallback_select_provider_console".tr(), + 'recovering.fallback_select_provider_console'.tr(), style: Theme.of(context).textTheme.titleMedium, ), subtitle: Text( - "recovering.fallback_select_provider_console_hint".tr(), + 'recovering.fallback_select_provider_console_hint'.tr(), style: Theme.of(context).textTheme.bodyMedium, ), - leading: Icon(Icons.web), + leading: const Icon(Icons.web), onTap: () => Navigator.of(context) - .push(materialRoute(RecoverByOldTokenInstruction( + .push(materialRoute(const RecoverByOldTokenInstruction( instructionFilename: 'how_fallback_terminal', ))), ), diff --git a/lib/ui/pages/setup/recovering/recovery_routing.dart b/lib/ui/pages/setup/recovering/recovery_routing.dart index 743e4aab..7ce718fd 100644 --- a/lib/ui/pages/setup/recovering/recovery_routing.dart +++ b/lib/ui/pages/setup/recovering/recovery_routing.dart @@ -4,7 +4,7 @@ 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/FilledButton.dart'; +import 'package:selfprivacy/ui/components/brand_button/filled_button.dart'; import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.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'; @@ -16,51 +16,56 @@ import 'package:selfprivacy/ui/pages/setup/recovering/recovery_hentzner_connecte import 'package:selfprivacy/ui/pages/setup/recovering/recovery_method_select.dart'; class RecoveryRouting extends StatelessWidget { + const RecoveryRouting({Key? key}) : super(key: key); + @override Widget build(BuildContext context) { var serverInstallation = context.watch().state; - Widget currentPage = SelectDomainToRecover(); + Widget currentPage = const SelectDomainToRecover(); if (serverInstallation is ServerInstallationRecovery) { switch (serverInstallation.currentStep) { - case RecoveryStep.Selecting: + case RecoveryStep.selecting: if (serverInstallation.recoveryCapabilities != - ServerRecoveryCapabilities.none) - currentPage = RecoveryMethodSelect(); + ServerRecoveryCapabilities.none) { + currentPage = const RecoveryMethodSelect(); + } break; - case RecoveryStep.RecoveryKey: - currentPage = RecoverByRecoveryKey(); + case RecoveryStep.recoveryKey: + currentPage = const RecoverByRecoveryKey(); break; - case RecoveryStep.NewDeviceKey: + case RecoveryStep.newDeviceKey: currentPage = RecoverByNewDeviceKeyInstruction(); break; - case RecoveryStep.OldToken: + case RecoveryStep.oldToken: currentPage = RecoverByOldToken(); break; - case RecoveryStep.HetznerToken: - currentPage = RecoveryHetznerConnected(); + case RecoveryStep.hetznerToken: + currentPage = const RecoveryHetznerConnected(); break; - case RecoveryStep.ServerSelection: - currentPage = RecoveryConfirmServer(); + case RecoveryStep.serverSelection: + currentPage = const RecoveryConfirmServer(); break; - case RecoveryStep.CloudflareToken: - currentPage = RecoveryConfirmCloudflare(); + case RecoveryStep.cloudflareToken: + currentPage = const RecoveryConfirmCloudflare(); break; - case RecoveryStep.BackblazeToken: - currentPage = RecoveryConfirmBackblaze(); + case RecoveryStep.backblazeToken: + currentPage = const RecoveryConfirmBackblaze(); break; } } return AnimatedSwitcher( - duration: Duration(milliseconds: 300), + duration: const Duration(milliseconds: 300), child: currentPage, ); } } class SelectDomainToRecover extends StatelessWidget { + const SelectDomainToRecover({Key? key}) : super(key: key); + @override Widget build(BuildContext context) { var serverInstallation = context.watch(); @@ -75,19 +80,19 @@ class SelectDomainToRecover extends StatelessWidget { return BlocListener( listener: (context, state) { if (state is ServerInstallationRecovery) { - if (state.currentStep == RecoveryStep.Selecting) { + if (state.currentStep == RecoveryStep.selecting) { if (state.recoveryCapabilities == ServerRecoveryCapabilities.none) { context .read() - .setCustomError("recovering.domain_recover_error".tr()); + .setCustomError('recovering.domain_recover_error'.tr()); } } } }, child: BrandHeroScreen( - heroTitle: "recovering.recovery_main_header".tr(), - heroSubtitle: "recovering.domain_recovery_description".tr(), + heroTitle: 'recovering.recovery_main_header'.tr(), + heroSubtitle: 'recovering.domain_recovery_description'.tr(), hasBackButton: true, hasFlashButton: false, onBackButtonPressed: @@ -99,13 +104,13 @@ class SelectDomainToRecover extends StatelessWidget { formFieldCubit: context.read().serverDomainField, decoration: InputDecoration( - border: OutlineInputBorder(), - labelText: "recovering.domain_recover_placeholder".tr(), + border: const OutlineInputBorder(), + labelText: 'recovering.domain_recover_placeholder'.tr(), ), ), - SizedBox(height: 16), + const SizedBox(height: 16), FilledButton( - title: "more.continue".tr(), + title: 'more.continue'.tr(), onPressed: formCubitState.isSubmitting ? null : () => diff --git a/lib/ui/pages/ssh_keys/new_ssh_key.dart b/lib/ui/pages/ssh_keys/new_ssh_key.dart index 4d7a3625..abeda0db 100644 --- a/lib/ui/pages/ssh_keys/new_ssh_key.dart +++ b/lib/ui/pages/ssh_keys/new_ssh_key.dart @@ -3,7 +3,7 @@ part of 'ssh_keys.dart'; class _NewSshKey extends StatelessWidget { final User user; - _NewSshKey(this.user); + const _NewSshKey(this.user); @override Widget build(BuildContext context) { @@ -14,11 +14,11 @@ class _NewSshKey extends StatelessWidget { var jobState = jobCubit.state; if (jobState is JobsStateWithJobs) { var jobs = jobState.jobList; - jobs.forEach((job) { + for (var job in jobs) { if (job is CreateSSHKeyJob && job.user.login == user.login) { user.sshKeys.add(job.publicKey); } - }); + } } return SshFormCubit( jobsCubit: jobCubit, @@ -41,7 +41,7 @@ class _NewSshKey extends StatelessWidget { BrandHeader( title: user.login, ), - SizedBox(width: 14), + const SizedBox(width: 14), Padding( padding: paddingH15V0, child: Column( @@ -55,14 +55,14 @@ class _NewSshKey extends StatelessWidget { ), ), ), - SizedBox(height: 30), + const SizedBox(height: 30), BrandButton.rised( onPressed: formCubitState.isSubmitting ? null : () => context.read().trySubmit(), text: 'ssh.create'.tr(), ), - SizedBox(height: 30), + const SizedBox(height: 30), ], ), ), diff --git a/lib/ui/pages/ssh_keys/ssh_keys.dart b/lib/ui/pages/ssh_keys/ssh_keys.dart index 3967bf65..c527a972 100644 --- a/lib/ui/pages/ssh_keys/ssh_keys.dart +++ b/lib/ui/pages/ssh_keys/ssh_keys.dart @@ -21,7 +21,7 @@ part 'new_ssh_key.dart'; class SshKeysPage extends StatefulWidget { final User user; - SshKeysPage({Key? key, required this.user}) : super(key: key); + const SshKeysPage({Key? key, required this.user}) : super(key: key); @override _SshKeysPageState createState() => _SshKeysPageState(); @@ -59,7 +59,7 @@ class _SshKeysPageState extends State { 'ssh.create'.tr(), style: Theme.of(context).textTheme.headline6, ), - leading: Icon(Icons.add_circle_outline_rounded), + leading: const Icon(Icons.add_circle_outline_rounded), onTap: () { showModalBottomSheet( context: context, @@ -73,7 +73,7 @@ class _SshKeysPageState extends State { ); }, ), - Divider(height: 0), + const Divider(height: 0), // show a list of ListTiles with ssh keys // Clicking on one should delete it Column( @@ -108,13 +108,13 @@ class _SshKeysPageState extends State { TextButton( child: Text('basis.cancel'.tr()), onPressed: () { - Navigator.of(context)..pop(); + Navigator.of(context).pop(); }, ), TextButton( child: Text( 'basis.delete'.tr(), - style: TextStyle( + style: const TextStyle( color: BrandColors.red1, ), ), diff --git a/lib/ui/pages/users/empty.dart b/lib/ui/pages/users/empty.dart index e9623403..2e5c5906 100644 --- a/lib/ui/pages/users/empty.dart +++ b/lib/ui/pages/users/empty.dart @@ -12,19 +12,19 @@ class _NoUsers extends StatelessWidget { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(BrandIcons.users, size: 50, color: BrandColors.grey7), - SizedBox(height: 20), + const Icon(BrandIcons.users, size: 50, color: BrandColors.grey7), + const SizedBox(height: 20), BrandText.h2( 'users.nobody_here'.tr(), - style: TextStyle( + style: const TextStyle( color: BrandColors.grey7, ), ), - SizedBox(height: 10), + const SizedBox(height: 10), BrandText.medium( text, textAlign: TextAlign.center, - style: TextStyle( + style: const TextStyle( color: BrandColors.grey7, ), ), diff --git a/lib/ui/pages/users/fab.dart b/lib/ui/pages/users/fab.dart index d9a5a0ea..e6f0ed29 100644 --- a/lib/ui/pages/users/fab.dart +++ b/lib/ui/pages/users/fab.dart @@ -5,15 +5,15 @@ class _Fab extends StatelessWidget { @override Widget build(BuildContext context) { - return Container( + return SizedBox( width: 48.0, height: 48.0, child: RawMaterialButton( fillColor: BrandColors.blue, - shape: CircleBorder(), + shape: const CircleBorder(), elevation: 0.0, highlightElevation: 2, - child: Icon( + child: const Icon( Icons.add, color: Colors.white, size: 34, diff --git a/lib/ui/pages/users/new_user.dart b/lib/ui/pages/users/new_user.dart index 05136b66..9336d5c2 100644 --- a/lib/ui/pages/users/new_user.dart +++ b/lib/ui/pages/users/new_user.dart @@ -16,11 +16,11 @@ class _NewUser extends StatelessWidget { users.addAll(context.read().state.users); if (jobState is JobsStateWithJobs) { var jobs = jobState.jobList; - jobs.forEach((job) { + for (var job in jobs) { if (job is CreateUserJob) { users.add(job.user); } - }); + } } return UserFormCubit( jobsCubit: jobCubit, @@ -43,7 +43,7 @@ class _NewUser extends StatelessWidget { BrandHeader( title: 'users.new_user'.tr(), ), - SizedBox(width: 14), + const SizedBox(width: 14), Padding( padding: paddingH15V0, child: Column( @@ -58,7 +58,7 @@ class _NewUser extends StatelessWidget { ), ), ), - SizedBox(height: 20), + const SizedBox(height: 20), CubitFormTextField( formFieldCubit: context.read().password, decoration: InputDecoration( @@ -67,7 +67,7 @@ class _NewUser extends StatelessWidget { suffixIcon: Padding( padding: const EdgeInsets.only(right: 8), child: IconButton( - icon: Icon( + icon: const Icon( BrandIcons.refresh, color: BrandColors.blue, ), @@ -77,16 +77,16 @@ class _NewUser extends StatelessWidget { ), ), ), - SizedBox(height: 30), + const SizedBox(height: 30), BrandButton.rised( onPressed: formCubitState.isSubmitting ? null : () => context.read().trySubmit(), text: 'basis.create'.tr(), ), - SizedBox(height: 40), + const SizedBox(height: 40), Text('users.new_user_info_note'.tr()), - SizedBox(height: 30), + const SizedBox(height: 30), ], ), ), diff --git a/lib/ui/pages/users/user.dart b/lib/ui/pages/users/user.dart index a748a374..c59d53fc 100644 --- a/lib/ui/pages/users/user.dart +++ b/lib/ui/pages/users/user.dart @@ -30,7 +30,7 @@ class _User extends StatelessWidget { shape: BoxShape.circle, ), ), - SizedBox(width: 20), + const SizedBox(width: 20), Flexible( child: isRootUser ? BrandText.h4Underlined(user.login) @@ -38,7 +38,8 @@ class _User extends StatelessWidget { : BrandText.h4(user.login, style: user.isFoundOnServer ? null - : TextStyle(decoration: TextDecoration.lineThrough)), + : const TextStyle( + decoration: TextDecoration.lineThrough)), ), ], ), diff --git a/lib/ui/pages/users/user_details.dart b/lib/ui/pages/users/user_details.dart index 69f3b87e..bd7cbb2d 100644 --- a/lib/ui/pages/users/user_details.dart +++ b/lib/ui/pages/users/user_details.dart @@ -25,7 +25,7 @@ class _UserDetails extends StatelessWidget { height: 200, decoration: BoxDecoration( color: user.color, - borderRadius: BorderRadius.vertical( + borderRadius: const BorderRadius.vertical( top: Radius.circular(20), ), ), @@ -36,7 +36,7 @@ class _UserDetails extends StatelessWidget { Align( alignment: Alignment.centerRight, child: Padding( - padding: EdgeInsets.symmetric( + padding: const EdgeInsets.symmetric( vertical: 4, horizontal: 2, ), @@ -64,13 +64,13 @@ class _UserDetails extends StatelessWidget { TextButton( child: Text('basis.cancel'.tr()), onPressed: () { - Navigator.of(context)..pop(); + Navigator.of(context).pop(); }, ), TextButton( child: Text( 'basis.delete'.tr(), - style: TextStyle( + style: const TextStyle( color: BrandColors.red1, ), ), @@ -89,7 +89,7 @@ class _UserDetails extends StatelessWidget { break; } }, - icon: Icon(Icons.more_vert), + icon: const Icon(Icons.more_vert), itemBuilder: (BuildContext context) => [ // PopupMenuItem( // value: PopupMenuItemType.reset, @@ -101,10 +101,10 @@ class _UserDetails extends StatelessWidget { PopupMenuItem( value: PopupMenuItemType.delete, child: Container( - padding: EdgeInsets.only(left: 5), + padding: const EdgeInsets.only(left: 5), child: Text( 'basis.delete'.tr(), - style: TextStyle(color: BrandColors.red1), + style: const TextStyle(color: BrandColors.red1), ), ), ), @@ -112,9 +112,9 @@ class _UserDetails extends StatelessWidget { ), ), ), - Spacer(), + const Spacer(), Padding( - padding: EdgeInsets.symmetric( + padding: const EdgeInsets.symmetric( vertical: 20, horizontal: 15, ), @@ -129,7 +129,7 @@ class _UserDetails extends StatelessWidget { ], ), ), - SizedBox(height: 20), + const SizedBox(height: 20), Padding( padding: paddingH15V0.copyWith(bottom: 20), child: Column( @@ -145,7 +145,7 @@ class _UserDetails extends StatelessWidget { Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox(height: 14), + const SizedBox(height: 14), BrandText.small('basis.password'.tr()), Container( height: 40, @@ -154,21 +154,21 @@ class _UserDetails extends StatelessWidget { ), ], ), - SizedBox(height: 24), - BrandDivider(), - SizedBox(height: 20), + const SizedBox(height: 24), + const BrandDivider(), + const SizedBox(height: 20), ListTile( onTap: () { Navigator.of(context) .push(materialRoute(SshKeysPage(user: user))); }, title: Text('ssh.title'.tr()), - subtitle: user.sshKeys.length > 0 + subtitle: user.sshKeys.isNotEmpty ? Text('ssh.subtitle_with_keys' .tr(args: [user.sshKeys.length.toString()])) : Text('ssh.subtitle_without_keys'.tr()), - trailing: Icon(BrandIcons.key)), - SizedBox(height: 20), + trailing: const Icon(BrandIcons.key)), + const SizedBox(height: 20), ListTile( onTap: () { Share.share( @@ -177,7 +177,7 @@ class _UserDetails extends StatelessWidget { title: Text( 'users.send_registration_data'.tr(), ), - trailing: Icon(BrandIcons.share), + trailing: const Icon(BrandIcons.share), ), ], ), diff --git a/lib/ui/pages/users/users.dart b/lib/ui/pages/users/users.dart index f10a4afa..133dfb16 100644 --- a/lib/ui/pages/users/users.dart +++ b/lib/ui/pages/users/users.dart @@ -74,13 +74,13 @@ class UsersPage extends StatelessWidget { return Scaffold( appBar: PreferredSize( + preferredSize: const Size.fromHeight(52), child: BrandHeader( title: 'basis.users'.tr(), hasFlashButton: true, ), - preferredSize: Size.fromHeight(52), ), - floatingActionButton: isReady ? _Fab() : null, + floatingActionButton: isReady ? const _Fab() : null, body: child, ); } @@ -89,8 +89,8 @@ class UsersPage extends StatelessWidget { return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 15), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 15), child: NotReadyCard(), ), Expanded( diff --git a/lib/utils/color_utils.dart b/lib/utils/color_utils.dart index 6785c171..ac0c63ba 100644 --- a/lib/utils/color_utils.dart +++ b/lib/utils/color_utils.dart @@ -6,7 +6,7 @@ Color stringToColor(String string) { return colorPalette[index]; } -var originalColor = Color(0xFFDBD8BD); +var originalColor = const Color(0xFFDBD8BD); var count = 40; var colorPalette = List.generate( count, diff --git a/lib/utils/extensions/duration.dart b/lib/utils/extensions/duration.dart index a81627c0..49fa96b8 100644 --- a/lib/utils/extensions/duration.dart +++ b/lib/utils/extensions/duration.dart @@ -26,7 +26,7 @@ extension DurationFormatter on Duration { String toHoursMinutesSecondsFormat() { // WAT: https://flutterigniter.com/how-to-format-duration/ - return this.toString().split('.').first.padLeft(8, "0"); + return this.toString().split('.').first.padLeft(8, '0'); } String toDayHourMinuteFormat2() { @@ -36,6 +36,6 @@ extension DurationFormatter on Duration { ].map((seg) { return seg.toString().padLeft(2, '0'); }); - return segments.first + " h" + " " + segments.last + " min"; + return '${segments.first} h ${segments.last} min'; } } diff --git a/lib/utils/extensions/text_extensions.dart b/lib/utils/extensions/text_extensions.dart index 26932a11..bf810f51 100644 --- a/lib/utils/extensions/text_extensions.dart +++ b/lib/utils/extensions/text_extensions.dart @@ -3,19 +3,19 @@ import 'package:flutter/cupertino.dart'; extension TextExtension on Text { Text withColor(Color color) => Text( data!, - key: this.key, - strutStyle: this.strutStyle, - textAlign: this.textAlign, - textDirection: this.textDirection, - locale: this.locale, - softWrap: this.softWrap, - overflow: this.overflow, - textScaleFactor: this.textScaleFactor, - maxLines: this.maxLines, - semanticsLabel: this.semanticsLabel, - textWidthBasis: textWidthBasis ?? this.textWidthBasis, - style: this.style != null - ? this.style!.copyWith(color: color) + key: key, + strutStyle: strutStyle, + textAlign: textAlign, + textDirection: textDirection, + locale: locale, + softWrap: softWrap, + overflow: overflow, + textScaleFactor: textScaleFactor, + maxLines: maxLines, + semanticsLabel: semanticsLabel, + textWidthBasis: textWidthBasis ?? textWidthBasis, + style: style != null + ? style!.copyWith(color: color) : TextStyle(color: color), ); diff --git a/lib/utils/route_transitions/slide_bottom.dart b/lib/utils/route_transitions/slide_bottom.dart index 380b1142..28363e2d 100644 --- a/lib/utils/route_transitions/slide_bottom.dart +++ b/lib/utils/route_transitions/slide_bottom.dart @@ -21,7 +21,7 @@ Function transitionsBuilder = ( child: Container( decoration: animation.isCompleted ? null - : BoxDecoration( + : const BoxDecoration( border: Border( bottom: BorderSide( color: Colors.black, @@ -36,7 +36,7 @@ Function transitionsBuilder = ( class SlideBottomRoute extends PageRouteBuilder { SlideBottomRoute(this.widget) : super( - transitionDuration: Duration(milliseconds: 150), + transitionDuration: const Duration(milliseconds: 150), pageBuilder: pageBuilder(widget), transitionsBuilder: transitionsBuilder as Widget Function( BuildContext, Animation, Animation, Widget), diff --git a/lib/utils/route_transitions/slide_right.dart b/lib/utils/route_transitions/slide_right.dart index f01c4b0f..635bb021 100644 --- a/lib/utils/route_transitions/slide_right.dart +++ b/lib/utils/route_transitions/slide_right.dart @@ -21,7 +21,7 @@ Function transitionsBuilder = ( child: Container( decoration: animation.isCompleted ? null - : BoxDecoration( + : const BoxDecoration( border: Border( right: BorderSide( color: Colors.black, diff --git a/pubspec.lock b/pubspec.lock index de1631fe..e3faf1b6 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -526,7 +526,7 @@ packages: source: hosted version: "3.1.3" intl: - dependency: transitive + dependency: "direct main" description: name: intl url: "https://pub.dartlang.org" diff --git a/pubspec.yaml b/pubspec.yaml index a99c36a0..519f9996 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -29,6 +29,7 @@ dependencies: gtk_theme_fl: ^0.0.1 hive: ^2.0.5 hive_flutter: ^1.1.0 + intl: ^0.17.0 ionicons: ^0.1.2 json_annotation: ^4.4.0 local_auth: ^2.0.2 diff --git a/test/widget_test.dart b/test/widget_test.dart index aaa33419..48cbdccf 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -114,9 +114,9 @@ void main() { }); } -var regExpNewLines = RegExp(r"[\n\r]+"); -var regExpWhiteSpaces = RegExp(r"[\s]+"); -var regExpUppercaseLetters = RegExp(r"[A-Z]"); -var regExpLowercaseLetters = RegExp(r"[a-z]"); -var regExpNumbers = RegExp(r"[0-9]"); +var regExpNewLines = RegExp(r'[\n\r]+'); +var regExpWhiteSpaces = RegExp(r'[\s]+'); +var regExpUppercaseLetters = RegExp(r'[A-Z]'); +var regExpLowercaseLetters = RegExp(r'[a-z]'); +var regExpNumbers = RegExp(r'[0-9]'); var regExpSymbols = RegExp(r'(?:_|[^\w\s])+'); From 5dcaa060a1917e5e1941414070ea2035843a03f8 Mon Sep 17 00:00:00 2001 From: NaiJi Date: Wed, 25 May 2022 15:21:56 +0300 Subject: [PATCH 33/52] Linting Co-authored-by: Inex Code --- analysis_options.yaml | 4 ++++ .../cubit/forms/validations/validations.dart | 6 +++--- lib/main.dart | 7 ++++--- .../components/brand_button/brand_button.dart | 2 +- lib/ui/components/brand_md/brand_md.dart | 2 +- .../brand_tab_bar/brand_tab_bar.dart | 2 +- lib/ui/components/brand_timer/brand_timer.dart | 2 +- .../icon_status_mask/icon_status_mask.dart | 6 +++++- .../not_ready_card/not_ready_card.dart | 2 +- .../pre_styled_buttons/flash_fab.dart | 2 +- .../components/progress_bar/progress_bar.dart | 2 +- .../pages/backup_details/backup_details.dart | 2 +- lib/ui/pages/dns_details/dns_details.dart | 4 +++- .../pages/more/app_settings/app_setting.dart | 6 +++++- lib/ui/pages/more/console/console.dart | 2 +- lib/ui/pages/more/more.dart | 4 ++-- lib/ui/pages/onboarding/onboarding.dart | 2 +- lib/ui/pages/providers/providers.dart | 4 ++-- lib/ui/pages/root_route.dart | 2 +- lib/ui/pages/server_details/chart.dart | 18 +++++++++++------- lib/ui/pages/server_details/cpu_chart.dart | 11 ++++++++--- .../pages/server_details/network_charts.dart | 17 +++++++++-------- .../server_details/server_details_screen.dart | 2 +- .../pages/server_details/server_settings.dart | 5 ++++- .../server_details/time_zone/time_zone.dart | 2 +- lib/ui/pages/services/services.dart | 6 +++--- lib/ui/pages/setup/initializing.dart | 2 ++ .../setup/recovering/recover_by_old_token.dart | 4 +++- .../recovering/recovery_confirm_server.dart | 16 ++++++++-------- .../recovering/recovery_method_select.dart | 2 +- .../setup/recovering/recovery_routing.dart | 4 ++-- lib/ui/pages/ssh_keys/ssh_keys.dart | 2 +- 32 files changed, 93 insertions(+), 61 deletions(-) diff --git a/analysis_options.yaml b/analysis_options.yaml index 39d63410..11ccd2ae 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -9,6 +9,10 @@ # packages, and plugins designed to encourage good coding practices. include: package:flutter_lints/flutter.yaml +analyzer: + exclude: + - lib/generated_plugin_registrant.dart + linter: # The lint rules applied to this project can be customized in the # section below to disable rules from the `package:flutter_lints/flutter.yaml` diff --git a/lib/logic/cubit/forms/validations/validations.dart b/lib/logic/cubit/forms/validations/validations.dart index b7d054d0..800ca77b 100644 --- a/lib/logic/cubit/forms/validations/validations.dart +++ b/lib/logic/cubit/forms/validations/validations.dart @@ -6,10 +6,10 @@ abstract class LengthStringValidation extends ValidationModel { : super(predicate, errorMessage); @override - String? check(String value) { - var length = value.length; + String? check(String val) { + var length = val.length; var errorMessage = errorMassage.replaceAll('[]', length.toString()); - return test(value) ? errorMessage : null; + return test(val) ? errorMessage : null; } } diff --git a/lib/main.dart b/lib/main.dart index 6e3d181b..9593ee31 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -7,7 +7,7 @@ import 'package:selfprivacy/config/hive_config.dart'; import 'package:selfprivacy/theming/factory/app_theme_factory.dart'; import 'package:selfprivacy/ui/pages/setup/initializing.dart'; import 'package:selfprivacy/ui/pages/onboarding/onboarding.dart'; -import 'package:selfprivacy/ui/pages/rootRoute.dart'; +import 'package:selfprivacy/ui/pages/root_route.dart'; import 'package:wakelock/wakelock.dart'; import 'package:timezone/data/latest.dart' as tz; @@ -55,9 +55,10 @@ void main() async { class MyApp extends StatelessWidget { const MyApp({ + Key? key, required this.lightThemeData, required this.darkThemeData, - }); + }) : super(key: key); final ThemeData lightThemeData; final ThemeData darkThemeData; @@ -84,7 +85,7 @@ class MyApp extends StatelessWidget { themeMode: appSettings.isDarkModeOn ? ThemeMode.dark : ThemeMode.light, home: appSettings.isOnboardingShowing - ? OnboardingPage(nextPage: InitializingPage()) + ? const OnboardingPage(nextPage: InitializingPage()) : const RootPage(), builder: (BuildContext context, Widget? widget) { Widget error = const Text('...rendering error...'); diff --git a/lib/ui/components/brand_button/brand_button.dart b/lib/ui/components/brand_button/brand_button.dart index 398e9cc2..186056c0 100644 --- a/lib/ui/components/brand_button/brand_button.dart +++ b/lib/ui/components/brand_button/brand_button.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:selfprivacy/ui/components/brand_button/FilledButton.dart'; +import 'package:selfprivacy/ui/components/brand_button/filled_button.dart'; import 'package:selfprivacy/ui/components/brand_text/brand_text.dart'; enum BrandButtonTypes { rised, text, iconText } diff --git a/lib/ui/components/brand_md/brand_md.dart b/lib/ui/components/brand_md/brand_md.dart index f2895cff..24c7c860 100644 --- a/lib/ui/components/brand_md/brand_md.dart +++ b/lib/ui/components/brand_md/brand_md.dart @@ -15,7 +15,7 @@ class BrandMarkdown extends StatefulWidget { final String fileName; @override - _BrandMarkdownState createState() => _BrandMarkdownState(); + State createState() => _BrandMarkdownState(); } class _BrandMarkdownState extends State { diff --git a/lib/ui/components/brand_tab_bar/brand_tab_bar.dart b/lib/ui/components/brand_tab_bar/brand_tab_bar.dart index 021c75e0..0c32fd5f 100644 --- a/lib/ui/components/brand_tab_bar/brand_tab_bar.dart +++ b/lib/ui/components/brand_tab_bar/brand_tab_bar.dart @@ -7,7 +7,7 @@ class BrandTabBar extends StatefulWidget { final TabController? controller; @override - _BrandTabBarState createState() => _BrandTabBarState(); + State createState() => _BrandTabBarState(); } class _BrandTabBarState extends State { diff --git a/lib/ui/components/brand_timer/brand_timer.dart b/lib/ui/components/brand_timer/brand_timer.dart index 2e93d415..2aa75bce 100644 --- a/lib/ui/components/brand_timer/brand_timer.dart +++ b/lib/ui/components/brand_timer/brand_timer.dart @@ -16,7 +16,7 @@ class BrandTimer extends StatefulWidget { final Duration duration; @override - _BrandTimerState createState() => _BrandTimerState(); + State createState() => _BrandTimerState(); } class _BrandTimerState extends State { diff --git a/lib/ui/components/icon_status_mask/icon_status_mask.dart b/lib/ui/components/icon_status_mask/icon_status_mask.dart index 89426968..c1f3b80c 100644 --- a/lib/ui/components/icon_status_mask/icon_status_mask.dart +++ b/lib/ui/components/icon_status_mask/icon_status_mask.dart @@ -3,7 +3,11 @@ import 'package:selfprivacy/config/brand_colors.dart'; import 'package:selfprivacy/logic/models/state_types.dart'; class IconStatusMask extends StatelessWidget { - const IconStatusMask({required this.child, required this.status}); + const IconStatusMask({ + Key? key, + required this.child, + required this.status, + }) : super(key: key); final Icon child; final StateType status; 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 8bc3ec32..8fad8dd6 100644 --- a/lib/ui/components/not_ready_card/not_ready_card.dart +++ b/lib/ui/components/not_ready_card/not_ready_card.dart @@ -27,7 +27,7 @@ class NotReadyCard extends StatelessWidget { child: GestureDetector( onTap: () => Navigator.of(context).push( materialRoute( - InitializingPage(), + const InitializingPage(), ), ), child: Text( diff --git a/lib/ui/components/pre_styled_buttons/flash_fab.dart b/lib/ui/components/pre_styled_buttons/flash_fab.dart index fac36a37..733541c0 100644 --- a/lib/ui/components/pre_styled_buttons/flash_fab.dart +++ b/lib/ui/components/pre_styled_buttons/flash_fab.dart @@ -11,7 +11,7 @@ class BrandFab extends StatefulWidget { const BrandFab({Key? key}) : super(key: key); @override - _BrandFabState createState() => _BrandFabState(); + State createState() => _BrandFabState(); } class _BrandFabState extends State diff --git a/lib/ui/components/progress_bar/progress_bar.dart b/lib/ui/components/progress_bar/progress_bar.dart index 393fcc70..4dfc10af 100644 --- a/lib/ui/components/progress_bar/progress_bar.dart +++ b/lib/ui/components/progress_bar/progress_bar.dart @@ -17,7 +17,7 @@ class ProgressBar extends StatefulWidget { final List steps; @override - _ProgressBarState createState() => _ProgressBarState(); + State createState() => _ProgressBarState(); } class _ProgressBarState extends State { diff --git a/lib/ui/pages/backup_details/backup_details.dart b/lib/ui/pages/backup_details/backup_details.dart index 78d45a5b..d36238da 100644 --- a/lib/ui/pages/backup_details/backup_details.dart +++ b/lib/ui/pages/backup_details/backup_details.dart @@ -21,7 +21,7 @@ class BackupDetails extends StatefulWidget { const BackupDetails({Key? key}) : super(key: key); @override - _BackupDetailsState createState() => _BackupDetailsState(); + State createState() => _BackupDetailsState(); } class _BackupDetailsState extends State diff --git a/lib/ui/pages/dns_details/dns_details.dart b/lib/ui/pages/dns_details/dns_details.dart index b93de2f9..b891c4d1 100644 --- a/lib/ui/pages/dns_details/dns_details.dart +++ b/lib/ui/pages/dns_details/dns_details.dart @@ -8,8 +8,10 @@ import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.da import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart'; class DnsDetailsPage extends StatefulWidget { + const DnsDetailsPage({Key? key}) : super(key: key); + @override - _DnsDetailsPageState createState() => _DnsDetailsPageState(); + State createState() => _DnsDetailsPageState(); } class _DnsDetailsPageState extends State { diff --git a/lib/ui/pages/more/app_settings/app_setting.dart b/lib/ui/pages/more/app_settings/app_setting.dart index f490bc11..ac4cbc49 100644 --- a/lib/ui/pages/more/app_settings/app_setting.dart +++ b/lib/ui/pages/more/app_settings/app_setting.dart @@ -16,7 +16,7 @@ class AppSettingsPage extends StatefulWidget { const AppSettingsPage({Key? key}) : super(key: key); @override - _AppSettingsPageState createState() => _AppSettingsPageState(); + State createState() => _AppSettingsPageState(); } class _AppSettingsPageState extends State { @@ -50,6 +50,7 @@ class _AppSettingsPageState extends State { child: _TextColumn( title: 'more.settings.1'.tr(), value: 'more.settings.2'.tr(), + hasWarning: false, ), ), const SizedBox(width: 5), @@ -76,6 +77,7 @@ class _AppSettingsPageState extends State { child: _TextColumn( title: 'more.settings.3'.tr(), value: 'more.settings.4'.tr(), + hasWarning: false, ), ), const SizedBox(width: 5), @@ -144,6 +146,7 @@ class _AppSettingsPageState extends State { child: _TextColumn( title: 'more.settings.5'.tr(), value: 'more.settings.6'.tr(), + hasWarning: false, ), ), const SizedBox(width: 5), @@ -177,6 +180,7 @@ class _AppSettingsPageState extends State { await context .read() .serverDelete(); + if (!mounted) return; Navigator.of(context).pop(); }), ActionButton( diff --git a/lib/ui/pages/more/console/console.dart b/lib/ui/pages/more/console/console.dart index 984f20b3..1c77e6bf 100644 --- a/lib/ui/pages/more/console/console.dart +++ b/lib/ui/pages/more/console/console.dart @@ -11,7 +11,7 @@ class Console extends StatefulWidget { const Console({Key? key}) : super(key: key); @override - _ConsoleState createState() => _ConsoleState(); + State createState() => _ConsoleState(); } class _ConsoleState extends State { diff --git a/lib/ui/pages/more/more.dart b/lib/ui/pages/more/more.dart index 790170c5..55b6239a 100644 --- a/lib/ui/pages/more/more.dart +++ b/lib/ui/pages/more/more.dart @@ -10,7 +10,7 @@ import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart'; import 'package:selfprivacy/ui/components/brand_text/brand_text.dart'; import 'package:selfprivacy/ui/pages/setup/initializing.dart'; import 'package:selfprivacy/ui/pages/onboarding/onboarding.dart'; -import 'package:selfprivacy/ui/pages/rootRoute.dart'; +import 'package:selfprivacy/ui/pages/root_route.dart'; import 'package:selfprivacy/ui/pages/ssh_keys/ssh_keys.dart'; import 'package:selfprivacy/utils/route_transitions/basic.dart'; @@ -43,7 +43,7 @@ class MorePage extends StatelessWidget { _NavItem( title: 'more.configuration_wizard'.tr(), iconData: BrandIcons.triangle, - goTo: InitializingPage(), + goTo: const InitializingPage(), ), _NavItem( title: 'more.settings.title'.tr(), diff --git a/lib/ui/pages/onboarding/onboarding.dart b/lib/ui/pages/onboarding/onboarding.dart index eecd55c8..4530c746 100644 --- a/lib/ui/pages/onboarding/onboarding.dart +++ b/lib/ui/pages/onboarding/onboarding.dart @@ -10,7 +10,7 @@ class OnboardingPage extends StatefulWidget { final Widget nextPage; @override - _OnboardingPageState createState() => _OnboardingPageState(); + State createState() => _OnboardingPageState(); } class _OnboardingPageState extends State { diff --git a/lib/ui/pages/providers/providers.dart b/lib/ui/pages/providers/providers.dart index 754e7866..b3566f2f 100644 --- a/lib/ui/pages/providers/providers.dart +++ b/lib/ui/pages/providers/providers.dart @@ -24,7 +24,7 @@ class ProvidersPage extends StatefulWidget { const ProvidersPage({Key? key}) : super(key: key); @override - _ProvidersPageState createState() => _ProvidersPageState(); + State createState() => _ProvidersPageState(); } class _ProvidersPageState extends State { @@ -124,7 +124,7 @@ class _Card extends StatelessWidget { stableText = 'providers.domain.status'.tr(); onTap = () => Navigator.of(context).push(materialRoute( - DnsDetailsPage(), + const DnsDetailsPage(), )); break; case ProviderType.backup: diff --git a/lib/ui/pages/root_route.dart b/lib/ui/pages/root_route.dart index d66d73c3..e3ad4ce4 100644 --- a/lib/ui/pages/root_route.dart +++ b/lib/ui/pages/root_route.dart @@ -13,7 +13,7 @@ class RootPage extends StatefulWidget { const RootPage({Key? key}) : super(key: key); @override - _RootPageState createState() => _RootPageState(); + State createState() => _RootPageState(); } class _RootPageState extends State diff --git a/lib/ui/pages/server_details/chart.dart b/lib/ui/pages/server_details/chart.dart index 7829234b..bc94bac4 100644 --- a/lib/ui/pages/server_details/chart.dart +++ b/lib/ui/pages/server_details/chart.dart @@ -90,7 +90,11 @@ class _Chart extends StatelessWidget { return SizedBox( height: 200, - child: CpuChart(data, state.period, state.start), + child: CpuChart( + data: data, + period: state.period, + start: state.start, + ), ); } @@ -101,9 +105,9 @@ class _Chart extends StatelessWidget { return SizedBox( height: 200, child: NetworkChart( - [ppsIn, ppsOut], - state.period, - state.start, + listData: [ppsIn, ppsOut], + period: state.period, + start: state.start, ), ); } @@ -115,9 +119,9 @@ class _Chart extends StatelessWidget { return SizedBox( height: 200, child: NetworkChart( - [ppsIn, ppsOut], - state.period, - state.start, + listData: [ppsIn, ppsOut], + period: state.period, + start: state.start, ), ); } diff --git a/lib/ui/pages/server_details/cpu_chart.dart b/lib/ui/pages/server_details/cpu_chart.dart index 113c797b..07bd8539 100644 --- a/lib/ui/pages/server_details/cpu_chart.dart +++ b/lib/ui/pages/server_details/cpu_chart.dart @@ -7,7 +7,12 @@ import 'package:selfprivacy/logic/models/hetzner_metrics.dart'; import 'package:intl/intl.dart'; class CpuChart extends StatelessWidget { - const CpuChart(this.data, this.period, this.start); + const CpuChart({ + Key? key, + required this.data, + required this.period, + required this.start, + }) : super(key: key); final List data; final Period period; @@ -103,8 +108,8 @@ class CpuChart extends StatelessWidget { } else if (value == 0) { return true; } - var _value = value - minValue; - var v = _value / 20; + var localValue = value - minValue; + var v = localValue / 20; return v - v.floor() == 0; } diff --git a/lib/ui/pages/server_details/network_charts.dart b/lib/ui/pages/server_details/network_charts.dart index 4e0b385e..31b3dd21 100644 --- a/lib/ui/pages/server_details/network_charts.dart +++ b/lib/ui/pages/server_details/network_charts.dart @@ -9,11 +9,12 @@ import 'package:selfprivacy/logic/models/hetzner_metrics.dart'; import 'package:intl/intl.dart'; class NetworkChart extends StatelessWidget { - const NetworkChart( - this.listData, - this.period, - this.start, - ); + const NetworkChart({ + Key? key, + required this.listData, + required this.period, + required this.start, + }) : super(key: key); final List> listData; final Period period; @@ -132,9 +133,9 @@ class NetworkChart extends StatelessWidget { } else if (value == 0) { return true; } - var _value = value - minValue; - var v = _value / 20; - return v - v.floor() == 0; + var diff = value - minValue; + var finalValue = diff / 20; + return finalValue - finalValue.floor() == 0; } String bottomTitle(int value) { diff --git a/lib/ui/pages/server_details/server_details_screen.dart b/lib/ui/pages/server_details/server_details_screen.dart index 8fe4b04e..5eb3be3e 100644 --- a/lib/ui/pages/server_details/server_details_screen.dart +++ b/lib/ui/pages/server_details/server_details_screen.dart @@ -37,7 +37,7 @@ class ServerDetailsScreen extends StatefulWidget { const ServerDetailsScreen({Key? key}) : super(key: key); @override - _ServerDetailsScreenState createState() => _ServerDetailsScreenState(); + State createState() => _ServerDetailsScreenState(); } class _ServerDetailsScreenState extends State diff --git a/lib/ui/pages/server_details/server_settings.dart b/lib/ui/pages/server_details/server_settings.dart index f5047d6c..93393632 100644 --- a/lib/ui/pages/server_details/server_settings.dart +++ b/lib/ui/pages/server_details/server_settings.dart @@ -42,6 +42,7 @@ class _ServerSettings extends StatelessWidget { child: const _TextColumn( title: 'Allow Auto-upgrade', value: 'Wether to allow automatic packages upgrades', + hasWarning: false, ), ), SwitcherBlock( @@ -50,15 +51,17 @@ class _ServerSettings extends StatelessWidget { child: const _TextColumn( title: 'Reboot after upgrade', value: 'Reboot without prompt after applying updates', + hasWarning: false, ), ), _Button( onTap: () { - Navigator.of(context).push(materialRoute(SelectTimezone())); + Navigator.of(context).push(materialRoute(const SelectTimezone())); }, child: _TextColumn( title: 'Server Timezone', value: serverDetailsState.serverTimezone.timezone.name, + hasWarning: false, ), ), ], 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 0ae610cf..402aeb26 100644 --- a/lib/ui/pages/server_details/time_zone/time_zone.dart +++ b/lib/ui/pages/server_details/time_zone/time_zone.dart @@ -8,7 +8,7 @@ class SelectTimezone extends StatefulWidget { const SelectTimezone({Key? key}) : super(key: key); @override - _SelectTimezoneState createState() => _SelectTimezoneState(); + State createState() => _SelectTimezoneState(); } class _SelectTimezoneState extends State { diff --git a/lib/ui/pages/services/services.dart b/lib/ui/pages/services/services.dart index 77134124..e2f3f4ed 100644 --- a/lib/ui/pages/services/services.dart +++ b/lib/ui/pages/services/services.dart @@ -35,13 +35,13 @@ class ServicesPage extends StatefulWidget { const ServicesPage({Key? key}) : super(key: key); @override - _ServicesPageState createState() => _ServicesPageState(); + State createState() => _ServicesPageState(); } void _launchURL(url) async { - var _possible = await canLaunchUrlString(url); + var canLaunch = await canLaunchUrlString(url); - if (_possible) { + if (canLaunch) { try { await launchUrlString( url, diff --git a/lib/ui/pages/setup/initializing.dart b/lib/ui/pages/setup/initializing.dart index 5121ac36..98853c8f 100644 --- a/lib/ui/pages/setup/initializing.dart +++ b/lib/ui/pages/setup/initializing.dart @@ -21,6 +21,8 @@ import 'package:selfprivacy/ui/pages/setup/recovering/recovery_routing.dart'; import 'package:selfprivacy/utils/route_transitions/basic.dart'; class InitializingPage extends StatelessWidget { + const InitializingPage({Key? key}) : super(key: key); + @override Widget build(BuildContext context) { var cubit = context.watch(); 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 ea267a39..b16ff964 100644 --- a/lib/ui/pages/setup/recovering/recover_by_old_token.dart +++ b/lib/ui/pages/setup/recovering/recover_by_old_token.dart @@ -10,7 +10,9 @@ import 'package:selfprivacy/logic/cubit/forms/factories/field_cubit_factory.dart class RecoverByOldTokenInstruction extends StatelessWidget { @override - const RecoverByOldTokenInstruction({required this.instructionFilename}); + const RecoverByOldTokenInstruction( + {Key? key, required this.instructionFilename}) + : super(key: key); @override Widget build(BuildContext context) { diff --git a/lib/ui/pages/setup/recovering/recovery_confirm_server.dart b/lib/ui/pages/setup/recovering/recovery_confirm_server.dart index 0f03090e..96bcb51d 100644 --- a/lib/ui/pages/setup/recovering/recovery_confirm_server.dart +++ b/lib/ui/pages/setup/recovering/recovery_confirm_server.dart @@ -11,7 +11,7 @@ class RecoveryConfirmServer extends StatefulWidget { const RecoveryConfirmServer({Key? key}) : super(key: key); @override - _RecoveryConfirmServerState createState() => _RecoveryConfirmServerState(); + State createState() => _RecoveryConfirmServerState(); } class _RecoveryConfirmServerState extends State { @@ -57,11 +57,11 @@ class _RecoveryConfirmServerState extends State { children: [ if (servers.length == 1 || (!_isExtended && _isServerFound(servers))) - _ConfirmServer(context, _firstValidServer(servers), + confirmServer(context, _firstValidServer(servers), servers.length > 1), if (servers.length > 1 && (_isExtended || !_isServerFound(servers))) - _ChooseServer(context, servers), + chooseServer(context, servers), ], ), if (servers?.isEmpty ?? true) @@ -84,14 +84,14 @@ class _RecoveryConfirmServerState extends State { ); } - Widget _ConfirmServer( + Widget confirmServer( BuildContext context, ServerBasicInfoWithValidators server, bool showMoreServersButton, ) { return Column( children: [ - _ServerCard( + serverCard( context: context, server: server, ), @@ -110,14 +110,14 @@ class _RecoveryConfirmServerState extends State { ); } - Widget _ChooseServer( + Widget chooseServer( BuildContext context, List servers) { return Column( children: [ for (final server in servers) Padding( padding: const EdgeInsets.symmetric(vertical: 8.0), - child: _ServerCard( + child: serverCard( context: context, server: server, onTap: () => _showConfirmationDialog(context, server), @@ -127,7 +127,7 @@ class _RecoveryConfirmServerState extends State { ); } - Widget _ServerCard( + Widget serverCard( {required BuildContext context, required ServerBasicInfoWithValidators server, VoidCallback? onTap}) { diff --git a/lib/ui/pages/setup/recovering/recovery_method_select.dart b/lib/ui/pages/setup/recovering/recovery_method_select.dart index ed768c5a..554f40eb 100644 --- a/lib/ui/pages/setup/recovering/recovery_method_select.dart +++ b/lib/ui/pages/setup/recovering/recovery_method_select.dart @@ -47,7 +47,7 @@ class RecoveryMethodSelect extends StatelessWidget { BrandButton.text( title: 'recovering.method_select_nothing'.tr(), onPressed: () => Navigator.of(context) - .push(materialRoute(RecoveryFallbackMethodSelect())), + .push(materialRoute(const RecoveryFallbackMethodSelect())), ) ], ); diff --git a/lib/ui/pages/setup/recovering/recovery_routing.dart b/lib/ui/pages/setup/recovering/recovery_routing.dart index 7ce718fd..65ff5688 100644 --- a/lib/ui/pages/setup/recovering/recovery_routing.dart +++ b/lib/ui/pages/setup/recovering/recovery_routing.dart @@ -36,10 +36,10 @@ class RecoveryRouting extends StatelessWidget { currentPage = const RecoverByRecoveryKey(); break; case RecoveryStep.newDeviceKey: - currentPage = RecoverByNewDeviceKeyInstruction(); + currentPage = const RecoverByNewDeviceKeyInstruction(); break; case RecoveryStep.oldToken: - currentPage = RecoverByOldToken(); + currentPage = const RecoverByOldToken(); break; case RecoveryStep.hetznerToken: currentPage = const RecoveryHetznerConnected(); diff --git a/lib/ui/pages/ssh_keys/ssh_keys.dart b/lib/ui/pages/ssh_keys/ssh_keys.dart index c527a972..6fc5087b 100644 --- a/lib/ui/pages/ssh_keys/ssh_keys.dart +++ b/lib/ui/pages/ssh_keys/ssh_keys.dart @@ -24,7 +24,7 @@ class SshKeysPage extends StatefulWidget { const SshKeysPage({Key? key, required this.user}) : super(key: key); @override - _SshKeysPageState createState() => _SshKeysPageState(); + State createState() => _SshKeysPageState(); } class _SshKeysPageState extends State { From 72ef16c6f64bee32550fcd3c583e80b3b0f6fe6f Mon Sep 17 00:00:00 2001 From: NaiJi Date: Thu, 26 May 2022 04:02:06 +0300 Subject: [PATCH 34/52] Implement recovery key pages and device cubit Co-authored-by: Inex Code --- assets/translations/en.json | 13 +- lib/logic/common_enum/common_enum.dart | 7 + lib/logic/cubit/devices/devices_cubit.dart | 75 ++++++ lib/logic/cubit/devices/devices_state.dart | 33 +++ .../recovery_key/recovery_key_cubit.dart | 7 +- .../recovery_key/recovery_key_state.dart | 9 +- .../server_installation_cubit.dart | 5 +- .../server_installation_repository.dart | 24 ++ .../brand_button/filled_button.dart | 17 +- lib/ui/pages/recovery_key/recovery_key.dart | 234 +++++++++++++++++- .../recovery_key/recovery_key_receiving.dart | 36 +++ .../recovering/recover_by_old_token.dart | 1 - .../recovering/recovery_method_select.dart | 103 ++++---- .../setup/recovering/recovery_routing.dart | 8 +- 14 files changed, 502 insertions(+), 70 deletions(-) create mode 100644 lib/logic/cubit/devices/devices_cubit.dart create mode 100644 lib/logic/cubit/devices/devices_state.dart diff --git a/assets/translations/en.json b/assets/translations/en.json index 7cbfdd4f..73234e0c 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -325,13 +325,24 @@ "confirm_backblaze_description": "Enter a Backblaze token with access to backup storage:" }, "recovery_key": { + "key_connection_error": "Couldn't connect to the server.", + "key_synchronizing": "Synchronizing...", "key_main_header": "Recovery key", "key_main_description": "Is needed for SelfPrivacy authorization when all your other authorized devices aren't available.", "key_amount_toggle": "Limit by number of uses", "key_amount_field_title": "Max number of uses", "key_duedate_toggle": "Limit by time", "key_duedate_field_title": "Due date of expiration", - "key_receive_button": "Receive key" + "key_receive_button": "Receive key", + "key_valid": "Your key is valid", + "key_invalid": "Your key is no longer valid", + "key_valid_until": "Valid until {}", + "key_valid_for": "Valid for {} uses", + "key_creation_date": "Created on {}", + "key_replace_button": "Generate new key", + "key_receiving_description": "Write down this key and put to a safe place. It is used to restore full access to your server:", + "key_receiving_info": "The key will never ever be shown again, but you will be able to replace it with another one.", + "key_receiving_done": "Done!" }, "modals": { "_comment": "messages in modals", diff --git a/lib/logic/common_enum/common_enum.dart b/lib/logic/common_enum/common_enum.dart index edaaf567..94acface 100644 --- a/lib/logic/common_enum/common_enum.dart +++ b/lib/logic/common_enum/common_enum.dart @@ -3,6 +3,13 @@ import 'package:flutter/cupertino.dart'; import 'package:ionicons/ionicons.dart'; import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart'; +enum LoadingStatus { + uninitialized, + refreshing, + success, + error, +} + enum InitializingSteps { setHetznerKey, setCloudFlareKey, diff --git a/lib/logic/cubit/devices/devices_cubit.dart b/lib/logic/cubit/devices/devices_cubit.dart new file mode 100644 index 00000000..4ec51d84 --- /dev/null +++ b/lib/logic/cubit/devices/devices_cubit.dart @@ -0,0 +1,75 @@ +import 'package:selfprivacy/config/get_it_config.dart'; +import 'package:selfprivacy/logic/api_maps/server.dart'; +import 'package:selfprivacy/logic/common_enum/common_enum.dart'; +import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart'; +import 'package:selfprivacy/logic/models/json/api_token.dart'; + +part 'devices_state.dart'; + +class ApiDevicesCubit + extends ServerInstallationDependendCubit { + ApiDevicesCubit(ServerInstallationCubit serverInstallationCubit) + : super(serverInstallationCubit, const ApiDevicesState.initial()); + + final api = ServerApi(); + + @override + void load() async { + if (serverInstallationCubit.state is ServerInstallationFinished) { + emit(const ApiDevicesState([], LoadingStatus.refreshing)); + final devices = await _getApiTokens(); + if (devices != null) { + emit(ApiDevicesState(devices, LoadingStatus.success)); + } else { + emit(const ApiDevicesState([], LoadingStatus.error)); + } + } + } + + Future refresh() async { + emit(const ApiDevicesState([], LoadingStatus.refreshing)); + final devices = await _getApiTokens(); + if (devices != null) { + emit(ApiDevicesState(devices, LoadingStatus.success)); + } else { + emit(const ApiDevicesState([], LoadingStatus.error)); + } + } + + Future?> _getApiTokens() async { + final response = await api.getApiTokens(); + if (response.isSuccess) { + return response.data; + } else { + return null; + } + } + + Future deleteDevice(ApiToken device) async { + final response = await api.deleteApiToken(device.name); + if (response.isSuccess) { + emit(ApiDevicesState( + state.devices.where((d) => d.name != device.name).toList(), + LoadingStatus.success)); + } else { + getIt() + .showSnackBar(response.errorMessage ?? 'Error deleting device'); + } + } + + Future getNewDeviceKey() async { + final response = await api.createDeviceToken(); + if (response.isSuccess) { + return response.data; + } else { + getIt().showSnackBar( + response.errorMessage ?? 'Error getting new device key'); + return null; + } + } + + @override + void clear() { + emit(const ApiDevicesState.initial()); + } +} diff --git a/lib/logic/cubit/devices/devices_state.dart b/lib/logic/cubit/devices/devices_state.dart new file mode 100644 index 00000000..bccc5e29 --- /dev/null +++ b/lib/logic/cubit/devices/devices_state.dart @@ -0,0 +1,33 @@ +part of 'devices_cubit.dart'; + +class ApiDevicesState extends ServerInstallationDependendState { + const ApiDevicesState(this._devices, this.status); + + const ApiDevicesState.initial() : this(const [], LoadingStatus.uninitialized); + final List _devices; + final LoadingStatus status; + + List get devices => _devices; + ApiToken get thisDevice => _devices.firstWhere((device) => device.isCaller, + orElse: () => ApiToken( + name: 'Error fetching device', + isCaller: true, + date: DateTime.now(), + )); + + List get otherDevices => + _devices.where((device) => !device.isCaller).toList(); + + ApiDevicesState copyWith({ + List? devices, + LoadingStatus? status, + }) { + return ApiDevicesState( + devices ?? _devices, + status ?? this.status, + ); + } + + @override + List get props => [_devices]; +} diff --git a/lib/logic/cubit/recovery_key/recovery_key_cubit.dart b/lib/logic/cubit/recovery_key/recovery_key_cubit.dart index 6092a03d..6b120d0e 100644 --- a/lib/logic/cubit/recovery_key/recovery_key_cubit.dart +++ b/lib/logic/cubit/recovery_key/recovery_key_cubit.dart @@ -1,4 +1,5 @@ import 'package:selfprivacy/logic/api_maps/server.dart'; +import 'package:selfprivacy/logic/common_enum/common_enum.dart'; import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart'; import 'package:selfprivacy/logic/models/json/recovery_token_status.dart'; @@ -18,7 +19,8 @@ class RecoveryKeyCubit if (status == null) { emit(state.copyWith(loadingStatus: LoadingStatus.error)); } else { - emit(state.copyWith(status: status, loadingStatus: LoadingStatus.good)); + emit(state.copyWith( + status: status, loadingStatus: LoadingStatus.success)); } } else { emit(state.copyWith(loadingStatus: LoadingStatus.uninitialized)); @@ -41,7 +43,8 @@ class RecoveryKeyCubit if (status == null) { emit(state.copyWith(loadingStatus: LoadingStatus.error)); } else { - emit(state.copyWith(status: status, loadingStatus: LoadingStatus.good)); + emit( + state.copyWith(status: status, loadingStatus: LoadingStatus.success)); } } diff --git a/lib/logic/cubit/recovery_key/recovery_key_state.dart b/lib/logic/cubit/recovery_key/recovery_key_state.dart index c88a9138..f5eb1090 100644 --- a/lib/logic/cubit/recovery_key/recovery_key_state.dart +++ b/lib/logic/cubit/recovery_key/recovery_key_state.dart @@ -1,12 +1,5 @@ part of 'recovery_key_cubit.dart'; -enum LoadingStatus { - uninitialized, - refreshing, - good, - error, -} - class RecoveryKeyState extends ServerInstallationDependendState { const RecoveryKeyState(this._status, this.loadingStatus); @@ -20,7 +13,7 @@ class RecoveryKeyState extends ServerInstallationDependendState { bool get exists => _status.exists; bool get isValid => _status.valid; DateTime? get generatedAt => _status.date; - DateTime? get expiresAt => _status.date; + DateTime? get expiresAt => _status.expiration; int? get usesLeft => _status.usesLeft; @override List get props => [_status, loadingStatus]; diff --git a/lib/logic/cubit/server_installation/server_installation_cubit.dart b/lib/logic/cubit/server_installation/server_installation_cubit.dart index 311f41e1..9aedf6fb 100644 --- a/lib/logic/cubit/server_installation/server_installation_cubit.dart +++ b/lib/logic/cubit/server_installation/server_installation_cubit.dart @@ -307,8 +307,8 @@ class ServerInstallationCubit extends Cubit { return; } try { - Future Function(ServerDomain, String) - recoveryFunction; + Future Function( + ServerDomain, String, ServerRecoveryCapabilities) recoveryFunction; switch (method) { case ServerRecoveryMethods.newDeviceKey: recoveryFunction = repository.authorizeByNewDeviceKey; @@ -325,6 +325,7 @@ class ServerInstallationCubit extends Cubit { final serverDetails = await recoveryFunction( serverDomain, token, + dataState.recoveryCapabilities, ); await repository.saveServerDetails(serverDetails); emit(dataState.copyWith( diff --git a/lib/logic/cubit/server_installation/server_installation_repository.dart b/lib/logic/cubit/server_installation/server_installation_repository.dart index e4e07804..5a1bfb58 100644 --- a/lib/logic/cubit/server_installation/server_installation_repository.dart +++ b/lib/logic/cubit/server_installation/server_installation_repository.dart @@ -392,6 +392,7 @@ class ServerInstallationRepository { Future authorizeByNewDeviceKey( ServerDomain serverDomain, String newDeviceKey, + ServerRecoveryCapabilities recoveryCapabilities, ) async { var serverApi = ServerApi( isWithToken: false, @@ -424,6 +425,7 @@ class ServerInstallationRepository { Future authorizeByRecoveryKey( ServerDomain serverDomain, String recoveryKey, + ServerRecoveryCapabilities recoveryCapabilities, ) async { var serverApi = ServerApi( isWithToken: false, @@ -455,12 +457,34 @@ class ServerInstallationRepository { Future authorizeByApiToken( ServerDomain serverDomain, String apiToken, + ServerRecoveryCapabilities recoveryCapabilities, ) async { var serverApi = ServerApi( isWithToken: false, overrideDomain: serverDomain.domainName, customToken: apiToken, ); + if (recoveryCapabilities == ServerRecoveryCapabilities.legacy) { + final apiResponse = await serverApi.servicesPowerCheck(); + if (apiResponse.isNotEmpty) { + return ServerHostingDetails( + apiToken: apiToken, + volume: ServerVolume( + id: 0, + name: '', + ), + provider: ServerProvider.unknown, + id: 0, + ip4: '', + startTime: null, + createTime: null, + ); + } else { + throw ServerAuthorizationException( + 'Couldn\'t connect to server with this token', + ); + } + } final deviceAuthKey = await serverApi.createDeviceToken(); final apiResponse = await serverApi.authorizeDevice( DeviceToken(device: await getDeviceName(), token: deviceAuthKey.data)); diff --git a/lib/ui/components/brand_button/filled_button.dart b/lib/ui/components/brand_button/filled_button.dart index 06822d4f..a3230ddf 100644 --- a/lib/ui/components/brand_button/filled_button.dart +++ b/lib/ui/components/brand_button/filled_button.dart @@ -6,20 +6,29 @@ class FilledButton extends StatelessWidget { this.onPressed, this.title, this.child, + this.disabled = false, }) : super(key: key); final VoidCallback? onPressed; final String? title; final Widget? child; + final bool disabled; @override Widget build(BuildContext context) { + final ButtonStyle _enabledStyle = ElevatedButton.styleFrom( + onPrimary: Theme.of(context).colorScheme.onPrimary, + primary: Theme.of(context).colorScheme.primary, + ).copyWith(elevation: ButtonStyleButton.allOrNull(0.0)); + + final ButtonStyle _disabledStyle = ElevatedButton.styleFrom( + onPrimary: Theme.of(context).colorScheme.onSurface.withAlpha(30), + primary: Theme.of(context).colorScheme.onSurface.withAlpha(98), + ).copyWith(elevation: ButtonStyleButton.allOrNull(0.0)); + return ElevatedButton( onPressed: onPressed, - style: ElevatedButton.styleFrom( - onPrimary: Theme.of(context).colorScheme.onPrimary, - primary: Theme.of(context).colorScheme.primary, - ).copyWith(elevation: ButtonStyleButton.allOrNull(0.0)), + style: disabled ? _disabledStyle : _enabledStyle, child: child ?? Text(title ?? ''), ); } diff --git a/lib/ui/pages/recovery_key/recovery_key.dart b/lib/ui/pages/recovery_key/recovery_key.dart index bf0fdd6d..30a7cc76 100644 --- a/lib/ui/pages/recovery_key/recovery_key.dart +++ b/lib/ui/pages/recovery_key/recovery_key.dart @@ -1,5 +1,17 @@ -/*import 'package:flutter/src/foundation/key.dart'; -import 'package:flutter/src/widgets/framework.dart'; +import 'package:cubit_form/cubit_form.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.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_button/filled_button.dart'; +import 'package:selfprivacy/ui/components/brand_cards/brand_cards.dart'; +import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; +import 'package:selfprivacy/ui/components/brand_text/brand_text.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({Key? key}) : super(key: key); @@ -10,5 +22,219 @@ class RecoveryKey extends StatefulWidget { class _RecoveryKeyState extends State { @override - Widget build(BuildContext context) {} -}*/ + Widget build(BuildContext context) { + var keyStatus = context.watch().state; + + final List widgets; + final String? subtitle = + keyStatus.exists ? null : 'recovery_key.key_main_description'.tr(); + switch (keyStatus.loadingStatus) { + case LoadingStatus.refreshing: + widgets = [ + const Icon(Icons.refresh_outlined), + const SizedBox(height: 18), + BrandText( + 'recovery_key.key_synchronizing'.tr(), + type: TextType.h1, + ), + ]; + break; + case LoadingStatus.success: + widgets = [ + const RecoveryKeyContent(), + ]; + break; + case LoadingStatus.uninitialized: + case LoadingStatus.error: + widgets = [ + const Icon(Icons.sentiment_dissatisfied_outlined), + const SizedBox(height: 18), + BrandText( + 'recovery_key.key_connection_error'.tr(), + type: TextType.h1, + ), + ]; + break; + } + + return BrandHeroScreen( + heroTitle: 'recovery_key.key_main_header'.tr(), + heroSubtitle: subtitle, + hasBackButton: true, + hasFlashButton: false, + children: widgets, + ); + } +} + +class RecoveryKeyContent extends StatefulWidget { + const RecoveryKeyContent({Key? key}) : super(key: key); + + @override + State createState() => _RecoveryKeyContentState(); +} + +class _RecoveryKeyContentState extends State { + bool _isAmountToggled = true; + bool _isExpirationToggled = true; + bool _isConfigurationVisible = false; + + final _amountController = TextEditingController(); + final _expirationController = TextEditingController(); + + @override + Widget build(BuildContext context) { + var keyStatus = context.read().state; + _isConfigurationVisible = !keyStatus.exists; + + List widgets = []; + + if (keyStatus.exists) { + if (keyStatus.isValid) { + widgets = [ + BrandCards.filled( + child: ListTile( + title: Text('recovery_key.key_valid'.tr()), + leading: const Icon(Icons.check_circle_outlined), + tileColor: Colors.lightGreen, + ), + ), + ...widgets + ]; + } else { + widgets = [ + BrandCards.filled( + child: ListTile( + title: Text('recovery_key.key_invalid'.tr()), + leading: const Icon(Icons.cancel_outlined), + tileColor: Colors.redAccent, + ), + ), + ...widgets + ]; + } + + if (keyStatus.expiresAt != null && !_isConfigurationVisible) { + widgets = [ + ...widgets, + const SizedBox(height: 18), + Text( + 'recovery_key.key_valid_until'.tr( + args: [keyStatus.expiresAt!.toIso8601String()], + ), + ) + ]; + } + + if (keyStatus.usesLeft != null && !_isConfigurationVisible) { + widgets = [ + ...widgets, + const SizedBox(height: 18), + Text( + 'recovery_key.key_valid_for'.tr( + args: [keyStatus.usesLeft!.toString()], + ), + ) + ]; + } + + if (keyStatus.generatedAt != null && !_isConfigurationVisible) { + widgets = [ + ...widgets, + const SizedBox(height: 18), + Text( + 'recovery_key.key_creation_date'.tr( + args: [keyStatus.generatedAt!.toIso8601String()], + ), + ) + ]; + } + + if (!_isConfigurationVisible) { + if (keyStatus.isValid) { + widgets = [ + ...widgets, + const SizedBox(height: 18), + BrandButton.text( + title: 'recovery_key.key_replace_button'.tr(), + onPressed: () => _isConfigurationVisible = true, + ), + ]; + } else { + widgets = [ + ...widgets, + const SizedBox(height: 18), + FilledButton( + title: 'recovery_key.key_replace_button'.tr(), + onPressed: () => _isConfigurationVisible = true, + ), + ]; + } + } + } + + if (_isConfigurationVisible) { + widgets = [ + ...widgets, + const SizedBox(height: 18), + Row( + children: [ + Text('key_amount_toggle'.tr()), + Switch( + value: _isAmountToggled, + onChanged: (bool toogled) => _isAmountToggled = toogled, + ), + ], + ), + const SizedBox(height: 18), + TextField( + enabled: _isAmountToggled, + controller: _amountController, + decoration: InputDecoration( + labelText: 'recovery_key.key_amount_field_title'.tr()), + keyboardType: TextInputType.number, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + ], // Only numbers can be entered + ), + const SizedBox(height: 18), + Row( + children: [ + Text('key_duedate_toggle'.tr()), + Switch( + value: _isExpirationToggled, + onChanged: (bool toogled) => _isExpirationToggled = toogled, + ), + ], + ), + const SizedBox(height: 18), + TextField( + enabled: _isExpirationToggled, + controller: _expirationController, + decoration: InputDecoration( + labelText: 'recovery_key.key_duedate_field_title'.tr()), + keyboardType: TextInputType.number, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + ], // Only numbers can be entered + ), + const SizedBox(height: 18), + FilledButton( + title: 'recovery_key.key_receive_button'.tr(), + disabled: + (_isExpirationToggled && _expirationController.text.isEmpty) || + (_isAmountToggled && _amountController.text.isEmpty), + onPressed: () { + Navigator.of(context).push( + materialRoute( + const RecoveryKeyReceiving(recoveryKey: ''), // TO DO + ), + ); + }, + ), + ]; + } + + return Column(children: widgets); + } +} diff --git a/lib/ui/pages/recovery_key/recovery_key_receiving.dart b/lib/ui/pages/recovery_key/recovery_key_receiving.dart index 8b137891..8605e871 100644 --- a/lib/ui/pages/recovery_key/recovery_key_receiving.dart +++ b/lib/ui/pages/recovery_key/recovery_key_receiving.dart @@ -1 +1,37 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:selfprivacy/ui/components/brand_button/filled_button.dart'; +import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; +import 'package:selfprivacy/ui/pages/root_route.dart'; +import 'package:selfprivacy/utils/route_transitions/basic.dart'; +class RecoveryKeyReceiving extends StatelessWidget { + const RecoveryKeyReceiving({required this.recoveryKey, Key? key}) + : super(key: key); + + final String recoveryKey; + + @override + Widget build(BuildContext context) { + return BrandHeroScreen( + heroTitle: 'recovery_key.key_main_header'.tr(), + heroSubtitle: 'recovering.method_select_description'.tr(), + hasBackButton: true, + hasFlashButton: false, + children: [ + Text(recoveryKey, style: Theme.of(context).textTheme.bodyLarge), + const SizedBox(height: 18), + const Icon(Icons.info_outlined, size: 14), + Text('recovery_key.key_receiving_info'.tr()), + const SizedBox(height: 18), + FilledButton( + title: 'recovery_key.key_receiving_done'.tr(), + onPressed: () { + Navigator.of(context) + .pushReplacement(materialRoute(const RootPage())); + }, + ), + ], + ); + } +} 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 b16ff964..d62221e9 100644 --- a/lib/ui/pages/setup/recovering/recover_by_old_token.dart +++ b/lib/ui/pages/setup/recovering/recover_by_old_token.dart @@ -21,7 +21,6 @@ class RecoverByOldTokenInstruction extends StatelessWidget { if (state is ServerInstallationRecovery && state.currentStep != RecoveryStep.selecting) { Navigator.of(context).pop(); - Navigator.of(context).pop(); } }, child: BrandHeroScreen( diff --git a/lib/ui/pages/setup/recovering/recovery_method_select.dart b/lib/ui/pages/setup/recovering/recovery_method_select.dart index 554f40eb..a8e4860c 100644 --- a/lib/ui/pages/setup/recovering/recovery_method_select.dart +++ b/lib/ui/pages/setup/recovering/recovery_method_select.dart @@ -1,5 +1,6 @@ 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/brand_cards.dart'; @@ -59,58 +60,68 @@ class RecoveryFallbackMethodSelect extends StatelessWidget { @override Widget build(BuildContext context) { - return BrandHeroScreen( - heroTitle: 'recovering.recovery_main_header'.tr(), - heroSubtitle: 'recovering.fallback_select_description'.tr(), - hasBackButton: true, - hasFlashButton: false, - children: [ - BrandCards.outlined( - child: ListTile( - title: Text( - 'recovering.fallback_select_token_copy'.tr(), - style: Theme.of(context).textTheme.titleMedium, + return BlocListener( + listener: (context, state) { + if (state is ServerInstallationRecovery && + state.recoveryCapabilities == + ServerRecoveryCapabilities.loginTokens && + state.currentStep != RecoveryStep.selecting) { + Navigator.of(context).pop(); + } + }, + child: BrandHeroScreen( + heroTitle: 'recovering.recovery_main_header'.tr(), + heroSubtitle: 'recovering.fallback_select_description'.tr(), + hasBackButton: true, + hasFlashButton: false, + children: [ + BrandCards.outlined( + child: ListTile( + title: Text( + 'recovering.fallback_select_token_copy'.tr(), + style: Theme.of(context).textTheme.titleMedium, + ), + leading: const Icon(Icons.vpn_key), + onTap: () => Navigator.of(context) + .push(materialRoute(const RecoverByOldTokenInstruction( + instructionFilename: 'how_fallback_old', + ))), ), - leading: const Icon(Icons.vpn_key), - onTap: () => Navigator.of(context) - .push(materialRoute(const RecoverByOldTokenInstruction( - instructionFilename: 'how_fallback_old', - ))), ), - ), - const SizedBox(height: 16), - BrandCards.outlined( - child: ListTile( - title: Text( - 'recovering.fallback_select_root_ssh'.tr(), - style: Theme.of(context).textTheme.titleMedium, + const SizedBox(height: 16), + BrandCards.outlined( + child: ListTile( + title: Text( + 'recovering.fallback_select_root_ssh'.tr(), + style: Theme.of(context).textTheme.titleMedium, + ), + leading: const Icon(Icons.terminal), + onTap: () => Navigator.of(context) + .push(materialRoute(const RecoverByOldTokenInstruction( + instructionFilename: 'how_fallback_ssh', + ))), ), - leading: const Icon(Icons.terminal), - onTap: () => Navigator.of(context) - .push(materialRoute(const RecoverByOldTokenInstruction( - instructionFilename: 'how_fallback_ssh', - ))), ), - ), - const SizedBox(height: 16), - BrandCards.outlined( - child: ListTile( - title: Text( - 'recovering.fallback_select_provider_console'.tr(), - style: Theme.of(context).textTheme.titleMedium, + const SizedBox(height: 16), + BrandCards.outlined( + child: ListTile( + title: Text( + 'recovering.fallback_select_provider_console'.tr(), + style: Theme.of(context).textTheme.titleMedium, + ), + subtitle: Text( + 'recovering.fallback_select_provider_console_hint'.tr(), + style: Theme.of(context).textTheme.bodyMedium, + ), + leading: const Icon(Icons.web), + onTap: () => Navigator.of(context) + .push(materialRoute(const RecoverByOldTokenInstruction( + instructionFilename: 'how_fallback_terminal', + ))), ), - subtitle: Text( - 'recovering.fallback_select_provider_console_hint'.tr(), - style: Theme.of(context).textTheme.bodyMedium, - ), - leading: const Icon(Icons.web), - onTap: () => Navigator.of(context) - .push(materialRoute(const RecoverByOldTokenInstruction( - instructionFilename: 'how_fallback_terminal', - ))), ), - ), - ], + ], + ), ); } } diff --git a/lib/ui/pages/setup/recovering/recovery_routing.dart b/lib/ui/pages/setup/recovering/recovery_routing.dart index 65ff5688..b43ff7e7 100644 --- a/lib/ui/pages/setup/recovering/recovery_routing.dart +++ b/lib/ui/pages/setup/recovering/recovery_routing.dart @@ -27,10 +27,14 @@ class RecoveryRouting extends StatelessWidget { if (serverInstallation is ServerInstallationRecovery) { switch (serverInstallation.currentStep) { case RecoveryStep.selecting: - if (serverInstallation.recoveryCapabilities != - ServerRecoveryCapabilities.none) { + if (serverInstallation.recoveryCapabilities == + ServerRecoveryCapabilities.loginTokens) { currentPage = const RecoveryMethodSelect(); } + if (serverInstallation.recoveryCapabilities == + ServerRecoveryCapabilities.legacy) { + currentPage = const RecoveryFallbackMethodSelect(); + } break; case RecoveryStep.recoveryKey: currentPage = const RecoverByRecoveryKey(); From b60fb19ecc64e27928f39ba4e78516305e65c045 Mon Sep 17 00:00:00 2001 From: Inex Code Date: Mon, 30 May 2022 16:49:42 +0300 Subject: [PATCH 35/52] some ui fixes --- lib/logic/common_enum/common_enum.dart | 5 +- .../components/brand_header/brand_header.dart | 4 -- .../brand_hero_screen/brand_hero_screen.dart | 3 +- lib/ui/pages/more/more.dart | 1 - lib/ui/pages/providers/providers.dart | 1 - lib/ui/pages/root_route.dart | 67 ++++++++++++++++--- lib/ui/pages/services/services.dart | 1 - lib/ui/pages/users/fab.dart | 36 ---------- lib/ui/pages/users/new_user.dart | 2 +- lib/ui/pages/users/users.dart | 3 - 10 files changed, 61 insertions(+), 62 deletions(-) delete mode 100644 lib/ui/pages/users/fab.dart diff --git a/lib/logic/common_enum/common_enum.dart b/lib/logic/common_enum/common_enum.dart index 94acface..3a1a4d80 100644 --- a/lib/logic/common_enum/common_enum.dart +++ b/lib/logic/common_enum/common_enum.dart @@ -1,6 +1,5 @@ import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:ionicons/ionicons.dart'; +import 'package:flutter/material.dart'; import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart'; enum LoadingStatus { @@ -134,7 +133,7 @@ extension ServiceTypesExt on ServiceTypes { case ServiceTypes.git: return BrandIcons.git; case ServiceTypes.vpn: - return Ionicons.shield_checkmark_outline; + return Icons.vpn_lock_outlined; } } diff --git a/lib/ui/components/brand_header/brand_header.dart b/lib/ui/components/brand_header/brand_header.dart index 9f136466..82e73e47 100644 --- a/lib/ui/components/brand_header/brand_header.dart +++ b/lib/ui/components/brand_header/brand_header.dart @@ -1,20 +1,17 @@ import 'package:flutter/material.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/pre_styled_buttons/pre_styled_buttons.dart'; class BrandHeader extends StatelessWidget { const BrandHeader({ Key? key, this.title = '', this.hasBackButton = false, - this.hasFlashButton = false, this.onBackButtonPressed, }) : super(key: key); final String title; final bool hasBackButton; - final bool hasFlashButton; final VoidCallback? onBackButtonPressed; @override @@ -37,7 +34,6 @@ class BrandHeader extends StatelessWidget { ], BrandText.h4(title), const Spacer(), - if (hasFlashButton) PreStyledButtons.flash(), ], ), ); diff --git a/lib/ui/components/brand_hero_screen/brand_hero_screen.dart b/lib/ui/components/brand_hero_screen/brand_hero_screen.dart index 105267a9..33ddcfa8 100644 --- a/lib/ui/components/brand_hero_screen/brand_hero_screen.dart +++ b/lib/ui/components/brand_hero_screen/brand_hero_screen.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:selfprivacy/ui/components/brand_header/brand_header.dart'; +import 'package:selfprivacy/ui/components/pre_styled_buttons/flash_fab.dart'; class BrandHeroScreen extends StatelessWidget { const BrandHeroScreen({ @@ -32,10 +33,10 @@ class BrandHeroScreen extends StatelessWidget { child: BrandHeader( title: headerTitle, hasBackButton: hasBackButton, - hasFlashButton: hasFlashButton, onBackButtonPressed: onBackButtonPressed, ), ), + floatingActionButton: hasFlashButton ? const BrandFab() : null, body: ListView( padding: const EdgeInsets.all(16.0), children: [ diff --git a/lib/ui/pages/more/more.dart b/lib/ui/pages/more/more.dart index 55b6239a..3897c19f 100644 --- a/lib/ui/pages/more/more.dart +++ b/lib/ui/pages/more/more.dart @@ -30,7 +30,6 @@ class MorePage extends StatelessWidget { preferredSize: const Size.fromHeight(52), child: BrandHeader( title: 'basis.more'.tr(), - hasFlashButton: true, ), ), body: ListView( diff --git a/lib/ui/pages/providers/providers.dart b/lib/ui/pages/providers/providers.dart index b3566f2f..2abed128 100644 --- a/lib/ui/pages/providers/providers.dart +++ b/lib/ui/pages/providers/providers.dart @@ -70,7 +70,6 @@ class _ProvidersPageState extends State { preferredSize: const Size.fromHeight(52), child: BrandHeader( title: 'providers.page_title'.tr(), - hasFlashButton: true, ), ), body: ListView( diff --git a/lib/ui/pages/root_route.dart b/lib/ui/pages/root_route.dart index e3ad4ce4..b14ecd52 100644 --- a/lib/ui/pages/root_route.dart +++ b/lib/ui/pages/root_route.dart @@ -16,20 +16,36 @@ class RootPage extends StatefulWidget { State createState() => _RootPageState(); } -class _RootPageState extends State - with SingleTickerProviderStateMixin { +class _RootPageState extends State with TickerProviderStateMixin { late TabController tabController; + 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() { - super.dispose(); tabController.dispose(); + _controller.dispose(); + super.dispose(); } var selfprivacyServer = ServerApi(); @@ -37,10 +53,10 @@ class _RootPageState extends State @override Widget build(BuildContext context) { return SafeArea( - child: Scaffold( - body: Provider( - create: (_) => ChangeTab(tabController.animateTo), - child: TabBarView( + child: Provider( + create: (_) => ChangeTab(tabController.animateTo), + child: Scaffold( + body: TabBarView( controller: tabController, children: const [ ProvidersPage(), @@ -49,11 +65,40 @@ class _RootPageState extends State MorePage(), ], ), + bottomNavigationBar: BrandTabBar( + controller: tabController, + ), + floatingActionButton: SizedBox( + height: 104 + 16, + child: Column( + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + ScaleTransition( + scale: _animation, + child: FloatingActionButton.small( + heroTag: 'new_user_fab', + onPressed: () { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (BuildContext context) { + return Padding( + padding: MediaQuery.of(context).viewInsets, + child: NewUser()); + }, + ); + }, + child: const Icon(Icons.person_add_outlined), + ), + ), + const SizedBox(height: 16), + const BrandFab(), + ], + ), + ), ), - bottomNavigationBar: BrandTabBar( - controller: tabController, - ), - floatingActionButton: const BrandFab(), ), ); } diff --git a/lib/ui/pages/services/services.dart b/lib/ui/pages/services/services.dart index e2f3f4ed..c5d781d4 100644 --- a/lib/ui/pages/services/services.dart +++ b/lib/ui/pages/services/services.dart @@ -65,7 +65,6 @@ class _ServicesPageState extends State { preferredSize: const Size.fromHeight(52), child: BrandHeader( title: 'basis.services'.tr(), - hasFlashButton: true, ), ), body: ListView( diff --git a/lib/ui/pages/users/fab.dart b/lib/ui/pages/users/fab.dart deleted file mode 100644 index e6f0ed29..00000000 --- a/lib/ui/pages/users/fab.dart +++ /dev/null @@ -1,36 +0,0 @@ -part of 'users.dart'; - -class _Fab extends StatelessWidget { - const _Fab({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return SizedBox( - width: 48.0, - height: 48.0, - child: RawMaterialButton( - fillColor: BrandColors.blue, - shape: const CircleBorder(), - elevation: 0.0, - highlightElevation: 2, - child: const Icon( - Icons.add, - color: Colors.white, - size: 34, - ), - onPressed: () { - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (BuildContext context) { - return Padding( - padding: MediaQuery.of(context).viewInsets, - child: _NewUser()); - }, - ); - }, - ), - ); - } -} diff --git a/lib/ui/pages/users/new_user.dart b/lib/ui/pages/users/new_user.dart index 9336d5c2..5b023bad 100644 --- a/lib/ui/pages/users/new_user.dart +++ b/lib/ui/pages/users/new_user.dart @@ -1,6 +1,6 @@ part of 'users.dart'; -class _NewUser extends StatelessWidget { +class NewUser extends StatelessWidget { @override Widget build(BuildContext context) { var config = context.watch().state; diff --git a/lib/ui/pages/users/users.dart b/lib/ui/pages/users/users.dart index 133dfb16..9276b759 100644 --- a/lib/ui/pages/users/users.dart +++ b/lib/ui/pages/users/users.dart @@ -27,7 +27,6 @@ import 'package:share_plus/share_plus.dart'; import '../../../utils/route_transitions/basic.dart'; part 'empty.dart'; -part 'fab.dart'; part 'new_user.dart'; part 'user.dart'; part 'user_details.dart'; @@ -77,10 +76,8 @@ class UsersPage extends StatelessWidget { preferredSize: const Size.fromHeight(52), child: BrandHeader( title: 'basis.users'.tr(), - hasFlashButton: true, ), ), - floatingActionButton: isReady ? const _Fab() : null, body: child, ); } From ead19d2210bc484c4db0a46a81a3e44f7d39c9ac Mon Sep 17 00:00:00 2001 From: NaiJi Date: Mon, 30 May 2022 16:55:52 +0300 Subject: [PATCH 36/52] Finish recovery key workflow and pages Co-authored-by: Inex Code --- .../server_installation_cubit.dart | 23 +- .../server_installation_repository.dart | 6 +- .../components/brand_button/brand_button.dart | 2 +- .../brand_button/filled_button.dart | 14 +- .../brand_hero_screen/brand_hero_screen.dart | 8 +- lib/ui/pages/more/more.dart | 8 + lib/ui/pages/recovery_key/recovery_key.dart | 384 ++++++++++++------ .../recovery_key/recovery_key_receiving.dart | 4 +- .../recovering/recover_by_old_token.dart | 4 +- .../recovery_confirm_backblaze.dart | 6 +- .../recovery_confirm_cloudflare.dart | 4 +- .../recovering/recovery_confirm_server.dart | 115 ++++-- pubspec.lock | 16 +- 13 files changed, 404 insertions(+), 190 deletions(-) diff --git a/lib/logic/cubit/server_installation/server_installation_cubit.dart b/lib/logic/cubit/server_installation/server_installation_cubit.dart index 9aedf6fb..44e06862 100644 --- a/lib/logic/cubit/server_installation/server_installation_cubit.dart +++ b/lib/logic/cubit/server_installation/server_installation_cubit.dart @@ -80,12 +80,7 @@ class ServerInstallationCubit extends Cubit { ); await repository.saveBackblazeKey(backblazeCredential); if (state is ServerInstallationRecovery) { - final mainUser = await repository.getMainUser(); - final updatedState = (state as ServerInstallationRecovery).copyWith( - backblazeCredential: backblazeCredential, - rootUser: mainUser, - ); - emit(updatedState.finish()); + finishRecoveryProcess(backblazeCredential); return; } emit((state as ServerInstallationNotFinished) @@ -458,6 +453,19 @@ class ServerInstallationCubit extends Cubit { )); } + void finishRecoveryProcess(BackblazeCredential backblazeCredential) async { + await repository.saveIsServerStarted(true); + await repository.saveIsServerResetedFirstTime(true); + await repository.saveIsServerResetedSecondTime(true); + await repository.saveHasFinalChecked(true); + final mainUser = await repository.getMainUser(); + final updatedState = (state as ServerInstallationRecovery).copyWith( + backblazeCredential: backblazeCredential, + rootUser: mainUser, + ); + emit(updatedState.finish()); + } + @override void onChange(Change change) { super.onChange(change); @@ -474,6 +482,9 @@ class ServerInstallationCubit extends Cubit { print( 'Recovery Capabilities: ${(change.nextState as ServerInstallationRecovery).recoveryCapabilities}'); } + if (change.nextState is TimerState) { + print('Timer: ${(change.nextState as TimerState).duration}'); + } } void clearAppConfig() { diff --git a/lib/logic/cubit/server_installation/server_installation_repository.dart b/lib/logic/cubit/server_installation/server_installation_repository.dart index 5a1bfb58..a31ef764 100644 --- a/lib/logic/cubit/server_installation/server_installation_repository.dart +++ b/lib/logic/cubit/server_installation/server_installation_repository.dart @@ -431,6 +431,7 @@ class ServerInstallationRepository { isWithToken: false, overrideDomain: serverDomain.domainName, ); + final serverIp = await getServerIpFromDomain(serverDomain); final apiResponse = await serverApi.useRecoveryToken( DeviceToken(device: await getDeviceName(), token: recoveryKey)); @@ -443,7 +444,7 @@ class ServerInstallationRepository { ), provider: ServerProvider.unknown, id: 0, - ip4: '', + ip4: serverIp, startTime: null, createTime: null, ); @@ -464,6 +465,7 @@ class ServerInstallationRepository { overrideDomain: serverDomain.domainName, customToken: apiToken, ); + final serverIp = await getServerIpFromDomain(serverDomain); if (recoveryCapabilities == ServerRecoveryCapabilities.legacy) { final apiResponse = await serverApi.servicesPowerCheck(); if (apiResponse.isNotEmpty) { @@ -475,7 +477,7 @@ class ServerInstallationRepository { ), provider: ServerProvider.unknown, id: 0, - ip4: '', + ip4: serverIp, startTime: null, createTime: null, ); diff --git a/lib/ui/components/brand_button/brand_button.dart b/lib/ui/components/brand_button/brand_button.dart index 186056c0..0b5b9c49 100644 --- a/lib/ui/components/brand_button/brand_button.dart +++ b/lib/ui/components/brand_button/brand_button.dart @@ -34,7 +34,7 @@ class BrandButton { }) => ConstrainedBox( constraints: const BoxConstraints( - minHeight: 48, + minHeight: 40, minWidth: double.infinity, ), child: TextButton(onPressed: onPressed, child: Text(title)), diff --git a/lib/ui/components/brand_button/filled_button.dart b/lib/ui/components/brand_button/filled_button.dart index a3230ddf..7bf1b1dd 100644 --- a/lib/ui/components/brand_button/filled_button.dart +++ b/lib/ui/components/brand_button/filled_button.dart @@ -26,10 +26,16 @@ class FilledButton extends StatelessWidget { primary: Theme.of(context).colorScheme.onSurface.withAlpha(98), ).copyWith(elevation: ButtonStyleButton.allOrNull(0.0)); - return ElevatedButton( - onPressed: onPressed, - style: disabled ? _disabledStyle : _enabledStyle, - child: child ?? Text(title ?? ''), + return ConstrainedBox( + constraints: const BoxConstraints( + minHeight: 40, + minWidth: double.infinity, + ), + child: ElevatedButton( + onPressed: onPressed, + style: disabled ? _disabledStyle : _enabledStyle, + child: child ?? Text(title ?? ''), + ), ); } } diff --git a/lib/ui/components/brand_hero_screen/brand_hero_screen.dart b/lib/ui/components/brand_hero_screen/brand_hero_screen.dart index 33ddcfa8..6d00963c 100644 --- a/lib/ui/components/brand_hero_screen/brand_hero_screen.dart +++ b/lib/ui/components/brand_hero_screen/brand_hero_screen.dart @@ -52,13 +52,17 @@ class BrandHeroScreen extends StatelessWidget { if (heroTitle != null) Text( heroTitle!, - style: Theme.of(context).textTheme.headlineMedium, + style: Theme.of(context).textTheme.headlineMedium!.copyWith( + color: Theme.of(context).colorScheme.onBackground, + ), textAlign: TextAlign.start, ), const SizedBox(height: 8.0), if (heroSubtitle != null) Text(heroSubtitle!, - style: Theme.of(context).textTheme.bodyMedium, + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: Theme.of(context).colorScheme.onBackground, + ), textAlign: TextAlign.start), const SizedBox(height: 16.0), ...children, diff --git a/lib/ui/pages/more/more.dart b/lib/ui/pages/more/more.dart index 3897c19f..49b5e840 100644 --- a/lib/ui/pages/more/more.dart +++ b/lib/ui/pages/more/more.dart @@ -8,6 +8,7 @@ import 'package:selfprivacy/ui/components/brand_divider/brand_divider.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/components/brand_text/brand_text.dart'; +import 'package:selfprivacy/ui/pages/recovery_key/recovery_key.dart'; import 'package:selfprivacy/ui/pages/setup/initializing.dart'; import 'package:selfprivacy/ui/pages/onboarding/onboarding.dart'; import 'package:selfprivacy/ui/pages/root_route.dart'; @@ -77,6 +78,13 @@ class MorePage extends StatelessWidget { goTo: SshKeysPage( user: context.read().state.rootUser, )), + _NavItem( + isEnabled: context.read().state + is ServerInstallationFinished, + iconData: Icons.password_outlined, + goTo: const RecoveryKey(), + title: 'recovery_key.key_main_header'.tr(), + ) ], ), ) diff --git a/lib/ui/pages/recovery_key/recovery_key.dart b/lib/ui/pages/recovery_key/recovery_key.dart index 30a7cc76..70dbaf8f 100644 --- a/lib/ui/pages/recovery_key/recovery_key.dart +++ b/lib/ui/pages/recovery_key/recovery_key.dart @@ -31,8 +31,8 @@ class _RecoveryKeyState extends State { switch (keyStatus.loadingStatus) { case LoadingStatus.refreshing: widgets = [ - const Icon(Icons.refresh_outlined), - const SizedBox(height: 18), + const Center(child: CircularProgressIndicator()), + const SizedBox(height: 16), BrandText( 'recovery_key.key_synchronizing'.tr(), type: TextType.h1, @@ -48,7 +48,7 @@ class _RecoveryKeyState extends State { case LoadingStatus.error: widgets = [ const Icon(Icons.sentiment_dissatisfied_outlined), - const SizedBox(height: 18), + const SizedBox(height: 16), BrandText( 'recovery_key.key_connection_error'.tr(), type: TextType.h1, @@ -75,78 +75,25 @@ class RecoveryKeyContent extends StatefulWidget { } class _RecoveryKeyContentState extends State { - bool _isAmountToggled = true; - bool _isExpirationToggled = true; bool _isConfigurationVisible = false; - final _amountController = TextEditingController(); - final _expirationController = TextEditingController(); - @override Widget build(BuildContext context) { - var keyStatus = context.read().state; - _isConfigurationVisible = !keyStatus.exists; + final keyStatus = context.watch().state; List widgets = []; if (keyStatus.exists) { - if (keyStatus.isValid) { - widgets = [ - BrandCards.filled( - child: ListTile( - title: Text('recovery_key.key_valid'.tr()), - leading: const Icon(Icons.check_circle_outlined), - tileColor: Colors.lightGreen, - ), - ), - ...widgets - ]; - } else { - widgets = [ - BrandCards.filled( - child: ListTile( - title: Text('recovery_key.key_invalid'.tr()), - leading: const Icon(Icons.cancel_outlined), - tileColor: Colors.redAccent, - ), - ), - ...widgets - ]; - } + widgets = [ + RecoveryKeyStatusCard(isValid: keyStatus.isValid), + RecoveryKeyInformation(state: keyStatus), + ...widgets, + ]; - if (keyStatus.expiresAt != null && !_isConfigurationVisible) { + if (_isConfigurationVisible) { widgets = [ ...widgets, - const SizedBox(height: 18), - Text( - 'recovery_key.key_valid_until'.tr( - args: [keyStatus.expiresAt!.toIso8601String()], - ), - ) - ]; - } - - if (keyStatus.usesLeft != null && !_isConfigurationVisible) { - widgets = [ - ...widgets, - const SizedBox(height: 18), - Text( - 'recovery_key.key_valid_for'.tr( - args: [keyStatus.usesLeft!.toString()], - ), - ) - ]; - } - - if (keyStatus.generatedAt != null && !_isConfigurationVisible) { - widgets = [ - ...widgets, - const SizedBox(height: 18), - Text( - 'recovery_key.key_creation_date'.tr( - args: [keyStatus.generatedAt!.toIso8601String()], - ), - ) + const RecoveryKeyConfiguration(), ]; } @@ -154,87 +101,274 @@ class _RecoveryKeyContentState extends State { if (keyStatus.isValid) { widgets = [ ...widgets, - const SizedBox(height: 18), + const SizedBox(height: 16), BrandButton.text( title: 'recovery_key.key_replace_button'.tr(), - onPressed: () => _isConfigurationVisible = true, + onPressed: () { + setState(() { + _isConfigurationVisible = true; + }); + }, ), ]; } else { widgets = [ ...widgets, - const SizedBox(height: 18), + const SizedBox(height: 16), FilledButton( title: 'recovery_key.key_replace_button'.tr(), - onPressed: () => _isConfigurationVisible = true, + onPressed: () { + setState(() { + _isConfigurationVisible = true; + }); + }, ), ]; } } } - if (_isConfigurationVisible) { + if (!keyStatus.exists) { widgets = [ ...widgets, - const SizedBox(height: 18), - Row( - children: [ - Text('key_amount_toggle'.tr()), - Switch( - value: _isAmountToggled, - onChanged: (bool toogled) => _isAmountToggled = toogled, - ), - ], - ), - const SizedBox(height: 18), - TextField( - enabled: _isAmountToggled, - controller: _amountController, - decoration: InputDecoration( - labelText: 'recovery_key.key_amount_field_title'.tr()), - keyboardType: TextInputType.number, - inputFormatters: [ - FilteringTextInputFormatter.digitsOnly, - ], // Only numbers can be entered - ), - const SizedBox(height: 18), - Row( - children: [ - Text('key_duedate_toggle'.tr()), - Switch( - value: _isExpirationToggled, - onChanged: (bool toogled) => _isExpirationToggled = toogled, - ), - ], - ), - const SizedBox(height: 18), - TextField( - enabled: _isExpirationToggled, - controller: _expirationController, - decoration: InputDecoration( - labelText: 'recovery_key.key_duedate_field_title'.tr()), - keyboardType: TextInputType.number, - inputFormatters: [ - FilteringTextInputFormatter.digitsOnly, - ], // Only numbers can be entered - ), - const SizedBox(height: 18), - FilledButton( - title: 'recovery_key.key_receive_button'.tr(), - disabled: - (_isExpirationToggled && _expirationController.text.isEmpty) || - (_isAmountToggled && _amountController.text.isEmpty), - onPressed: () { - Navigator.of(context).push( - materialRoute( - const RecoveryKeyReceiving(recoveryKey: ''), // TO DO - ), - ); - }, - ), + const RecoveryKeyConfiguration(), ]; } return Column(children: widgets); } } + +class RecoveryKeyStatusCard extends StatelessWidget { + const RecoveryKeyStatusCard({required this.isValid, Key? key}) + : super(key: key); + + final bool isValid; + + @override + Widget build(BuildContext context) { + return BrandCards.filled( + child: ListTile( + title: isValid + ? Text( + 'recovery_key.key_valid'.tr(), + style: Theme.of(context).textTheme.titleMedium!.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ) + : Text( + 'recovery_key.key_invalid'.tr(), + style: Theme.of(context).textTheme.titleMedium!.copyWith( + color: Theme.of(context).colorScheme.onErrorContainer, + ), + ), + leading: isValid + ? Icon( + Icons.check_circle_outlined, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ) + : Icon( + Icons.cancel_outlined, + color: Theme.of(context).colorScheme.onErrorContainer, + ), + tileColor: isValid + ? Theme.of(context).colorScheme.surfaceVariant + : Theme.of(context).colorScheme.errorContainer, + ), + ); + } +} + +class RecoveryKeyInformation extends StatelessWidget { + const RecoveryKeyInformation({required this.state, Key? key}) + : super(key: key); + + final RecoveryKeyState state; + + @override + Widget build(BuildContext context) { + const padding = EdgeInsets.symmetric(vertical: 8.0); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (state.expiresAt != null) + Padding( + padding: padding, + child: Text( + 'recovery_key.key_valid_until'.tr( + args: [state.expiresAt!.toIso8601String()], + ), + ), + ), + if (state.usesLeft != null) + Padding( + padding: padding, + child: Text( + 'recovery_key.key_valid_for'.tr( + args: [state.usesLeft!.toString()], + ), + ), + ), + if (state.generatedAt != null) + Padding( + padding: padding, + child: Text( + 'recovery_key.key_creation_date'.tr( + args: [state.generatedAt!.toIso8601String()], + ), + textAlign: TextAlign.start, + ), + ), + ], + ); + } +} + +class RecoveryKeyConfiguration extends StatefulWidget { + const RecoveryKeyConfiguration({Key? key}) : super(key: key); + + @override + State createState() => _RecoveryKeyConfigurationState(); +} + +class _RecoveryKeyConfigurationState extends State { + bool _isAmountToggled = false; + bool _isExpirationToggled = false; + + bool _isAmountError = false; + bool _isExpirationError = false; + + final TextEditingController _amountController = TextEditingController(); + final TextEditingController _expirationController = TextEditingController(); + + DateTime _selectedDate = DateTime.now(); + bool _isDateSelected = false; + + @override + Widget build(BuildContext context) { + if (_isDateSelected) { + _expirationController.text = _selectedDate.toIso8601String(); + } + + return Column( + children: [ + const SizedBox(height: 16), + Row( + children: [ + Text('recovery_key.key_amount_toggle'.tr()), + Switch( + value: _isAmountToggled, + onChanged: (bool toogled) { + setState( + () { + _isAmountToggled = toogled; + _isExpirationToggled = _isExpirationToggled; + }, + ); + }, + ), + ], + ), + const SizedBox(height: 16), + TextField( + enabled: _isAmountToggled, + controller: _amountController, + decoration: InputDecoration( + errorText: _isAmountError ? ' ' : null, + labelText: 'recovery_key.key_amount_field_title'.tr()), + keyboardType: TextInputType.number, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + ], // Only numbers can be entered + ), + const SizedBox(height: 16), + Row( + children: [ + Text('recovery_key.key_duedate_toggle'.tr()), + Switch( + value: _isExpirationToggled, + onChanged: (bool toogled) { + setState( + () { + _isAmountToggled = _isAmountToggled; + _isExpirationToggled = toogled; + }, + ); + }, + ), + ], + ), + const SizedBox(height: 16), + TextField( + enabled: _isExpirationToggled, + controller: _expirationController, + onTap: () { + _selectDate(context); + }, + decoration: InputDecoration( + errorText: _isExpirationError ? ' ' : null, + labelText: 'recovery_key.key_duedate_field_title'.tr()), + keyboardType: TextInputType.number, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + ], // Only numbers can be entered + ), + const SizedBox(height: 16), + FilledButton( + title: 'recovery_key.key_receive_button'.tr(), + onPressed: () { + if (_isExpirationToggled && _expirationController.text.isEmpty) { + setState(() { + _isExpirationError = true; + _isAmountError = false; + _isAmountToggled = _isAmountToggled; + _isExpirationToggled = _isExpirationToggled; + }); + return; + } else if (_isAmountToggled && _amountController.text.isEmpty) { + setState(() { + _isAmountError = true; + _isExpirationError = false; + _isAmountToggled = _isAmountToggled; + _isExpirationToggled = _isExpirationToggled; + }); + return; + } else { + setState(() { + _isAmountError = false; + _isExpirationError = false; + _isAmountToggled = _isAmountToggled; + _isExpirationToggled = _isExpirationToggled; + }); + + Navigator.of(context).push( + materialRoute( + const RecoveryKeyReceiving(recoveryKey: ''), // TO DO + ), + ); + } + }, + ), + ], + ); + } + + Future _selectDate(BuildContext context) async { + final selected = await showDatePicker( + context: context, + initialDate: _selectedDate, + firstDate: DateTime.now(), + lastDate: DateTime(DateTime.now().year + 50)); + + if (selected != null && selected != _selectedDate) { + setState( + () { + _selectedDate = selected; + _isDateSelected = true; + }, + ); + } + + return _selectedDate; + } +} diff --git a/lib/ui/pages/recovery_key/recovery_key_receiving.dart b/lib/ui/pages/recovery_key/recovery_key_receiving.dart index 8605e871..edd3f89e 100644 --- a/lib/ui/pages/recovery_key/recovery_key_receiving.dart +++ b/lib/ui/pages/recovery_key/recovery_key_receiving.dart @@ -20,10 +20,10 @@ class RecoveryKeyReceiving extends StatelessWidget { hasFlashButton: false, children: [ Text(recoveryKey, style: Theme.of(context).textTheme.bodyLarge), - const SizedBox(height: 18), + const SizedBox(height: 16), const Icon(Icons.info_outlined, size: 14), Text('recovery_key.key_receiving_info'.tr()), - const SizedBox(height: 18), + const SizedBox(height: 16), FilledButton( title: 'recovery_key.key_receiving_done'.tr(), onPressed: () { 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 d62221e9..46c1b3b4 100644 --- a/lib/ui/pages/setup/recovering/recover_by_old_token.dart +++ b/lib/ui/pages/setup/recovering/recover_by_old_token.dart @@ -33,7 +33,7 @@ class RecoverByOldTokenInstruction extends StatelessWidget { BrandMarkdown( fileName: instructionFilename, ), - const SizedBox(height: 18), + const SizedBox(height: 16), FilledButton( title: 'recovering.method_device_button'.tr(), onPressed: () => context @@ -79,7 +79,7 @@ class RecoverByOldToken extends StatelessWidget { labelText: 'recovering.method_device_input_placeholder'.tr(), ), ), - const SizedBox(height: 18), + const SizedBox(height: 16), FilledButton( title: 'more.continue'.tr(), onPressed: formCubitState.isSubmitting diff --git a/lib/ui/pages/setup/recovering/recovery_confirm_backblaze.dart b/lib/ui/pages/setup/recovering/recovery_confirm_backblaze.dart index 63e3a019..646984a3 100644 --- a/lib/ui/pages/setup/recovering/recovery_confirm_backblaze.dart +++ b/lib/ui/pages/setup/recovering/recovery_confirm_backblaze.dart @@ -36,7 +36,7 @@ class RecoveryConfirmBackblaze extends StatelessWidget { hintText: 'KeyID', ), ), - const SizedBox(height: 18), + const SizedBox(height: 16), CubitFormTextField( formFieldCubit: context.read().applicationKey, textAlign: TextAlign.center, @@ -46,14 +46,14 @@ class RecoveryConfirmBackblaze extends StatelessWidget { hintText: 'Master Application Key', ), ), - const SizedBox(height: 18), + const SizedBox(height: 16), BrandButton.rised( onPressed: formCubitState.isSubmitting ? null : () => context.read().trySubmit(), text: 'basis.connect'.tr(), ), - const SizedBox(height: 18), + const SizedBox(height: 16), BrandButton.text( onPressed: () => showModalBottomSheet( context: context, diff --git a/lib/ui/pages/setup/recovering/recovery_confirm_cloudflare.dart b/lib/ui/pages/setup/recovering/recovery_confirm_cloudflare.dart index 19dce048..1006e390 100644 --- a/lib/ui/pages/setup/recovering/recovery_confirm_cloudflare.dart +++ b/lib/ui/pages/setup/recovering/recovery_confirm_cloudflare.dart @@ -38,14 +38,14 @@ class RecoveryConfirmCloudflare extends StatelessWidget { hintText: 'initializing.5'.tr(), ), ), - const SizedBox(height: 18), + const SizedBox(height: 16), BrandButton.rised( onPressed: formCubitState.isSubmitting ? null : () => context.read().trySubmit(), text: 'basis.connect'.tr(), ), - const SizedBox(height: 18), + const SizedBox(height: 16), BrandButton.text( onPressed: () => showModalBottomSheet( context: context, diff --git a/lib/ui/pages/setup/recovering/recovery_confirm_server.dart b/lib/ui/pages/setup/recovering/recovery_confirm_server.dart index 96bcb51d..e1b5403a 100644 --- a/lib/ui/pages/setup/recovering/recovery_confirm_server.dart +++ b/lib/ui/pages/setup/recovering/recovery_confirm_server.dart @@ -133,22 +133,54 @@ class _RecoveryConfirmServerState extends State { VoidCallback? onTap}) { return BrandCards.filled( child: ListTile( + contentPadding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 16), onTap: onTap, - title: Text(server.name), - leading: const Icon(Icons.dns), + title: Text( + server.name, + style: Theme.of(context).textTheme.titleMedium!.copyWith( + color: Theme.of(context).colorScheme.onSurface, + ), + ), + leading: Icon( + Icons.dns, + color: Theme.of(context).colorScheme.onSurface, + ), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ - Icon(server.isReverseDnsValid ? Icons.check : Icons.close), - Text('rDNS: ${server.reverseDns}'), + Icon( + server.isReverseDnsValid ? Icons.check : Icons.close, + color: Theme.of(context).colorScheme.onSurface, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + 'rDNS: ${server.reverseDns}', + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: Theme.of(context).colorScheme.onSurface, + ), + ), + ), ], ), Row( children: [ - Icon(server.isIpValid ? Icons.check : Icons.close), - Text('IP: ${server.ip}'), + Icon( + server.isIpValid ? Icons.check : Icons.close, + color: Theme.of(context).colorScheme.onSurface, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + 'IP: ${server.ip}', + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: Theme.of(context).colorScheme.onSurface, + ), + ), + ), ], ), ], @@ -186,27 +218,19 @@ class _RecoveryConfirmServerState extends State { style: Theme.of(context).textTheme.titleMedium, textAlign: TextAlign.start, ), - const SizedBox(height: 4), - Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Icon(server.isReverseDnsValid ? Icons.check : Icons.close), - const SizedBox(width: 8), - Text(server.isReverseDnsValid - ? 'recovering.modal_confirmation_dns_valid'.tr() - : 'recovering.modal_confirmation_dns_invalid'.tr()), - ], + const SizedBox(height: 8), + IsValidStringDisplay( + isValid: server.isReverseDnsValid, + textIfValid: 'recovering.modal_confirmation_dns_valid'.tr(), + textIfInvalid: + 'recovering.modal_confirmation_dns_invalid'.tr(), ), - const SizedBox(height: 4), - Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Icon(server.isIpValid ? Icons.check : Icons.close), - const SizedBox(width: 8), - Text(server.isIpValid - ? 'recovering.modal_confirmation_ip_valid'.tr() - : 'recovering.modal_confirmation_ip_invalid'.tr()), - ], + const SizedBox(height: 8), + IsValidStringDisplay( + isValid: server.isIpValid, + textIfValid: 'recovering.modal_confirmation_ip_valid'.tr(), + textIfInvalid: + 'recovering.modal_confirmation_ip_invalid'.tr(), ), ], ), @@ -229,3 +253,42 @@ class _RecoveryConfirmServerState extends State { }, ); } + +class IsValidStringDisplay extends StatelessWidget { + const IsValidStringDisplay({ + Key? key, + required this.isValid, + required this.textIfValid, + required this.textIfInvalid, + }) : super(key: key); + + final bool isValid; + final String textIfValid; + final String textIfInvalid; + + @override + Widget build(BuildContext context) { + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + isValid + ? Icon(Icons.check, color: Theme.of(context).colorScheme.onSurface) + : Icon(Icons.close, color: Theme.of(context).colorScheme.error), + const SizedBox(width: 8), + isValid + ? Text( + textIfValid, + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: Theme.of(context).colorScheme.onSurface, + ), + ) + : Text( + textIfInvalid, + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: Theme.of(context).colorScheme.error, + ), + ) + ], + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index e3faf1b6..965eb666 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -363,13 +363,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.9.2" - flutter_lints: - dependency: "direct dev" - description: - name: flutter_lints - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.1" flutter_localizations: dependency: transitive description: flutter @@ -526,7 +519,7 @@ packages: source: hosted version: "3.1.3" intl: - dependency: "direct main" + dependency: transitive description: name: intl url: "https://pub.dartlang.org" @@ -567,13 +560,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "6.2.0" - lints: - dependency: transitive - description: - name: lints - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.0" local_auth: dependency: "direct main" description: From 1db8e9556ef7052c67a51b00f1375ce862115c22 Mon Sep 17 00:00:00 2001 From: NaiJi Date: Mon, 30 May 2022 19:55:09 +0300 Subject: [PATCH 37/52] Fix UI colors and such :) Co-authored-by: Inex Code --- lib/config/text_themes.dart | 2 - .../components/brand_cards/brand_cards.dart | 4 +- .../components/brand_switch/brand_switch.dart | 5 +- .../icon_status_mask/icon_status_mask.dart | 5 +- lib/ui/pages/recovery_key/recovery_key.dart | 304 +++++++++--------- lib/ui/pages/services/services.dart | 11 +- lib/ui/pages/users/new_user.dart | 4 +- 7 files changed, 172 insertions(+), 163 deletions(-) diff --git a/lib/config/text_themes.dart b/lib/config/text_themes.dart index 8e775783..ae166980 100644 --- a/lib/config/text_themes.dart +++ b/lib/config/text_themes.dart @@ -67,8 +67,6 @@ final mediumStyle = defaultTextStyle.copyWith(fontSize: 13, height: 1.53); final smallStyle = defaultTextStyle.copyWith(fontSize: 11, height: 1.45); -final linkStyle = defaultTextStyle.copyWith(color: BrandColors.blue); - const progressTextStyleLight = TextStyle( fontSize: 11, color: BrandColors.textColor1, diff --git a/lib/ui/components/brand_cards/brand_cards.dart b/lib/ui/components/brand_cards/brand_cards.dart index b290c00e..660054a8 100644 --- a/lib/ui/components/brand_cards/brand_cards.dart +++ b/lib/ui/components/brand_cards/brand_cards.dart @@ -46,9 +46,7 @@ class _BrandCard extends StatelessWidget { Widget build(BuildContext context) { return Container( decoration: BoxDecoration( - color: Theme.of(context).brightness == Brightness.dark - ? BrandColors.black - : BrandColors.white, + color: Theme.of(context).colorScheme.surface, borderRadius: borderRadius, boxShadow: shadow, ), diff --git a/lib/ui/components/brand_switch/brand_switch.dart b/lib/ui/components/brand_switch/brand_switch.dart index 60c411cf..adf7e4e5 100644 --- a/lib/ui/components/brand_switch/brand_switch.dart +++ b/lib/ui/components/brand_switch/brand_switch.dart @@ -1,7 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:selfprivacy/config/brand_colors.dart'; - class BrandSwitch extends StatelessWidget { const BrandSwitch({ Key? key, @@ -15,8 +13,7 @@ class BrandSwitch extends StatelessWidget { @override Widget build(BuildContext context) { return Switch( - activeColor: BrandColors.green1, - activeTrackColor: BrandColors.green2, + activeColor: Theme.of(context).colorScheme.primary, value: value, onChanged: onChanged, ); diff --git a/lib/ui/components/icon_status_mask/icon_status_mask.dart b/lib/ui/components/icon_status_mask/icon_status_mask.dart index c1f3b80c..64f50668 100644 --- a/lib/ui/components/icon_status_mask/icon_status_mask.dart +++ b/lib/ui/components/icon_status_mask/icon_status_mask.dart @@ -20,7 +20,10 @@ class IconStatusMask extends StatelessWidget { colors = BrandColors.uninitializedGradientColors; break; case StateType.stable: - colors = BrandColors.stableGradientColors; + colors = [ + Theme.of(context).colorScheme.primary, + Theme.of(context).colorScheme.tertiary, + ]; break; case StateType.warning: colors = BrandColors.warningGradientColors; diff --git a/lib/ui/pages/recovery_key/recovery_key.dart b/lib/ui/pages/recovery_key/recovery_key.dart index 70dbaf8f..67280596 100644 --- a/lib/ui/pages/recovery_key/recovery_key.dart +++ b/lib/ui/pages/recovery_key/recovery_key.dart @@ -81,61 +81,35 @@ class _RecoveryKeyContentState extends State { Widget build(BuildContext context) { final keyStatus = context.watch().state; - List widgets = []; - - if (keyStatus.exists) { - widgets = [ - RecoveryKeyStatusCard(isValid: keyStatus.isValid), - RecoveryKeyInformation(state: keyStatus), - ...widgets, - ]; - - if (_isConfigurationVisible) { - widgets = [ - ...widgets, - const RecoveryKeyConfiguration(), - ]; - } - - if (!_isConfigurationVisible) { - if (keyStatus.isValid) { - widgets = [ - ...widgets, - const SizedBox(height: 16), - BrandButton.text( - title: 'recovery_key.key_replace_button'.tr(), - onPressed: () { - setState(() { - _isConfigurationVisible = true; - }); - }, - ), - ]; - } else { - widgets = [ - ...widgets, - const SizedBox(height: 16), - FilledButton( - title: 'recovery_key.key_replace_button'.tr(), - onPressed: () { - setState(() { - _isConfigurationVisible = true; - }); - }, - ), - ]; - } - } - } - - if (!keyStatus.exists) { - widgets = [ - ...widgets, - const RecoveryKeyConfiguration(), - ]; - } - - return Column(children: widgets); + return Column( + children: [ + if (keyStatus.exists) RecoveryKeyStatusCard(isValid: keyStatus.isValid), + const SizedBox(height: 16), + if (keyStatus.exists && !_isConfigurationVisible) + RecoveryKeyInformation(state: keyStatus), + if (_isConfigurationVisible || !keyStatus.exists) + RecoveryKeyConfiguration(), + const SizedBox(height: 16), + if (!_isConfigurationVisible && keyStatus.isValid) + BrandButton.text( + title: 'recovery_key.key_replace_button'.tr(), + onPressed: () { + setState(() { + _isConfigurationVisible = true; + }); + }, + ), + if (!_isConfigurationVisible && !keyStatus.isValid) + FilledButton( + title: 'recovery_key.key_replace_button'.tr(), + onPressed: () { + setState(() { + _isConfigurationVisible = true; + }); + }, + ), + ], + ); } } @@ -235,119 +209,151 @@ class _RecoveryKeyConfigurationState extends State { bool _isAmountToggled = false; bool _isExpirationToggled = false; - bool _isAmountError = false; - bool _isExpirationError = false; - final TextEditingController _amountController = TextEditingController(); final TextEditingController _expirationController = TextEditingController(); + bool _isAmountError = false; + bool _isExpirationError = false; + DateTime _selectedDate = DateTime.now(); bool _isDateSelected = false; + void _updateErrorStatuses() { + final amount = _amountController.text; + final expiration = _expirationController.text; + + print('amount: $amount'); + print('_isAmountToggled: $_isAmountToggled'); + print('_isExpirationToggled: $_isExpirationToggled'); + + setState(() { + if (!_isAmountToggled) { + _isAmountError = false; + } else if (amount.isEmpty) { + _isAmountError = true; + } else { + final amountInt = int.tryParse(amount); + _isAmountError = amountInt == null || amountInt <= 0; + } + + if (!_isExpirationToggled) { + _isExpirationError = false; + } else if (expiration.isEmpty) { + _isExpirationError = true; + } else { + _isExpirationError = + _selectedDate == null || _selectedDate.isBefore(DateTime.now()); + } + }); + + print('_isAmountError: $_isAmountError'); + print('_isExpirationError: $_isExpirationError'); + } + @override Widget build(BuildContext context) { if (_isDateSelected) { - _expirationController.text = _selectedDate.toIso8601String(); + _expirationController.text = DateFormat.yMMMMd().format(_selectedDate); } + _amountController.addListener(_updateErrorStatuses); + + _expirationController.addListener(_updateErrorStatuses); + return Column( children: [ - const SizedBox(height: 16), - Row( - children: [ - Text('recovery_key.key_amount_toggle'.tr()), - Switch( - value: _isAmountToggled, - onChanged: (bool toogled) { - setState( - () { - _isAmountToggled = toogled; - _isExpirationToggled = _isExpirationToggled; - }, - ); + SwitchListTile( + value: _isAmountToggled, + title: Text('recovery_key.key_amount_toggle'.tr()), + activeColor: Theme.of(context).colorScheme.primary, + onChanged: (bool toogled) { + setState( + () { + _isAmountToggled = toogled; }, - ), - ], - ), - const SizedBox(height: 16), - TextField( - enabled: _isAmountToggled, - controller: _amountController, - decoration: InputDecoration( - errorText: _isAmountError ? ' ' : null, - labelText: 'recovery_key.key_amount_field_title'.tr()), - keyboardType: TextInputType.number, - inputFormatters: [ - FilteringTextInputFormatter.digitsOnly, - ], // Only numbers can be entered - ), - const SizedBox(height: 16), - Row( - children: [ - Text('recovery_key.key_duedate_toggle'.tr()), - Switch( - value: _isExpirationToggled, - onChanged: (bool toogled) { - setState( - () { - _isAmountToggled = _isAmountToggled; - _isExpirationToggled = toogled; - }, - ); - }, - ), - ], - ), - const SizedBox(height: 16), - TextField( - enabled: _isExpirationToggled, - controller: _expirationController, - onTap: () { - _selectDate(context); + ); + _updateErrorStatuses(); }, - decoration: InputDecoration( - errorText: _isExpirationError ? ' ' : null, - labelText: 'recovery_key.key_duedate_field_title'.tr()), - keyboardType: TextInputType.number, - inputFormatters: [ - FilteringTextInputFormatter.digitsOnly, - ], // Only numbers can be entered + ), + AnimatedCrossFade( + duration: const Duration(milliseconds: 200), + crossFadeState: _isAmountToggled + ? CrossFadeState.showFirst + : CrossFadeState.showSecond, + firstChild: Column( + children: [ + const SizedBox(height: 8), + TextField( + enabled: _isAmountToggled, + controller: _amountController, + decoration: InputDecoration( + border: OutlineInputBorder(), + errorText: _isAmountError ? ' ' : null, + labelText: 'recovery_key.key_amount_field_title'.tr()), + keyboardType: TextInputType.number, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + ], // Only numbers can be entered + ), + const SizedBox(height: 8), + ], + ), + secondChild: Container(), + ), + SwitchListTile( + value: _isExpirationToggled, + title: Text('recovery_key.key_duedate_toggle'.tr()), + activeColor: Theme.of(context).colorScheme.primary, + onChanged: (bool toogled) { + setState( + () { + _isExpirationToggled = toogled; + }, + ); + _updateErrorStatuses(); + }, + ), + AnimatedCrossFade( + duration: const Duration(milliseconds: 200), + crossFadeState: _isExpirationToggled + ? CrossFadeState.showFirst + : CrossFadeState.showSecond, + firstChild: Column( + children: [ + const SizedBox(height: 8), + TextField( + enabled: _isExpirationToggled, + controller: _expirationController, + onTap: () { + _selectDate(context); + }, + readOnly: true, + decoration: InputDecoration( + border: OutlineInputBorder(), + errorText: _isExpirationError ? ' ' : null, + labelText: 'recovery_key.key_duedate_field_title'.tr()), + keyboardType: TextInputType.number, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + ], // Only numbers can be entered + ), + ], + ), + secondChild: Container(), ), const SizedBox(height: 16), FilledButton( title: 'recovery_key.key_receive_button'.tr(), - onPressed: () { - if (_isExpirationToggled && _expirationController.text.isEmpty) { - setState(() { - _isExpirationError = true; - _isAmountError = false; - _isAmountToggled = _isAmountToggled; - _isExpirationToggled = _isExpirationToggled; - }); - return; - } else if (_isAmountToggled && _amountController.text.isEmpty) { - setState(() { - _isAmountError = true; - _isExpirationError = false; - _isAmountToggled = _isAmountToggled; - _isExpirationToggled = _isExpirationToggled; - }); - return; - } else { - setState(() { - _isAmountError = false; - _isExpirationError = false; - _isAmountToggled = _isAmountToggled; - _isExpirationToggled = _isExpirationToggled; - }); - - Navigator.of(context).push( - materialRoute( - const RecoveryKeyReceiving(recoveryKey: ''), // TO DO - ), - ); - } - }, + disabled: _isAmountError || _isExpirationError, + onPressed: !_isAmountError && !_isExpirationError + ? () { + Navigator.of(context).push( + materialRoute( + const RecoveryKeyReceiving(recoveryKey: ''), // TO DO + ), + ); + } + : null, ), ], ); diff --git a/lib/ui/pages/services/services.dart b/lib/ui/pages/services/services.dart index c5d781d4..c4939615 100644 --- a/lib/ui/pages/services/services.dart +++ b/lib/ui/pages/services/services.dart @@ -189,7 +189,11 @@ class _Card extends StatelessWidget { 'https://${serviceType.subdomain}.$domainName'), child: Text( '${serviceType.subdomain}.$domainName', - style: linkStyle, + style: TextStyle( + color: + Theme.of(context).colorScheme.secondary, + decoration: TextDecoration.underline, + ), ), ), const SizedBox(height: 10), @@ -199,7 +203,10 @@ class _Card extends StatelessWidget { Column(children: [ Text( domainName, - style: linkStyle, + style: TextStyle( + color: Theme.of(context).colorScheme.primary, + decoration: TextDecoration.underline, + ), ), const SizedBox(height: 10), ]), diff --git a/lib/ui/pages/users/new_user.dart b/lib/ui/pages/users/new_user.dart index 5b023bad..77a38918 100644 --- a/lib/ui/pages/users/new_user.dart +++ b/lib/ui/pages/users/new_user.dart @@ -67,9 +67,9 @@ class NewUser extends StatelessWidget { suffixIcon: Padding( padding: const EdgeInsets.only(right: 8), child: IconButton( - icon: const Icon( + icon: Icon( BrandIcons.refresh, - color: BrandColors.blue, + color: Theme.of(context).colorScheme.secondary, ), onPressed: context.read().genNewPassword, From 8ec3b8c3e3b373467f066287ac3f4ad3c484c418 Mon Sep 17 00:00:00 2001 From: Inex Code Date: Tue, 31 May 2022 02:06:08 +0300 Subject: [PATCH 38/52] Finish recovery key screen --- assets/translations/en.json | 5 +- assets/translations/ru.json | 14 +- lib/config/hive_config.dart | 3 + lib/logic/api_maps/server.dart | 3 +- .../recovery_device_form_cubit.dart | 11 +- .../server_installation_cubit.dart | 2 + .../server_installation_repository.dart | 7 +- .../brand_button/filled_button.dart | 6 +- .../components/brand_cards/brand_cards.dart | 16 +- lib/ui/pages/more/more.dart | 162 ++++++++---------- lib/ui/pages/recovery_key/recovery_key.dart | 117 ++++++++----- .../recovery_key/recovery_key_receiving.dart | 25 ++- lib/ui/pages/root_route.dart | 53 +++--- .../recovery_confirm_backblaze.dart | 8 +- .../recovery_confirm_cloudflare.dart | 4 +- .../recovering/recovery_confirm_server.dart | 34 ++-- lib/ui/pages/users/add_user_fab.dart | 25 +++ lib/ui/pages/users/new_user.dart | 2 + lib/ui/pages/users/users.dart | 1 + 19 files changed, 285 insertions(+), 213 deletions(-) create mode 100644 lib/ui/pages/users/add_user_fab.dart diff --git a/assets/translations/en.json b/assets/translations/en.json index 73234e0c..e75e564c 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -291,7 +291,7 @@ "method_select_other_device": "I have access on another device", "method_select_recovery_key": "I have a recovery key", "method_select_nothing": "I don't have any of that", - "method_device_description": "Open the application on another device, then go to the device page. Press \"Add device\" to receive your token.", + "method_device_description": "Open the application on another device, then go to the devices page. Press \"Add device\" to receive your token.", "method_device_button": "I have received my token", "method_device_input_description": "Enter your authorization token", "method_device_input_placeholder": "Token", @@ -342,7 +342,8 @@ "key_replace_button": "Generate new key", "key_receiving_description": "Write down this key and put to a safe place. It is used to restore full access to your server:", "key_receiving_info": "The key will never ever be shown again, but you will be able to replace it with another one.", - "key_receiving_done": "Done!" + "key_receiving_done": "Done!", + "generation_error": "Couldn't generate a recovery key. {}" }, "modals": { "_comment": "messages in modals", diff --git a/assets/translations/ru.json b/assets/translations/ru.json index 7b060a49..042998f4 100644 --- a/assets/translations/ru.json +++ b/assets/translations/ru.json @@ -323,13 +323,25 @@ "confirm_backblaze_description": "Введите токен Backblaze, который имеет права на хранилище резервных копий:" }, "recovery_key": { + "key_connection_error": "Не удалось соединиться с сервером", + "key_synchronizing": "Синхронизация...", "key_main_header": "Ключ восстановления", "key_main_description": "Требуется для авторизации SelfPrivacy, когда авторизованные устройства недоступны.", "key_amount_toggle": "Ограничить использования", "key_amount_field_title": "Макс. кол-во использований", "key_duedate_toggle": "Ограничить срок использования", "key_duedate_field_title": "Дата окончания срока", - "key_receive_button": "Получить ключ" + "key_receive_button": "Получить ключ", + "key_valid": "Ваш ключ действителен", + "key_invalid": "Ваш ключ больше не действителен", + "key_valid_until": "Действителен до {}", + "key_valid_for": "Можно использовать ещё {} раз", + "key_creation_date": "Создан {}", + "key_replace_button": "Сгенерировать новый ключ", + "key_receiving_description": "Запишите этот ключ в безопасном месте. Он предоставляет полный доступ к вашему серверу:", + "key_receiving_info": "Этот ключ больше не будет показан, но вы сможете заменить его новым.", + "key_receiving_done": "Готово!", + "generation_error": "Не удалось сгенерировать ключ. {}" }, "modals": { "_comment": "messages in modals", diff --git a/lib/config/hive_config.dart b/lib/config/hive_config.dart index 28748a98..e3db5dc7 100644 --- a/lib/config/hive_config.dart +++ b/lib/config/hive_config.dart @@ -109,6 +109,9 @@ class BNames { /// A boolean field of [serverInstallationBox] box. static String isServerResetedSecondTime = 'isServerResetedSecondTime'; + /// A boolean field of [serverInstallationBox] box. + static String isRecoveringServer = 'isRecoveringServer'; + /// Deprecated users box as it is unencrypted static String usersDeprecated = 'users'; diff --git a/lib/logic/api_maps/server.dart b/lib/logic/api_maps/server.dart index b58aff55..35c3c753 100644 --- a/lib/logic/api_maps/server.dart +++ b/lib/logic/api_maps/server.dart @@ -664,7 +664,8 @@ class ServerApi extends ApiMap { var client = await getClient(); var data = {}; if (expiration != null) { - data['expiration'] = expiration.toIso8601String(); + data['expiration'] = '${expiration.toIso8601String()}Z'; + print(data['expiration']); } if (uses != null) { data['uses'] = uses; diff --git a/lib/logic/cubit/forms/setup/recovering/recovery_device_form_cubit.dart b/lib/logic/cubit/forms/setup/recovering/recovery_device_form_cubit.dart index ddc35426..98c08f5c 100644 --- a/lib/logic/cubit/forms/setup/recovering/recovery_device_form_cubit.dart +++ b/lib/logic/cubit/forms/setup/recovering/recovery_device_form_cubit.dart @@ -14,7 +14,16 @@ class RecoveryDeviceFormCubit extends FormCubit { @override FutureOr onSubmit() async { - installationCubit.tryToRecover(tokenField.state.value, recoveryMethod); + late final String token; + // Trim spaces and make lowercase + if (recoveryMethod == ServerRecoveryMethods.recoveryKey || + recoveryMethod == ServerRecoveryMethods.newDeviceKey) { + token = tokenField.state.value.trim().toLowerCase(); + } else { + token = tokenField.state.value.trim(); + } + + installationCubit.tryToRecover(token, recoveryMethod); } final ServerInstallationCubit installationCubit; diff --git a/lib/logic/cubit/server_installation/server_installation_cubit.dart b/lib/logic/cubit/server_installation/server_installation_cubit.dart index 44e06862..20a89a52 100644 --- a/lib/logic/cubit/server_installation/server_installation_cubit.dart +++ b/lib/logic/cubit/server_installation/server_installation_cubit.dart @@ -287,6 +287,7 @@ class ServerInstallationCubit extends Cubit { await repository.getRecoveryCapabilities(serverDomain); await repository.saveDomain(serverDomain); + await repository.saveIsRecoveringServer(true); emit(ServerInstallationRecovery( serverDomain: serverDomain, @@ -458,6 +459,7 @@ class ServerInstallationCubit extends Cubit { await repository.saveIsServerResetedFirstTime(true); await repository.saveIsServerResetedSecondTime(true); await repository.saveHasFinalChecked(true); + await repository.saveIsRecoveringServer(false); final mainUser = await repository.getMainUser(); final updatedState = (state as ServerInstallationRecovery).copyWith( backblazeCredential: backblazeCredential, diff --git a/lib/logic/cubit/server_installation/server_installation_repository.dart b/lib/logic/cubit/server_installation/server_installation_repository.dart index a31ef764..f009f6ed 100644 --- a/lib/logic/cubit/server_installation/server_installation_repository.dart +++ b/lib/logic/cubit/server_installation/server_installation_repository.dart @@ -62,7 +62,8 @@ class ServerInstallationRepository { ); } - if (serverDomain != null && serverDomain.provider == DnsProvider.unknown) { + if (box.get(BNames.isRecoveringServer, defaultValue: false) && + serverDomain != null) { return ServerInstallationRecovery( hetznerKey: hetznerToken, cloudFlareKey: cloudflareToken, @@ -601,6 +602,10 @@ class ServerInstallationRepository { await box.put(BNames.rootUser, rootUser); } + Future saveIsRecoveringServer(bool value) async { + await box.put(BNames.isRecoveringServer, value); + } + Future saveHasFinalChecked(bool value) async { await box.put(BNames.hasFinalChecked, value); } diff --git a/lib/ui/components/brand_button/filled_button.dart b/lib/ui/components/brand_button/filled_button.dart index 7bf1b1dd..cc6aeb26 100644 --- a/lib/ui/components/brand_button/filled_button.dart +++ b/lib/ui/components/brand_button/filled_button.dart @@ -16,12 +16,12 @@ class FilledButton extends StatelessWidget { @override Widget build(BuildContext context) { - final ButtonStyle _enabledStyle = ElevatedButton.styleFrom( + final ButtonStyle enabledStyle = ElevatedButton.styleFrom( onPrimary: Theme.of(context).colorScheme.onPrimary, primary: Theme.of(context).colorScheme.primary, ).copyWith(elevation: ButtonStyleButton.allOrNull(0.0)); - final ButtonStyle _disabledStyle = ElevatedButton.styleFrom( + final ButtonStyle disabledStyle = ElevatedButton.styleFrom( onPrimary: Theme.of(context).colorScheme.onSurface.withAlpha(30), primary: Theme.of(context).colorScheme.onSurface.withAlpha(98), ).copyWith(elevation: ButtonStyleButton.allOrNull(0.0)); @@ -33,7 +33,7 @@ class FilledButton extends StatelessWidget { ), child: ElevatedButton( onPressed: onPressed, - style: disabled ? _disabledStyle : _enabledStyle, + style: disabled ? disabledStyle : enabledStyle, child: child ?? Text(title ?? ''), ), ); diff --git a/lib/ui/components/brand_cards/brand_cards.dart b/lib/ui/components/brand_cards/brand_cards.dart index 660054a8..7f19e47d 100644 --- a/lib/ui/components/brand_cards/brand_cards.dart +++ b/lib/ui/components/brand_cards/brand_cards.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:selfprivacy/config/brand_colors.dart'; class BrandCards { static Widget big({required Widget child}) => _BrandCard( @@ -23,7 +22,9 @@ class BrandCards { static Widget outlined({required Widget child}) => _OutlinedCard( child: child, ); - static Widget filled({required Widget child}) => _FilledCard( + static Widget filled({required Widget child, bool tertiary = false}) => + _FilledCard( + tertiary: tertiary, child: child, ); } @@ -80,12 +81,11 @@ class _OutlinedCard extends StatelessWidget { } class _FilledCard extends StatelessWidget { - const _FilledCard({ - Key? key, - required this.child, - }) : super(key: key); + const _FilledCard({Key? key, required this.child, required this.tertiary}) + : super(key: key); final Widget child; + final bool tertiary; @override Widget build(BuildContext context) { return Card( @@ -94,7 +94,9 @@ class _FilledCard extends StatelessWidget { borderRadius: BorderRadius.all(Radius.circular(12)), ), clipBehavior: Clip.antiAlias, - color: Theme.of(context).colorScheme.surfaceVariant, + color: tertiary + ? Theme.of(context).colorScheme.tertiaryContainer + : Theme.of(context).colorScheme.surfaceVariant, child: child, ); } diff --git a/lib/ui/pages/more/more.dart b/lib/ui/pages/more/more.dart index 49b5e840..9c32128a 100644 --- a/lib/ui/pages/more/more.dart +++ b/lib/ui/pages/more/more.dart @@ -1,13 +1,11 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:ionicons/ionicons.dart'; -import 'package:selfprivacy/config/brand_colors.dart'; import 'package:selfprivacy/config/brand_theme.dart'; import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; -import 'package:selfprivacy/ui/components/brand_divider/brand_divider.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_icons/brand_icons.dart'; -import 'package:selfprivacy/ui/components/brand_text/brand_text.dart'; import 'package:selfprivacy/ui/pages/recovery_key/recovery_key.dart'; import 'package:selfprivacy/ui/pages/setup/initializing.dart'; import 'package:selfprivacy/ui/pages/onboarding/onboarding.dart'; @@ -26,6 +24,9 @@ class MorePage extends StatelessWidget { @override Widget build(BuildContext context) { + var isReady = context.watch().state + is ServerInstallationFinished; + return Scaffold( appBar: PreferredSize( preferredSize: const Size.fromHeight(52), @@ -39,52 +40,53 @@ class MorePage extends StatelessWidget { padding: paddingH15V0, child: Column( children: [ - const BrandDivider(), - _NavItem( - title: 'more.configuration_wizard'.tr(), - iconData: BrandIcons.triangle, - goTo: const InitializingPage(), - ), - _NavItem( + if (!isReady) + _MoreMenuItem( + title: 'more.configuration_wizard'.tr(), + iconData: Icons.change_history_outlined, + goTo: const InitializingPage(), + subtitle: 'not_ready_card.in_menu'.tr(), + accent: true, + ), + if (isReady) + _MoreMenuItem( + title: 'more.create_ssh_key'.tr(), + iconData: Ionicons.key_outline, + goTo: SshKeysPage( + user: context.read().state.rootUser, + )), + if (isReady) + _MoreMenuItem( + iconData: Icons.password_outlined, + goTo: const RecoveryKey(), + title: 'recovery_key.key_main_header'.tr(), + ), + _MoreMenuItem( title: 'more.settings.title'.tr(), - iconData: BrandIcons.settings, + iconData: Icons.settings_outlined, goTo: const AppSettingsPage(), ), - _NavItem( + _MoreMenuItem( title: 'more.about_project'.tr(), iconData: BrandIcons.engineer, goTo: const AboutPage(), ), - _NavItem( + _MoreMenuItem( title: 'more.about_app'.tr(), iconData: BrandIcons.fire, goTo: const InfoPage(), ), - _NavItem( - title: 'more.onboarding'.tr(), - iconData: BrandIcons.start, - goTo: const OnboardingPage(nextPage: RootPage()), - ), - _NavItem( + if (!isReady) + _MoreMenuItem( + title: 'more.onboarding'.tr(), + iconData: BrandIcons.start, + goTo: const OnboardingPage(nextPage: RootPage()), + ), + _MoreMenuItem( title: 'more.console'.tr(), iconData: BrandIcons.terminal, goTo: const Console(), ), - _NavItem( - isEnabled: context.read().state - is ServerInstallationFinished, - title: 'more.create_ssh_key'.tr(), - iconData: Ionicons.key_outline, - goTo: SshKeysPage( - user: context.read().state.rootUser, - )), - _NavItem( - isEnabled: context.read().state - is ServerInstallationFinished, - iconData: Icons.password_outlined, - goTo: const RecoveryKey(), - title: 'recovery_key.key_main_header'.tr(), - ) ], ), ) @@ -94,77 +96,53 @@ class MorePage extends StatelessWidget { } } -class _NavItem extends StatelessWidget { - const _NavItem({ - Key? key, - this.isEnabled = true, - required this.iconData, - required this.goTo, - required this.title, - }) : super(key: key); - - final IconData iconData; - final Widget goTo; - final String title; - final bool isEnabled; - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: isEnabled - ? () => Navigator.of(context).push(materialRoute(goTo)) - : null, - child: _MoreMenuItem( - iconData: iconData, - title: title, - isActive: isEnabled, - ), - ); - } -} - class _MoreMenuItem extends StatelessWidget { const _MoreMenuItem({ Key? key, required this.iconData, required this.title, - required this.isActive, + this.subtitle, + this.goTo, + this.accent = false, }) : super(key: key); final IconData iconData; final String title; - final bool isActive; + final Widget? goTo; + final String? subtitle; + final bool accent; @override Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.symmetric(vertical: 24), - decoration: const BoxDecoration( - border: Border( - bottom: BorderSide( - width: 1.0, - color: BrandColors.dividerColor, - ), + final color = accent + ? Theme.of(context).colorScheme.onTertiaryContainer + : Theme.of(context).colorScheme.onSurface; + return BrandCards.filled( + tertiary: accent, + child: ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + onTap: goTo != null + ? () => Navigator.of(context).push(materialRoute(goTo!)) + : null, + leading: Icon( + iconData, + size: 24, + color: color, ), - ), - child: Row( - children: [ - BrandText.body1( - title, - style: TextStyle( - color: isActive ? null : Colors.grey, - ), - ), - const Spacer(), - SizedBox( - width: 56, - child: Icon( - iconData, - size: 20, - color: isActive ? null : Colors.grey, - ), - ), - ], + title: Text( + title, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + color: color, + ), + ), + subtitle: subtitle != null + ? Text( + subtitle!, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: color, + ), + ) + : null, ), ); } diff --git a/lib/ui/pages/recovery_key/recovery_key.dart b/lib/ui/pages/recovery_key/recovery_key.dart index 67280596..81a50067 100644 --- a/lib/ui/pages/recovery_key/recovery_key.dart +++ b/lib/ui/pages/recovery_key/recovery_key.dart @@ -2,6 +2,7 @@ import 'package:cubit_form/cubit_form.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +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'; @@ -88,7 +89,7 @@ class _RecoveryKeyContentState extends State { if (keyStatus.exists && !_isConfigurationVisible) RecoveryKeyInformation(state: keyStatus), if (_isConfigurationVisible || !keyStatus.exists) - RecoveryKeyConfiguration(), + const RecoveryKeyConfiguration(), const SizedBox(height: 16), if (!_isConfigurationVisible && keyStatus.isValid) BrandButton.text( @@ -161,39 +162,42 @@ class RecoveryKeyInformation extends StatelessWidget { @override Widget build(BuildContext context) { - const padding = EdgeInsets.symmetric(vertical: 8.0); - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (state.expiresAt != null) - Padding( - padding: padding, - child: Text( - 'recovery_key.key_valid_until'.tr( - args: [state.expiresAt!.toIso8601String()], + const padding = EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0); + return SizedBox( + width: double.infinity, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (state.expiresAt != null) + Padding( + padding: padding, + child: Text( + 'recovery_key.key_valid_until'.tr( + args: [DateFormat.yMMMMd().format(state.expiresAt!)], + ), ), ), - ), - if (state.usesLeft != null) - Padding( - padding: padding, - child: Text( - 'recovery_key.key_valid_for'.tr( - args: [state.usesLeft!.toString()], + if (state.usesLeft != null) + Padding( + padding: padding, + child: Text( + 'recovery_key.key_valid_for'.tr( + args: [state.usesLeft!.toString()], + ), ), ), - ), - if (state.generatedAt != null) - Padding( - padding: padding, - child: Text( - 'recovery_key.key_creation_date'.tr( - args: [state.generatedAt!.toIso8601String()], + if (state.generatedAt != null) + Padding( + padding: padding, + child: Text( + 'recovery_key.key_creation_date'.tr( + args: [DateFormat.yMMMMd().format(state.generatedAt!)], + ), + textAlign: TextAlign.start, ), - textAlign: TextAlign.start, ), - ), - ], + ], + ), ); } } @@ -218,6 +222,38 @@ class _RecoveryKeyConfigurationState extends State { DateTime _selectedDate = DateTime.now(); bool _isDateSelected = false; + bool _isLoading = false; + + Future _generateRecoveryToken() async { + setState(() { + _isLoading = true; + }); + try { + final token = await context.read().generateRecoveryKey( + numberOfUses: + _isAmountToggled ? int.tryParse(_amountController.text) : null, + expirationDate: _isExpirationToggled ? _selectedDate : null, + ); + if (!mounted) return; + setState(() { + _isLoading = false; + }); + Navigator.of(context).push( + materialRoute( + RecoveryKeyReceiving(recoveryKey: token), // TO DO + ), + ); + } on GenerationError catch (e) { + setState(() { + _isLoading = false; + }); + getIt().showSnackBar( + 'recovery_key.generation_error'.tr(args: [e.message]), + ); + return; + } + } + void _updateErrorStatuses() { final amount = _amountController.text; final expiration = _expirationController.text; @@ -241,8 +277,7 @@ class _RecoveryKeyConfigurationState extends State { } else if (expiration.isEmpty) { _isExpirationError = true; } else { - _isExpirationError = - _selectedDate == null || _selectedDate.isBefore(DateTime.now()); + _isExpirationError = _selectedDate.isBefore(DateTime.now()); } }); @@ -266,10 +301,10 @@ class _RecoveryKeyConfigurationState extends State { value: _isAmountToggled, title: Text('recovery_key.key_amount_toggle'.tr()), activeColor: Theme.of(context).colorScheme.primary, - onChanged: (bool toogled) { + onChanged: (bool toggled) { setState( () { - _isAmountToggled = toogled; + _isAmountToggled = toggled; }, ); _updateErrorStatuses(); @@ -287,7 +322,7 @@ class _RecoveryKeyConfigurationState extends State { enabled: _isAmountToggled, controller: _amountController, decoration: InputDecoration( - border: OutlineInputBorder(), + border: const OutlineInputBorder(), errorText: _isAmountError ? ' ' : null, labelText: 'recovery_key.key_amount_field_title'.tr()), keyboardType: TextInputType.number, @@ -304,10 +339,10 @@ class _RecoveryKeyConfigurationState extends State { value: _isExpirationToggled, title: Text('recovery_key.key_duedate_toggle'.tr()), activeColor: Theme.of(context).colorScheme.primary, - onChanged: (bool toogled) { + onChanged: (bool toggled) { setState( () { - _isExpirationToggled = toogled; + _isExpirationToggled = toggled; }, ); _updateErrorStatuses(); @@ -329,7 +364,7 @@ class _RecoveryKeyConfigurationState extends State { }, readOnly: true, decoration: InputDecoration( - border: OutlineInputBorder(), + border: const OutlineInputBorder(), errorText: _isExpirationError ? ' ' : null, labelText: 'recovery_key.key_duedate_field_title'.tr()), keyboardType: TextInputType.number, @@ -344,15 +379,9 @@ class _RecoveryKeyConfigurationState extends State { const SizedBox(height: 16), FilledButton( title: 'recovery_key.key_receive_button'.tr(), - disabled: _isAmountError || _isExpirationError, + disabled: _isAmountError || _isExpirationError || _isLoading, onPressed: !_isAmountError && !_isExpirationError - ? () { - Navigator.of(context).push( - materialRoute( - const RecoveryKeyReceiving(recoveryKey: ''), // TO DO - ), - ); - } + ? _generateRecoveryToken : null, ), ], diff --git a/lib/ui/pages/recovery_key/recovery_key_receiving.dart b/lib/ui/pages/recovery_key/recovery_key_receiving.dart index edd3f89e..41f65a50 100644 --- a/lib/ui/pages/recovery_key/recovery_key_receiving.dart +++ b/lib/ui/pages/recovery_key/recovery_key_receiving.dart @@ -15,14 +15,31 @@ class RecoveryKeyReceiving extends StatelessWidget { Widget build(BuildContext context) { return BrandHeroScreen( heroTitle: 'recovery_key.key_main_header'.tr(), - heroSubtitle: 'recovering.method_select_description'.tr(), + heroSubtitle: 'recovery_key.key_receiving_description'.tr(), hasBackButton: true, hasFlashButton: false, children: [ - Text(recoveryKey, style: Theme.of(context).textTheme.bodyLarge), + const Divider(), const SizedBox(height: 16), - const Icon(Icons.info_outlined, size: 14), - Text('recovery_key.key_receiving_info'.tr()), + Text( + recoveryKey, + style: Theme.of(context).textTheme.bodyLarge!.copyWith( + fontSize: 24, + fontFamily: 'RobotoMono', + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + const Divider(), + const SizedBox(height: 16), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Icon(Icons.info_outlined, size: 24), + const SizedBox(height: 16), + Text('recovery_key.key_receiving_info'.tr()), + ], + ), const SizedBox(height: 16), FilledButton( title: 'recovery_key.key_receiving_done'.tr(), diff --git a/lib/ui/pages/root_route.dart b/lib/ui/pages/root_route.dart index b14ecd52..9e0fcf67 100644 --- a/lib/ui/pages/root_route.dart +++ b/lib/ui/pages/root_route.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:selfprivacy/logic/api_maps/server.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'; @@ -48,10 +47,11 @@ class _RootPageState extends State with TickerProviderStateMixin { super.dispose(); } - var selfprivacyServer = ServerApi(); - @override Widget build(BuildContext context) { + var isReady = context.watch().state + is ServerInstallationFinished; + return SafeArea( child: Provider( create: (_) => ChangeTab(tabController.animateTo), @@ -68,36 +68,23 @@ class _RootPageState extends State with TickerProviderStateMixin { bottomNavigationBar: BrandTabBar( controller: tabController, ), - floatingActionButton: SizedBox( - height: 104 + 16, - child: Column( - crossAxisAlignment: CrossAxisAlignment.end, - mainAxisAlignment: MainAxisAlignment.end, - children: [ - ScaleTransition( - scale: _animation, - child: FloatingActionButton.small( - heroTag: 'new_user_fab', - onPressed: () { - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (BuildContext context) { - return Padding( - padding: MediaQuery.of(context).viewInsets, - child: NewUser()); - }, - ); - }, - child: const Icon(Icons.person_add_outlined), + 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(), + ], ), - ), - const SizedBox(height: 16), - const BrandFab(), - ], - ), - ), + ) + : null, ), ), ); diff --git a/lib/ui/pages/setup/recovering/recovery_confirm_backblaze.dart b/lib/ui/pages/setup/recovering/recovery_confirm_backblaze.dart index 646984a3..2a9fd8a9 100644 --- a/lib/ui/pages/setup/recovering/recovery_confirm_backblaze.dart +++ b/lib/ui/pages/setup/recovering/recovery_confirm_backblaze.dart @@ -29,21 +29,17 @@ class RecoveryConfirmBackblaze extends StatelessWidget { children: [ CubitFormTextField( formFieldCubit: context.read().keyId, - textAlign: TextAlign.center, - scrollPadding: const EdgeInsets.only(bottom: 70), decoration: const InputDecoration( border: OutlineInputBorder(), - hintText: 'KeyID', + labelText: 'KeyID', ), ), const SizedBox(height: 16), CubitFormTextField( formFieldCubit: context.read().applicationKey, - textAlign: TextAlign.center, - scrollPadding: const EdgeInsets.only(bottom: 70), decoration: const InputDecoration( border: OutlineInputBorder(), - hintText: 'Master Application Key', + labelText: 'Master Application Key', ), ), const SizedBox(height: 16), diff --git a/lib/ui/pages/setup/recovering/recovery_confirm_cloudflare.dart b/lib/ui/pages/setup/recovering/recovery_confirm_cloudflare.dart index 1006e390..28f1a8fc 100644 --- a/lib/ui/pages/setup/recovering/recovery_confirm_cloudflare.dart +++ b/lib/ui/pages/setup/recovering/recovery_confirm_cloudflare.dart @@ -31,11 +31,9 @@ class RecoveryConfirmCloudflare extends StatelessWidget { children: [ CubitFormTextField( formFieldCubit: context.read().apiKey, - textAlign: TextAlign.center, - scrollPadding: const EdgeInsets.only(bottom: 70), decoration: InputDecoration( border: const OutlineInputBorder(), - hintText: 'initializing.5'.tr(), + labelText: 'initializing.5'.tr(), ), ), const SizedBox(height: 16), diff --git a/lib/ui/pages/setup/recovering/recovery_confirm_server.dart b/lib/ui/pages/setup/recovering/recovery_confirm_server.dart index e1b5403a..8242e521 100644 --- a/lib/ui/pages/setup/recovering/recovery_confirm_server.dart +++ b/lib/ui/pages/setup/recovering/recovery_confirm_server.dart @@ -143,7 +143,7 @@ class _RecoveryConfirmServerState extends State { ), ), leading: Icon( - Icons.dns, + Icons.dns_outlined, color: Theme.of(context).colorScheme.onSurface, ), subtitle: Column( @@ -199,10 +199,11 @@ class _RecoveryConfirmServerState extends State { crossAxisAlignment: CrossAxisAlignment.center, children: [ const Icon(Icons.warning_amber_outlined), - const SizedBox(height: 8), + const SizedBox(height: 16), Text( 'recovering.modal_confirmation_title'.tr(), style: Theme.of(context).textTheme.headlineSmall, + textAlign: TextAlign.center, ), ], ), @@ -212,7 +213,9 @@ class _RecoveryConfirmServerState extends State { children: [ Text('recovering.modal_confirmation_description'.tr(), style: Theme.of(context).textTheme.bodyMedium), + const SizedBox(height: 12), const Divider(), + const SizedBox(height: 12), Text( server.name, style: Theme.of(context).textTheme.titleMedium, @@ -275,19 +278,20 @@ class IsValidStringDisplay extends StatelessWidget { ? Icon(Icons.check, color: Theme.of(context).colorScheme.onSurface) : Icon(Icons.close, color: Theme.of(context).colorScheme.error), const SizedBox(width: 8), - isValid - ? Text( - textIfValid, - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - color: Theme.of(context).colorScheme.onSurface, - ), - ) - : Text( - textIfInvalid, - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - color: Theme.of(context).colorScheme.error, - ), - ) + Expanded( + child: isValid + ? Text( + textIfValid, + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: Theme.of(context).colorScheme.onSurface, + ), + ) + : Text( + textIfInvalid, + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: Theme.of(context).colorScheme.error, + ), + )), ], ); } diff --git a/lib/ui/pages/users/add_user_fab.dart b/lib/ui/pages/users/add_user_fab.dart new file mode 100644 index 00000000..c527a60b --- /dev/null +++ b/lib/ui/pages/users/add_user_fab.dart @@ -0,0 +1,25 @@ +part of 'users.dart'; + +class AddUserFab extends StatelessWidget { + const AddUserFab({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return FloatingActionButton.small( + heroTag: 'new_user_fab', + onPressed: () { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (BuildContext context) { + return Padding( + padding: MediaQuery.of(context).viewInsets, + child: const NewUser()); + }, + ); + }, + child: const Icon(Icons.person_add_outlined), + ); + } +} diff --git a/lib/ui/pages/users/new_user.dart b/lib/ui/pages/users/new_user.dart index 77a38918..4dd434b5 100644 --- a/lib/ui/pages/users/new_user.dart +++ b/lib/ui/pages/users/new_user.dart @@ -1,6 +1,8 @@ part of 'users.dart'; class NewUser extends StatelessWidget { + const NewUser({Key? key}) : super(key: key); + @override Widget build(BuildContext context) { var config = context.watch().state; diff --git a/lib/ui/pages/users/users.dart b/lib/ui/pages/users/users.dart index 9276b759..52bb430d 100644 --- a/lib/ui/pages/users/users.dart +++ b/lib/ui/pages/users/users.dart @@ -30,6 +30,7 @@ part 'empty.dart'; part 'new_user.dart'; part 'user.dart'; part 'user_details.dart'; +part 'add_user_fab.dart'; class UsersPage extends StatelessWidget { const UsersPage({Key? key}) : super(key: key); From 7810c2a2796190f204c8b223035c26744314a864 Mon Sep 17 00:00:00 2001 From: Inex Code Date: Tue, 31 May 2022 17:30:35 +0300 Subject: [PATCH 39/52] Fix recovery flow --- .../server_installation_cubit.dart | 3 ++- .../server_installation_repository.dart | 3 ++- lib/ui/pages/recovery_key/recovery_key.dart | 2 +- lib/ui/pages/setup/recovering/recovery_routing.dart | 13 ++++++++++--- 4 files changed, 15 insertions(+), 6 deletions(-) diff --git a/lib/logic/cubit/server_installation/server_installation_cubit.dart b/lib/logic/cubit/server_installation/server_installation_cubit.dart index 20a89a52..18367eb0 100644 --- a/lib/logic/cubit/server_installation/server_installation_cubit.dart +++ b/lib/logic/cubit/server_installation/server_installation_cubit.dart @@ -461,6 +461,7 @@ class ServerInstallationCubit extends Cubit { await repository.saveHasFinalChecked(true); await repository.saveIsRecoveringServer(false); final mainUser = await repository.getMainUser(); + await repository.saveRootUser(mainUser); final updatedState = (state as ServerInstallationRecovery).copyWith( backblazeCredential: backblazeCredential, rootUser: mainUser, @@ -502,7 +503,7 @@ class ServerInstallationCubit extends Cubit { if (state.serverDetails != null) { await repository.deleteServer(state.serverDomain!); } - await repository.deleteRecords(); + await repository.deleteServerRelatedRecords(); emit(ServerInstallationNotFinished( hetznerKey: state.hetznerKey, serverDomain: state.serverDomain, diff --git a/lib/logic/cubit/server_installation/server_installation_repository.dart b/lib/logic/cubit/server_installation/server_installation_repository.dart index f009f6ed..3152c39a 100644 --- a/lib/logic/cubit/server_installation/server_installation_repository.dart +++ b/lib/logic/cubit/server_installation/server_installation_repository.dart @@ -628,7 +628,7 @@ class ServerInstallationRepository { await cloudFlare.removeSimilarRecords(cloudFlareDomain: serverDomain); } - Future deleteRecords() async { + Future deleteServerRelatedRecords() async { await box.deleteAll([ BNames.serverDetails, BNames.isServerStarted, @@ -636,6 +636,7 @@ class ServerInstallationRepository { BNames.isServerResetedSecondTime, BNames.hasFinalChecked, BNames.isLoading, + BNames.isRecoveringServer, ]); getIt().init(); } diff --git a/lib/ui/pages/recovery_key/recovery_key.dart b/lib/ui/pages/recovery_key/recovery_key.dart index 81a50067..d170603b 100644 --- a/lib/ui/pages/recovery_key/recovery_key.dart +++ b/lib/ui/pages/recovery_key/recovery_key.dart @@ -24,7 +24,7 @@ class RecoveryKey extends StatefulWidget { class _RecoveryKeyState extends State { @override Widget build(BuildContext context) { - var keyStatus = context.watch().state; + final keyStatus = context.watch().state; final List widgets; final String? subtitle = diff --git a/lib/ui/pages/setup/recovering/recovery_routing.dart b/lib/ui/pages/setup/recovering/recovery_routing.dart index b43ff7e7..191ee1e6 100644 --- a/lib/ui/pages/setup/recovering/recovery_routing.dart +++ b/lib/ui/pages/setup/recovering/recovery_routing.dart @@ -60,9 +60,16 @@ class RecoveryRouting extends StatelessWidget { } } - return AnimatedSwitcher( - duration: const Duration(milliseconds: 300), - child: currentPage, + return BlocListener( + listener: (context, state) { + if (state is ServerInstallationFinished) { + Navigator.of(context).popUntil((route) => route.isFirst); + } + }, + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: currentPage, + ), ); } } From e8d5ecccf61a8731b33c7a82a4400e54f81f50be Mon Sep 17 00:00:00 2001 From: Inex Code Date: Tue, 31 May 2022 17:30:44 +0300 Subject: [PATCH 40/52] Add devices screen --- assets/translations/en.json | 30 +++++- assets/translations/ru.json | 28 +++++- lib/config/bloc_config.dart | 3 + lib/logic/api_maps/server.dart | 2 +- lib/ui/pages/devices/devices.dart | 143 +++++++++++++++++++++++++++ lib/ui/pages/devices/new_device.dart | 84 ++++++++++++++++ lib/ui/pages/more/more.dart | 7 ++ 7 files changed, 293 insertions(+), 4 deletions(-) create mode 100644 lib/ui/pages/devices/devices.dart create mode 100644 lib/ui/pages/devices/new_device.dart diff --git a/assets/translations/en.json b/assets/translations/en.json index e75e564c..46cee9b8 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -28,7 +28,8 @@ "no_data": "No data", "wait": "Wait", "remove": "Remove", - "apply": "Apply" + "apply": "Apply", + "done": "Done" }, "more": { "_comment": "'More' tab", @@ -286,7 +287,7 @@ "recovery_main_header": "Connect to an existing server", "domain_recovery_description": "Enter a server domain you want to get access for:", "domain_recover_placeholder": "Your domain", - "domain_recover_error": "Server with such domain is not found", + "domain_recover_error": "Server with such domain was not found", "method_select_description": "Select a recovery method:", "method_select_other_device": "I have access on another device", "method_select_recovery_key": "I have a recovery key", @@ -324,6 +325,31 @@ "confirm_backblaze": "Connect to Backblaze", "confirm_backblaze_description": "Enter a Backblaze token with access to backup storage:" }, + "devices": { + "main_screen": { + "header": "Devices", + "description": "These devices have full access to the server via SelfPrivacy app.", + "this_device": "This device", + "other_devices": "Other devices", + "authorize_new_device": "Authorize new device", + "access_granted_on" : "Access granted on {}", + "tip": "Press on the device to revoke access." + }, + "add_new_device_screen": { + "header": "Authorizing new device", + "description": "Enter the key on the device you want to authorize:", + "please_wait": "Please wait", + "tip": "The key is valid for 10 minutes.", + "expired": "The key has expired.", + "get_new_key": "Get new key" + }, + "revoke_device_alert": { + "header": "Revoke access?", + "description": "The device {} will no longer have access to the server.", + "yes": "Revoke", + "no": "Cancel" + } + }, "recovery_key": { "key_connection_error": "Couldn't connect to the server.", "key_synchronizing": "Synchronizing...", diff --git a/assets/translations/ru.json b/assets/translations/ru.json index 042998f4..68665e26 100644 --- a/assets/translations/ru.json +++ b/assets/translations/ru.json @@ -28,7 +28,8 @@ "no_data": "Нет данных", "wait": "Загрузка", "remove": "Удалить", - "apply": "Подать" + "apply": "Подать", + "done": "Готово" }, "more": { "_comment": "вкладка ещё", @@ -322,6 +323,31 @@ "confirm_backblze": "Подключение к Backblaze", "confirm_backblaze_description": "Введите токен Backblaze, который имеет права на хранилище резервных копий:" }, + "devices": { + "main_screen": { + "header": "Устройства", + "description": "Эти устройства имеют полный доступ к управлению сервером через приложение SelfPrivacy.", + "this_device": "Это устройство", + "other_devices": "Другие устройства", + "authorize_new_device": "Авторизовать новое устройство", + "access_granted_on" : "Доступ выдан {}", + "tip": "Нажмите на устройство, чтобы отозвать доступ." + }, + "add_new_device_screen": { + "header": "Авторизация нового устройства", + "description": "Введите этот ключ на новом устройстве:", + "please_wait": "Пожалуйста, подождите", + "tip": "Ключ действителен 10 минут.", + "expired": "Срок действия ключа истёк.", + "get_new_key": "Получить новый ключ" + }, + "revoke_device_alert": { + "header": "Отозвать доступ?", + "description": "Устройство {} больше не сможет управлять сервером.", + "yes": "Отозвать", + "no": "Отмена" + } + }, "recovery_key": { "key_connection_error": "Не удалось соединиться с сервером", "key_synchronizing": "Синхронизация...", diff --git a/lib/config/bloc_config.dart b/lib/config/bloc_config.dart index 222b70bf..9fce0383 100644 --- a/lib/config/bloc_config.dart +++ b/lib/config/bloc_config.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:selfprivacy/logic/cubit/devices/devices_cubit.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/logic/cubit/app_settings/app_settings_cubit.dart'; @@ -24,6 +25,7 @@ class BlocAndProviderConfig extends StatelessWidget { var backupsCubit = BackupsCubit(serverInstallationCubit); var dnsRecordsCubit = DnsRecordsCubit(serverInstallationCubit); var recoveryKeyCubit = RecoveryKeyCubit(serverInstallationCubit); + var apiDevicesCubit = ApiDevicesCubit(serverInstallationCubit); return MultiProvider( providers: [ BlocProvider( @@ -39,6 +41,7 @@ class BlocAndProviderConfig extends StatelessWidget { BlocProvider(create: (_) => backupsCubit..load(), lazy: false), BlocProvider(create: (_) => dnsRecordsCubit..load()), BlocProvider(create: (_) => recoveryKeyCubit..load()), + BlocProvider(create: (_) => apiDevicesCubit..load()), BlocProvider( create: (_) => JobsCubit(usersCubit: usersCubit, servicesCubit: servicesCubit), diff --git a/lib/logic/api_maps/server.dart b/lib/logic/api_maps/server.dart index 35c3c753..71bd03f1 100644 --- a/lib/logic/api_maps/server.dart +++ b/lib/logic/api_maps/server.dart @@ -847,7 +847,7 @@ class ServerApi extends ApiMap { response = await client.delete( '/auth/tokens', data: { - 'device': device, + 'token_name': device, }, ); } on DioError catch (e) { diff --git a/lib/ui/pages/devices/devices.dart b/lib/ui/pages/devices/devices.dart new file mode 100644 index 00000000..eb5b0531 --- /dev/null +++ b/lib/ui/pages/devices/devices.dart @@ -0,0 +1,143 @@ +import 'package:cubit_form/cubit_form.dart'; +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/logic/models/json/api_token.dart'; +import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; +import 'package:selfprivacy/ui/pages/devices/new_device.dart'; +import 'package:selfprivacy/utils/route_transitions/basic.dart'; + +class DevicesScreen extends StatefulWidget { + const DevicesScreen({Key? key}) : super(key: key); + + @override + State createState() => _DevicesScreenState(); +} + +class _DevicesScreenState extends State { + @override + Widget build(BuildContext context) { + final devicesStatus = context.watch().state; + + return RefreshIndicator( + onRefresh: () async { + context.read().refresh(); + }, + child: BrandHeroScreen( + heroTitle: 'devices.main_screen.header'.tr(), + heroSubtitle: 'devices.main_screen.description'.tr(), + hasBackButton: true, + hasFlashButton: false, + children: [ + Text( + 'devices.main_screen.this_device'.tr(), + style: Theme.of(context).textTheme.labelLarge!.copyWith( + color: Theme.of(context).colorScheme.secondary, + ), + ), + _DeviceTile(device: devicesStatus.thisDevice), + const Divider(height: 1), + const SizedBox(height: 16), + Text( + 'devices.main_screen.other_devices'.tr(), + style: Theme.of(context).textTheme.labelLarge!.copyWith( + color: Theme.of(context).colorScheme.secondary, + ), + ), + ...devicesStatus.otherDevices + .map((device) => _DeviceTile(device: device)) + .toList(), + const SizedBox(height: 16), + OutlinedButton( + onPressed: () => Navigator.of(context) + .push(materialRoute(const NewDeviceScreen())), + child: Text('devices.main_screen.authorize_new_device'.tr()), + ), + const SizedBox(height: 16), + const Divider(height: 1), + const SizedBox(height: 16), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon( + Icons.info_outline, + color: Theme.of(context).colorScheme.onBackground, + ), + const SizedBox(height: 16), + Text( + 'devices.main_screen.tip'.tr(), + style: Theme.of(context).textTheme.bodyMedium!, + ), + ], + ), + const SizedBox(height: 24), + ], + ), + ); + } +} + +class _DeviceTile extends StatelessWidget { + const _DeviceTile({Key? key, required this.device}) : super(key: key); + + final ApiToken device; + + @override + Widget build(BuildContext context) { + return ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 0, vertical: 4), + title: Text(device.name), + subtitle: Text('devices.main_screen.access_granted_on' + .tr(args: [DateFormat.yMMMMd().format(device.date)])), + onTap: device.isCaller + ? null + : () => _showConfirmationDialog(context, device), + ); + } + + _showConfirmationDialog(BuildContext context, ApiToken device) => showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Icon(Icons.link_off_outlined), + const SizedBox(height: 16), + Text( + 'devices.revoke_device_alert.header'.tr(), + style: Theme.of(context).textTheme.headlineSmall, + textAlign: TextAlign.center, + ), + ], + ), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'devices.revoke_device_alert.description' + .tr(args: [device.name]), + style: Theme.of(context).textTheme.bodyMedium), + ], + ), + actions: [ + TextButton( + child: Text('devices.revoke_device_alert.no'.tr()), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + TextButton( + child: Text('devices.revoke_device_alert.yes'.tr()), + onPressed: () { + context.read().deleteDevice(device); + Navigator.of(context).pop(); + }, + ), + ], + ); + }, + ); +} diff --git a/lib/ui/pages/devices/new_device.dart b/lib/ui/pages/devices/new_device.dart new file mode 100644 index 00000000..7929b73e --- /dev/null +++ b/lib/ui/pages/devices/new_device.dart @@ -0,0 +1,84 @@ +import 'package:cubit_form/cubit_form.dart'; +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/filled_button.dart'; +import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; + +class NewDeviceScreen extends StatelessWidget { + const NewDeviceScreen({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return BrandHeroScreen( + heroTitle: 'devices.add_new_device_screen.header'.tr(), + heroSubtitle: 'devices.add_new_device_screen.description'.tr(), + hasBackButton: true, + hasFlashButton: false, + children: [ + FutureBuilder( + future: context.read().getNewDeviceKey(), + builder: (context, snapshot) { + if (snapshot.hasData) { + return _KeyDisplay( + newDeviceKey: snapshot.data.toString(), + ); + } else { + return const Center(child: CircularProgressIndicator()); + } + }, + ), + ], + ); + } +} + +class _KeyDisplay extends StatelessWidget { + const _KeyDisplay({Key? key, required this.newDeviceKey}) : super(key: key); + final String newDeviceKey; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Divider(), + const SizedBox(height: 16), + Text( + newDeviceKey, + style: Theme.of(context).textTheme.bodyLarge!.copyWith( + fontSize: 24, + fontFamily: 'RobotoMono', + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + const Divider(), + const SizedBox(height: 16), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon( + Icons.info_outline, + color: Theme.of(context).colorScheme.onBackground, + ), + const SizedBox(height: 16), + Text( + 'devices.add_new_device_screen.tip'.tr(), + style: Theme.of(context).textTheme.bodyMedium!, + ), + ], + ), + const SizedBox(height: 16), + FilledButton( + child: Text( + 'basis.done'.tr(), + ), + onPressed: () => Navigator.of(context).pop(), + ), + const SizedBox(height: 24), + ], + ); + } +} diff --git a/lib/ui/pages/more/more.dart b/lib/ui/pages/more/more.dart index 9c32128a..334d0707 100644 --- a/lib/ui/pages/more/more.dart +++ b/lib/ui/pages/more/more.dart @@ -6,6 +6,7 @@ import 'package:selfprivacy/logic/cubit/server_installation/server_installation_ 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_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/setup/initializing.dart'; import 'package:selfprivacy/ui/pages/onboarding/onboarding.dart'; @@ -61,6 +62,12 @@ class MorePage extends StatelessWidget { goTo: const RecoveryKey(), title: 'recovery_key.key_main_header'.tr(), ), + if (isReady) + _MoreMenuItem( + iconData: Icons.devices_outlined, + goTo: const DevicesScreen(), + title: 'devices.main_screen.header'.tr(), + ), _MoreMenuItem( title: 'more.settings.title'.tr(), iconData: Icons.settings_outlined, From 5909b9a3e6210e941ee375f9249d3c12d8d3e253 Mon Sep 17 00:00:00 2001 From: NaiJi Date: Wed, 1 Jun 2022 17:29:37 +0300 Subject: [PATCH 41/52] Minor UI fixes on recovery key pages --- lib/ui/pages/recovery_key/recovery_key.dart | 15 ++++----------- .../recovery_key/recovery_key_receiving.dart | 5 +---- 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/lib/ui/pages/recovery_key/recovery_key.dart b/lib/ui/pages/recovery_key/recovery_key.dart index d170603b..810f3080 100644 --- a/lib/ui/pages/recovery_key/recovery_key.dart +++ b/lib/ui/pages/recovery_key/recovery_key.dart @@ -27,17 +27,14 @@ class _RecoveryKeyState extends State { final keyStatus = context.watch().state; final List widgets; - final String? subtitle = + String? subtitle = keyStatus.exists ? null : 'recovery_key.key_main_description'.tr(); + switch (keyStatus.loadingStatus) { case LoadingStatus.refreshing: + subtitle = 'recovery_key.key_synchronizing'.tr(); widgets = [ const Center(child: CircularProgressIndicator()), - const SizedBox(height: 16), - BrandText( - 'recovery_key.key_synchronizing'.tr(), - type: TextType.h1, - ), ]; break; case LoadingStatus.success: @@ -47,13 +44,9 @@ class _RecoveryKeyState extends State { break; case LoadingStatus.uninitialized: case LoadingStatus.error: + subtitle = 'recovery_key.key_connection_error'.tr(); widgets = [ const Icon(Icons.sentiment_dissatisfied_outlined), - const SizedBox(height: 16), - BrandText( - 'recovery_key.key_connection_error'.tr(), - type: TextType.h1, - ), ]; break; } diff --git a/lib/ui/pages/recovery_key/recovery_key_receiving.dart b/lib/ui/pages/recovery_key/recovery_key_receiving.dart index 41f65a50..7356864d 100644 --- a/lib/ui/pages/recovery_key/recovery_key_receiving.dart +++ b/lib/ui/pages/recovery_key/recovery_key_receiving.dart @@ -2,8 +2,6 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:selfprivacy/ui/components/brand_button/filled_button.dart'; import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; -import 'package:selfprivacy/ui/pages/root_route.dart'; -import 'package:selfprivacy/utils/route_transitions/basic.dart'; class RecoveryKeyReceiving extends StatelessWidget { const RecoveryKeyReceiving({required this.recoveryKey, Key? key}) @@ -44,8 +42,7 @@ class RecoveryKeyReceiving extends StatelessWidget { FilledButton( title: 'recovery_key.key_receiving_done'.tr(), onPressed: () { - Navigator.of(context) - .pushReplacement(materialRoute(const RootPage())); + Navigator.of(context).popUntil((route) => route.isFirst); }, ), ], From 4db0413c425cf45d81f188b19f0566e0e21193aa Mon Sep 17 00:00:00 2001 From: Inex Code Date: Sun, 5 Jun 2022 22:36:32 +0300 Subject: [PATCH 42/52] Linting --- analysis_options.yaml | 37 ++ lib/config/bloc_config.dart | 42 +- lib/config/bloc_observer.dart | 9 +- lib/config/brand_colors.dart | 24 +- lib/config/brand_theme.dart | 10 +- lib/config/get_it_config.dart | 2 +- lib/config/hive_config.dart | 18 +- lib/config/localization.dart | 10 +- lib/config/text_themes.dart | 32 +- lib/logic/api_maps/api_map.dart | 28 +- lib/logic/api_maps/backblaze.dart | 44 +- lib/logic/api_maps/cloudflare.dart | 140 +++-- lib/logic/api_maps/hetzner.dart | 114 ++-- lib/logic/api_maps/server.dart | 191 +++---- .../authentication_dependend_cubit.dart | 6 +- .../app_settings/app_settings_cubit.dart | 14 +- .../app_settings/app_settings_state.dart | 4 +- lib/logic/cubit/backups/backups_cubit.dart | 56 +- lib/logic/cubit/backups/backups_state.dart | 18 +- lib/logic/cubit/devices/devices_cubit.dart | 24 +- lib/logic/cubit/devices/devices_state.dart | 16 +- .../cubit/dns_records/dns_records_cubit.dart | 48 +- .../cubit/dns_records/dns_records_state.dart | 28 +- .../forms/factories/field_cubit_factory.dart | 24 +- .../initializing/backblaze_form_cubit.dart | 6 +- .../initializing/cloudflare_form_cubit.dart | 8 +- .../setup/initializing/domain_cloudflare.dart | 18 +- .../initializing/hetzner_form_cubit.dart | 8 +- .../initializing/root_user_form_cubit.dart | 6 +- .../recovery_device_form_cubit.dart | 4 +- .../recovery_domain_form_cubit.dart | 10 +- .../cubit/forms/user/ssh_form_cubit.dart | 12 +- .../cubit/forms/user/user_form_cubit.dart | 12 +- .../cubit/forms/validations/validations.dart | 23 +- .../hetzner_metrics_cubit.dart | 12 +- .../hetzner_metrics_repository.dart | 22 +- .../hetzner_metrics_state.dart | 2 + lib/logic/cubit/jobs/jobs_cubit.dart | 42 +- lib/logic/cubit/jobs/jobs_state.dart | 6 +- .../cubit/providers/providers_cubit.dart | 4 +- .../cubit/providers/providers_state.dart | 14 +- .../recovery_key/recovery_key_cubit.dart | 18 +- .../recovery_key/recovery_key_state.dart | 12 +- .../server_detailed_info_cubit.dart | 6 +- .../server_detailed_info_repository.dart | 14 +- .../server_detailed_info_state.dart | 14 +- .../server_installation_cubit.dart | 496 ++++++++++-------- .../server_installation_repository.dart | 250 ++++----- .../server_installation_state.dart | 141 ++--- lib/logic/cubit/services/services_cubit.dart | 6 +- lib/logic/cubit/services/services_state.dart | 38 +- lib/logic/cubit/users/users_cubit.dart | 132 ++--- lib/logic/cubit/users/users_state.dart | 16 +- lib/logic/get_it/api_config.dart | 16 +- lib/logic/get_it/console.dart | 4 +- lib/logic/get_it/navigation.dart | 12 +- lib/logic/models/hive/backblaze_bucket.dart | 6 +- .../models/hive/backblaze_credential.dart | 12 +- lib/logic/models/hive/server_details.dart | 4 +- lib/logic/models/hive/server_domain.dart | 4 +- lib/logic/models/hive/user.dart | 6 +- lib/logic/models/hive/user.g.dart | 12 +- lib/logic/models/job.dart | 13 +- lib/logic/models/json/api_token.dart | 6 +- .../models/json/auto_upgrade_settings.dart | 10 +- lib/logic/models/json/backup.dart | 12 +- lib/logic/models/json/device_token.dart | 6 +- lib/logic/models/json/dns_records.dart | 2 +- .../models/json/hetzner_server_info.dart | 58 +- .../models/json/recovery_token_status.dart | 8 +- .../models/json/server_configurations.dart | 8 +- lib/logic/models/message.dart | 4 +- lib/logic/models/provider.dart | 4 +- lib/logic/models/server_basic_info.dart | 53 +- lib/logic/models/server_status.dart | 12 +- lib/logic/models/timezone_settings.dart | 22 +- lib/main.dart | 36 +- lib/theming/factory/app_theme_factory.dart | 35 +- .../action_button/action_button.dart | 8 +- .../components/brand_alert/brand_alert.dart | 10 +- .../brand_bottom_sheet.dart | 10 +- .../components/brand_button/brand_button.dart | 37 +- .../brand_button/filled_button.dart | 6 +- .../components/brand_cards/brand_cards.dart | 94 ++-- .../brand_divider/brand_divider.dart | 6 +- .../components/brand_header/brand_header.dart | 10 +- .../brand_hero_screen/brand_hero_screen.dart | 12 +- .../components/brand_icons/brand_icons.dart | 2 +- .../components/brand_loader/brand_loader.dart | 6 +- lib/ui/components/brand_md/brand_md.dart | 16 +- .../components/brand_radio/brand_radio.dart | 12 +- .../brand_radio_tile/brand_radio_tile.dart | 8 +- .../brand_span_button/brand_span_button.dart | 16 +- .../components/brand_switch/brand_switch.dart | 8 +- .../brand_tab_bar/brand_tab_bar.dart | 12 +- lib/ui/components/brand_text/brand_text.dart | 180 +++---- .../components/brand_timer/brand_timer.dart | 22 +- .../dots_indicator/dots_indicator.dart | 10 +- lib/ui/components/error/error.dart | 6 +- .../icon_status_mask/icon_status_mask.dart | 8 +- .../components/jobs_content/jobs_content.dart | 25 +- .../not_ready_card/not_ready_card.dart | 8 +- lib/ui/components/one_page/one_page.dart | 10 +- .../components/pre_styled_buttons/close.dart | 6 +- .../components/pre_styled_buttons/flash.dart | 22 +- .../pre_styled_buttons/flash_fab.dart | 41 +- .../pre_styled_buttons.dart | 2 +- .../components/progress_bar/progress_bar.dart | 46 +- .../components/switch_block/switch_bloc.dart | 10 +- lib/ui/helpers/modals.dart | 4 +- .../pages/backup_details/backup_details.dart | 40 +- lib/ui/pages/devices/devices.dart | 26 +- lib/ui/pages/devices/new_device.dart | 14 +- lib/ui/pages/dns_details/dns_details.dart | 26 +- lib/ui/pages/more/about/about.dart | 8 +- .../pages/more/app_settings/app_setting.dart | 58 +- lib/ui/pages/more/console/console.dart | 18 +- lib/ui/pages/more/info/info.dart | 14 +- lib/ui/pages/more/more.dart | 26 +- lib/ui/pages/onboarding/onboarding.dart | 32 +- lib/ui/pages/providers/providers.dart | 32 +- lib/ui/pages/recovery_key/recovery_key.dart | 53 +- .../recovery_key/recovery_key_receiving.dart | 9 +- lib/ui/pages/root_route.dart | 12 +- lib/ui/pages/server_details/chart.dart | 34 +- lib/ui/pages/setup/initializing.dart | 4 +- .../recovering/recover_by_new_device_key.dart | 12 +- .../recovering/recover_by_old_token.dart | 23 +- .../recovering/recover_by_recovery_key.dart | 12 +- .../recovery_confirm_backblaze.dart | 20 +- .../recovery_confirm_cloudflare.dart | 24 +- .../recovering/recovery_confirm_server.dart | 6 +- .../recovery_hentzner_connected.dart | 14 +- lib/utils/extensions/duration.dart | 12 +- lib/utils/extensions/elevation_extension.dart | 22 +- lib/utils/extensions/text_extensions.dart | 32 +- lib/utils/password_generator.dart | 34 +- lib/utils/route_transitions/basic.dart | 10 +- lib/utils/route_transitions/slide_bottom.dart | 22 +- lib/utils/route_transitions/slide_right.dart | 22 +- lib/utils/ui_helpers.dart | 2 +- test/widget_test.dart | 46 +- 142 files changed, 2047 insertions(+), 2001 deletions(-) diff --git a/analysis_options.yaml b/analysis_options.yaml index 11ccd2ae..4aab61a8 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -12,6 +12,7 @@ include: package:flutter_lints/flutter.yaml analyzer: exclude: - lib/generated_plugin_registrant.dart + - lib/**.g.dart linter: # The lint rules applied to this project can be customized in the @@ -28,6 +29,42 @@ linter: 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 + invariant_booleans: 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_specify_types: true + avoid_escaping_inner_quotes: true + avoid_setters_without_getters: true + eol_at_end_of_file: true + prefer_constructors_over_static_methods: true + prefer_expression_function_bodies: true + prefer_final_in_for_each: true + prefer_final_locals: true + prefer_final_parameters: true + prefer_foreach: true + prefer_if_elements_to_conditional_expressions: true + prefer_mixin: true + prefer_null_aware_method_calls: true + require_trailing_commas: true + sized_box_shrink_expand: true + sort_constructors_first: true + unnecessary_await_in_return: true + unnecessary_lambdas: true + unnecessary_null_checks: true + unnecessary_parenthesis: true + use_enums: true + use_if_null_to_convert_nulls_to_bools: true + use_is_even_rather_than_modulo: true + use_late_for_private_fields_and_variables: true + use_named_constants: true + use_setters_to_change_properties: true + use_string_buffers: true + use_super_parameters: true + use_to_and_as_if_applicable: true # Additional information about this file can be found at # https://dart.dev/guides/language/analysis-options diff --git a/lib/config/bloc_config.dart b/lib/config/bloc_config.dart index 9fce0383..f800a4e2 100644 --- a/lib/config/bloc_config.dart +++ b/lib/config/bloc_config.dart @@ -1,3 +1,5 @@ +// ignore_for_file: always_specify_types + import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:selfprivacy/logic/cubit/devices/devices_cubit.dart'; @@ -12,38 +14,38 @@ import 'package:selfprivacy/logic/cubit/services/services_cubit.dart'; import 'package:selfprivacy/logic/cubit/users/users_cubit.dart'; class BlocAndProviderConfig extends StatelessWidget { - const BlocAndProviderConfig({Key? key, this.child}) : super(key: key); + const BlocAndProviderConfig({final super.key, this.child}); final Widget? child; @override - Widget build(BuildContext context) { - var isDark = false; - var serverInstallationCubit = ServerInstallationCubit()..load(); - var usersCubit = UsersCubit(serverInstallationCubit); - var servicesCubit = ServicesCubit(serverInstallationCubit); - var backupsCubit = BackupsCubit(serverInstallationCubit); - var dnsRecordsCubit = DnsRecordsCubit(serverInstallationCubit); - var recoveryKeyCubit = RecoveryKeyCubit(serverInstallationCubit); - var apiDevicesCubit = ApiDevicesCubit(serverInstallationCubit); + Widget build(final BuildContext context) { + const bool isDark = false; + final ServerInstallationCubit serverInstallationCubit = ServerInstallationCubit()..load(); + final UsersCubit usersCubit = UsersCubit(serverInstallationCubit); + final ServicesCubit servicesCubit = ServicesCubit(serverInstallationCubit); + final BackupsCubit backupsCubit = BackupsCubit(serverInstallationCubit); + final DnsRecordsCubit dnsRecordsCubit = DnsRecordsCubit(serverInstallationCubit); + final RecoveryKeyCubit recoveryKeyCubit = RecoveryKeyCubit(serverInstallationCubit); + final ApiDevicesCubit apiDevicesCubit = ApiDevicesCubit(serverInstallationCubit); return MultiProvider( providers: [ BlocProvider( - create: (_) => AppSettingsCubit( + create: (final _) => AppSettingsCubit( isDarkModeOn: isDark, isOnboardingShowing: true, )..load(), ), - BlocProvider(create: (_) => serverInstallationCubit, lazy: false), - BlocProvider(create: (_) => ProvidersCubit()), - BlocProvider(create: (_) => usersCubit..load(), lazy: false), - BlocProvider(create: (_) => servicesCubit..load(), lazy: false), - BlocProvider(create: (_) => backupsCubit..load(), lazy: false), - BlocProvider(create: (_) => dnsRecordsCubit..load()), - BlocProvider(create: (_) => recoveryKeyCubit..load()), - BlocProvider(create: (_) => apiDevicesCubit..load()), + BlocProvider(create: (final _) => serverInstallationCubit, lazy: false), + BlocProvider(create: (final _) => ProvidersCubit()), + BlocProvider(create: (final _) => usersCubit..load(), lazy: false), + BlocProvider(create: (final _) => servicesCubit..load(), lazy: false), + BlocProvider(create: (final _) => backupsCubit..load(), lazy: false), + BlocProvider(create: (final _) => dnsRecordsCubit..load()), + BlocProvider(create: (final _) => recoveryKeyCubit..load()), + BlocProvider(create: (final _) => apiDevicesCubit..load()), BlocProvider( - create: (_) => + create: (final _) => JobsCubit(usersCubit: usersCubit, servicesCubit: servicesCubit), ), ], diff --git a/lib/config/bloc_observer.dart b/lib/config/bloc_observer.dart index c53f5961..56fa5a32 100644 --- a/lib/config/bloc_observer.dart +++ b/lib/config/bloc_observer.dart @@ -1,15 +1,18 @@ +// ignore_for_file: always_specify_types + +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:selfprivacy/ui/components/error/error.dart'; import 'package:selfprivacy/utils/route_transitions/basic.dart'; -import './get_it_config.dart'; +import 'package:selfprivacy/config/get_it_config.dart'; class SimpleBlocObserver extends BlocObserver { SimpleBlocObserver(); @override - void onError(BlocBase bloc, Object error, StackTrace stackTrace) { - final navigator = getIt.get().navigator!; + void onError(final BlocBase bloc, final Object error, final StackTrace stackTrace) { + final NavigatorState navigator = getIt.get().navigator!; navigator.push( materialRoute( diff --git a/lib/config/brand_colors.dart b/lib/config/brand_colors.dart index a06b8895..f6866c2d 100644 --- a/lib/config/brand_colors.dart +++ b/lib/config/brand_colors.dart @@ -1,3 +1,5 @@ +// ignore_for_file: always_specify_types + import 'package:flutter/material.dart'; class BrandColors { @@ -20,8 +22,8 @@ class BrandColors { static const Color green2 = Color(0xFF0F8849); - static get navBackgroundLight => white.withOpacity(0.8); - static get navBackgroundDark => black.withOpacity(0.8); + static Color get navBackgroundLight => white.withOpacity(0.8); + static Color get navBackgroundDark => black.withOpacity(0.8); static const List uninitializedGradientColors = [ Color(0xFF555555), @@ -41,14 +43,14 @@ class BrandColors { Color(0xFFEFD135), ]; - static const primary = blue; - static const headlineColor = black; - static const inactive = gray2; - static const scaffoldBackground = gray3; - static const inputInactive = gray4; + 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 textColor1 = black; - static const textColor2 = gray1; - static const dividerColor = gray5; - static const warning = red1; + 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/brand_theme.dart b/lib/config/brand_theme.dart index 5837748b..9487fe5c 100644 --- a/lib/config/brand_theme.dart +++ b/lib/config/brand_theme.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; import 'package:selfprivacy/config/text_themes.dart'; -import 'brand_colors.dart'; +import 'package:selfprivacy/config/brand_colors.dart'; -final lightTheme = ThemeData( +final ThemeData lightTheme = ThemeData( useMaterial3: true, primaryColor: BrandColors.primary, fontFamily: 'Inter', @@ -52,7 +52,7 @@ final lightTheme = ThemeData( ), ); -var darkTheme = lightTheme.copyWith( +ThemeData darkTheme = lightTheme.copyWith( brightness: Brightness.dark, scaffoldBackgroundColor: const Color(0xFF202120), iconTheme: const IconThemeData(color: BrandColors.gray3), @@ -82,6 +82,6 @@ var darkTheme = lightTheme.copyWith( ), ); -const paddingH15V30 = EdgeInsets.symmetric(horizontal: 15, vertical: 30); +const EdgeInsets paddingH15V30 = EdgeInsets.symmetric(horizontal: 15, vertical: 30); -const paddingH15V0 = EdgeInsets.symmetric(horizontal: 15); +const EdgeInsets paddingH15V0 = EdgeInsets.symmetric(horizontal: 15); diff --git a/lib/config/get_it_config.dart b/lib/config/get_it_config.dart index 1b9fd1f0..6961ea94 100644 --- a/lib/config/get_it_config.dart +++ b/lib/config/get_it_config.dart @@ -9,7 +9,7 @@ export 'package:selfprivacy/logic/get_it/console.dart'; export 'package:selfprivacy/logic/get_it/navigation.dart'; export 'package:selfprivacy/logic/get_it/timer.dart'; -final getIt = GetIt.instance; +final GetIt getIt = GetIt.instance; Future getItSetup() async { getIt.registerSingleton(NavigationService()); diff --git a/lib/config/hive_config.dart b/lib/config/hive_config.dart index e3db5dc7..f4d67c7c 100644 --- a/lib/config/hive_config.dart +++ b/lib/config/hive_config.dart @@ -24,15 +24,15 @@ class HiveConfig { await Hive.openBox(BNames.appSettingsBox); - var cipher = HiveAesCipher( - await getEncryptedKey(BNames.serverInstallationEncryptionKey)); + final HiveAesCipher cipher = HiveAesCipher( + await getEncryptedKey(BNames.serverInstallationEncryptionKey),); await Hive.openBox(BNames.usersDeprecated); await Hive.openBox(BNames.usersBox, encryptionCipher: cipher); - Box deprecatedUsers = Hive.box(BNames.usersDeprecated); + final Box deprecatedUsers = Hive.box(BNames.usersDeprecated); if (deprecatedUsers.isNotEmpty) { - Box users = Hive.box(BNames.usersBox); + final Box users = Hive.box(BNames.usersBox); users.addAll(deprecatedUsers.values.toList()); deprecatedUsers.clear(); } @@ -40,15 +40,15 @@ class HiveConfig { await Hive.openBox(BNames.serverInstallationBox, encryptionCipher: cipher); } - static Future getEncryptedKey(String encKey) async { - const secureStorage = FlutterSecureStorage(); - var hasEncryptionKey = await secureStorage.containsKey(key: encKey); + static Future getEncryptedKey(final String encKey) async { + const FlutterSecureStorage secureStorage = FlutterSecureStorage(); + final bool hasEncryptionKey = await secureStorage.containsKey(key: encKey); if (!hasEncryptionKey) { - var key = Hive.generateSecureKey(); + final List key = Hive.generateSecureKey(); await secureStorage.write(key: encKey, value: base64UrlEncode(key)); } - String? string = await secureStorage.read(key: encKey); + final String? string = await secureStorage.read(key: encKey); return base64Url.decode(string!); } } diff --git a/lib/config/localization.dart b/lib/config/localization.dart index 9fe2dc20..e3f6d8d2 100644 --- a/lib/config/localization.dart +++ b/lib/config/localization.dart @@ -1,16 +1,17 @@ +// ignore_for_file: always_specify_types + import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; class Localization extends StatelessWidget { const Localization({ - Key? key, + final super.key, this.child, - }) : super(key: key); + }); final Widget? child; @override - Widget build(BuildContext context) { - return EasyLocalization( + Widget build(final BuildContext context) => EasyLocalization( supportedLocales: const [Locale('ru'), Locale('en')], path: 'assets/translations', fallbackLocale: const Locale('en'), @@ -18,5 +19,4 @@ class Localization extends StatelessWidget { useOnlyLangCode: true, child: child!, ); - } } diff --git a/lib/config/text_themes.dart b/lib/config/text_themes.dart index ae166980..b7224622 100644 --- a/lib/config/text_themes.dart +++ b/lib/config/text_themes.dart @@ -1,78 +1,78 @@ import 'package:flutter/material.dart'; import 'package:selfprivacy/utils/named_font_weight.dart'; -import 'brand_colors.dart'; +import 'package:selfprivacy/config/brand_colors.dart'; -const defaultTextStyle = TextStyle( +const TextStyle defaultTextStyle = TextStyle( fontSize: 15, color: BrandColors.textColor1, ); -final headline1Style = defaultTextStyle.copyWith( +final TextStyle headline1Style = defaultTextStyle.copyWith( fontSize: 40, fontWeight: NamedFontWeight.extraBold, color: BrandColors.headlineColor, ); -final headline2Style = defaultTextStyle.copyWith( +final TextStyle headline2Style = defaultTextStyle.copyWith( fontSize: 24, fontWeight: NamedFontWeight.extraBold, color: BrandColors.headlineColor, ); -final onboardingTitle = defaultTextStyle.copyWith( +final TextStyle onboardingTitle = defaultTextStyle.copyWith( fontSize: 30, fontWeight: NamedFontWeight.extraBold, color: BrandColors.headlineColor, ); -final headline3Style = defaultTextStyle.copyWith( +final TextStyle headline3Style = defaultTextStyle.copyWith( fontSize: 20, fontWeight: NamedFontWeight.extraBold, color: BrandColors.headlineColor, ); -final headline4Style = defaultTextStyle.copyWith( +final TextStyle headline4Style = defaultTextStyle.copyWith( fontSize: 18, fontWeight: NamedFontWeight.medium, color: BrandColors.headlineColor, ); -final headline4UnderlinedStyle = defaultTextStyle.copyWith( +final TextStyle headline4UnderlinedStyle = defaultTextStyle.copyWith( fontSize: 18, fontWeight: NamedFontWeight.medium, color: BrandColors.headlineColor, decoration: TextDecoration.underline, ); -final headline5Style = defaultTextStyle.copyWith( +final TextStyle headline5Style = defaultTextStyle.copyWith( fontSize: 15, fontWeight: NamedFontWeight.medium, color: BrandColors.headlineColor.withOpacity(0.8), ); -const body1Style = defaultTextStyle; -final body2Style = defaultTextStyle.copyWith( +const TextStyle body1Style = defaultTextStyle; +final TextStyle body2Style = defaultTextStyle.copyWith( color: BrandColors.textColor2, ); -final buttonTitleText = defaultTextStyle.copyWith( +final TextStyle buttonTitleText = defaultTextStyle.copyWith( color: BrandColors.white, fontSize: 16, fontWeight: FontWeight.bold, height: 1, ); -final mediumStyle = defaultTextStyle.copyWith(fontSize: 13, height: 1.53); +final TextStyle mediumStyle = defaultTextStyle.copyWith(fontSize: 13, height: 1.53); -final smallStyle = defaultTextStyle.copyWith(fontSize: 11, height: 1.45); +final TextStyle smallStyle = defaultTextStyle.copyWith(fontSize: 11, height: 1.45); -const progressTextStyleLight = TextStyle( +const TextStyle progressTextStyleLight = TextStyle( fontSize: 11, color: BrandColors.textColor1, height: 1.7, ); -final progressTextStyleDark = progressTextStyleLight.copyWith( +final TextStyle progressTextStyleDark = progressTextStyleLight.copyWith( color: BrandColors.white, ); diff --git a/lib/logic/api_maps/api_map.dart b/lib/logic/api_maps/api_map.dart index a00757fe..ce1a54b2 100644 --- a/lib/logic/api_maps/api_map.dart +++ b/lib/logic/api_maps/api_map.dart @@ -1,3 +1,5 @@ +// ignore_for_file: always_specify_types + import 'dart:async'; import 'dart:developer'; import 'dart:io'; @@ -10,19 +12,19 @@ import 'package:selfprivacy/logic/models/message.dart'; abstract class ApiMap { Future getClient() async { - var dio = Dio(await options); + final Dio dio = Dio(await options); if (hasLogger) { dio.interceptors.add(PrettyDioLogger()); } dio.interceptors.add(ConsoleInterceptor()); (dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = - (HttpClient client) { + (final HttpClient client) { client.badCertificateCallback = - (X509Certificate cert, String host, int port) => true; + (final X509Certificate cert, final String host, final int port) => true; return client; }; - dio.interceptors.add(InterceptorsWrapper(onError: (DioError e, handler) { + dio.interceptors.add(InterceptorsWrapper(onError: (final DioError e, final ErrorInterceptorHandler handler) { print(e.requestOptions.path); print(e.requestOptions.data); @@ -30,7 +32,7 @@ abstract class ApiMap { print(e.response); return handler.next(e); - })); + },),); return dio; } @@ -42,21 +44,21 @@ abstract class ApiMap { ValidateStatus? validateStatus; - void close(Dio client) { + void close(final Dio client) { client.close(); validateStatus = null; } } class ConsoleInterceptor extends InterceptorsWrapper { - void addMessage(Message message) { + void addMessage(final Message message) { getIt.get().addMessage(message); } @override Future onRequest( - RequestOptions options, - RequestInterceptorHandler handler, + final RequestOptions options, + final RequestInterceptorHandler handler, ) async { addMessage( Message( @@ -69,8 +71,8 @@ class ConsoleInterceptor extends InterceptorsWrapper { @override Future onResponse( - Response response, - ResponseInterceptorHandler handler, + final Response response, + final ResponseInterceptorHandler handler, ) async { addMessage( Message( @@ -85,8 +87,8 @@ class ConsoleInterceptor extends InterceptorsWrapper { } @override - Future onError(DioError err, ErrorInterceptorHandler handler) async { - var response = err.response; + Future onError(final DioError err, final ErrorInterceptorHandler handler) async { + final Response? response = err.response; log(err.toString()); addMessage( Message.warn( diff --git a/lib/logic/api_maps/backblaze.dart b/lib/logic/api_maps/backblaze.dart index abf460ea..0957df2d 100644 --- a/lib/logic/api_maps/backblaze.dart +++ b/lib/logic/api_maps/backblaze.dart @@ -1,3 +1,5 @@ +// ignore_for_file: always_specify_types + import 'dart:io'; import 'package:dio/dio.dart'; @@ -14,7 +16,7 @@ class BackblazeApiAuth { class BackblazeApplicationKey { BackblazeApplicationKey( - {required this.applicationKeyId, required this.applicationKey}); + {required this.applicationKeyId, required this.applicationKey,}); final String applicationKeyId; final String applicationKey; @@ -25,10 +27,10 @@ class BackblazeApi extends ApiMap { @override BaseOptions get options { - var options = BaseOptions(baseUrl: rootAddress); + final BaseOptions options = BaseOptions(baseUrl: rootAddress); if (isWithToken) { - var backblazeCredential = getIt().backblazeCredential; - var token = backblazeCredential!.applicationKey; + final BackblazeCredential? backblazeCredential = getIt().backblazeCredential; + final String token = backblazeCredential!.applicationKey; options.headers = {'Authorization': 'Basic $token'}; } @@ -45,14 +47,14 @@ class BackblazeApi extends ApiMap { String apiPrefix = '/b2api/v2'; Future getAuthorizationToken() async { - var client = await getClient(); - var backblazeCredential = getIt().backblazeCredential; + final Dio client = await getClient(); + final BackblazeCredential? backblazeCredential = getIt().backblazeCredential; if (backblazeCredential == null) { throw Exception('Backblaze credential is null'); } final String encodedApiKey = encodedBackblazeKey( - backblazeCredential.keyId, backblazeCredential.applicationKey); - var response = await client.get( + backblazeCredential.keyId, backblazeCredential.applicationKey,); + final Response response = await client.get( 'b2_authorize_account', options: Options(headers: {'Authorization': 'Basic $encodedApiKey'}), ); @@ -65,9 +67,9 @@ class BackblazeApi extends ApiMap { ); } - Future isValid(String encodedApiKey) async { - var client = await getClient(); - Response response = await client.get( + Future isValid(final String encodedApiKey) async { + final Dio client = await getClient(); + final Response response = await client.get( 'b2_authorize_account', options: Options(headers: {'Authorization': 'Basic $encodedApiKey'}), ); @@ -85,12 +87,12 @@ class BackblazeApi extends ApiMap { } // Create bucket - Future createBucket(String bucketName) async { - final auth = await getAuthorizationToken(); - var backblazeCredential = getIt().backblazeCredential; - var client = await getClient(); + Future createBucket(final String bucketName) async { + final BackblazeApiAuth auth = await getAuthorizationToken(); + final BackblazeCredential? backblazeCredential = getIt().backblazeCredential; + final Dio client = await getClient(); client.options.baseUrl = auth.apiUrl; - var response = await client.post( + final Response response = await client.post( '$apiPrefix/b2_create_bucket', data: { 'accountId': backblazeCredential!.keyId, @@ -117,11 +119,11 @@ class BackblazeApi extends ApiMap { } // Create a limited capability key with access to the given bucket - Future createKey(String bucketId) async { - final auth = await getAuthorizationToken(); - var client = await getClient(); + Future createKey(final String bucketId) async { + final BackblazeApiAuth auth = await getAuthorizationToken(); + final Dio client = await getClient(); client.options.baseUrl = auth.apiUrl; - var response = await client.post( + final Response response = await client.post( '$apiPrefix/b2_create_key', data: { 'accountId': getIt().backblazeCredential!.keyId, @@ -137,7 +139,7 @@ class BackblazeApi extends ApiMap { if (response.statusCode == HttpStatus.ok) { return BackblazeApplicationKey( applicationKeyId: response.data['applicationKeyId'], - applicationKey: response.data['applicationKey']); + applicationKey: response.data['applicationKey'],); } else { throw Exception('code: ${response.statusCode}'); } diff --git a/lib/logic/api_maps/cloudflare.dart b/lib/logic/api_maps/cloudflare.dart index 8fc4aa1c..29fb2c46 100644 --- a/lib/logic/api_maps/cloudflare.dart +++ b/lib/logic/api_maps/cloudflare.dart @@ -1,3 +1,5 @@ +// ignore_for_file: always_specify_types + import 'dart:io'; import 'package:dio/dio.dart'; @@ -7,11 +9,17 @@ import 'package:selfprivacy/logic/models/hive/server_domain.dart'; import 'package:selfprivacy/logic/models/json/dns_records.dart'; class DomainNotFoundException implements Exception { - final String message; DomainNotFoundException(this.message); + final String message; } class CloudflareApi extends ApiMap { + + CloudflareApi({ + this.hasLogger = false, + this.isWithToken = true, + this.customToken, + }); @override final bool hasLogger; @override @@ -19,17 +27,11 @@ class CloudflareApi extends ApiMap { final String? customToken; - CloudflareApi({ - this.hasLogger = false, - this.isWithToken = true, - this.customToken, - }); - @override BaseOptions get options { - var options = BaseOptions(baseUrl: rootAddress); + final BaseOptions options = BaseOptions(baseUrl: rootAddress); if (isWithToken) { - var token = getIt().cloudFlareKey; + final String? token = getIt().cloudFlareKey; assert(token != null); options.headers = {'Authorization': 'Bearer $token'}; } @@ -47,14 +49,12 @@ class CloudflareApi extends ApiMap { @override String rootAddress = 'https://api.cloudflare.com/client/v4'; - Future isValid(String token) async { - validateStatus = (status) { - return status == HttpStatus.ok || status == HttpStatus.unauthorized; - }; + Future isValid(final String token) async { + validateStatus = (final status) => status == HttpStatus.ok || status == HttpStatus.unauthorized; - var client = await getClient(); - Response response = await client.get('/user/tokens/verify', - options: Options(headers: {'Authorization': 'Bearer $token'})); + final Dio client = await getClient(); + final Response response = await client.get('/user/tokens/verify', + options: Options(headers: {'Authorization': 'Bearer $token'}),); close(client); @@ -67,12 +67,10 @@ class CloudflareApi extends ApiMap { } } - Future getZoneId(String domain) async { - validateStatus = (status) { - return status == HttpStatus.ok || status == HttpStatus.forbidden; - }; - var client = await getClient(); - Response response = await client.get( + Future getZoneId(final String domain) async { + validateStatus = (final status) => status == HttpStatus.ok || status == HttpStatus.forbidden; + final Dio client = await getClient(); + final Response response = await client.get( '/zones', queryParameters: {'name': domain}, ); @@ -87,21 +85,21 @@ class CloudflareApi extends ApiMap { } Future removeSimilarRecords({ - String? ip4, - required ServerDomain cloudFlareDomain, + required final ServerDomain cloudFlareDomain, + final String? ip4, }) async { - var domainName = cloudFlareDomain.domainName; - var domainZoneId = cloudFlareDomain.zoneId; + final String domainName = cloudFlareDomain.domainName; + final String domainZoneId = cloudFlareDomain.zoneId; - var url = '/zones/$domainZoneId/dns_records'; + final String url = '/zones/$domainZoneId/dns_records'; - var client = await getClient(); - Response response = await client.get(url); + final Dio client = await getClient(); + final Response response = await client.get(url); - List records = response.data['result'] ?? []; - var allDeleteFutures = []; + final List records = response.data['result'] ?? []; + final List allDeleteFutures = []; - for (var record in records) { + for (final record in records) { if (record['zone_name'] == domainName) { allDeleteFutures.add( client.delete('$url/${record["id"]}'), @@ -114,20 +112,20 @@ class CloudflareApi extends ApiMap { } Future> getDnsRecords({ - required ServerDomain cloudFlareDomain, + required final ServerDomain cloudFlareDomain, }) async { - var domainName = cloudFlareDomain.domainName; - var domainZoneId = cloudFlareDomain.zoneId; + final String domainName = cloudFlareDomain.domainName; + final String domainZoneId = cloudFlareDomain.zoneId; - var url = '/zones/$domainZoneId/dns_records'; + final String url = '/zones/$domainZoneId/dns_records'; - var client = await getClient(); - Response response = await client.get(url); + final Dio client = await getClient(); + final Response response = await client.get(url); - List records = response.data['result'] ?? []; - var allRecords = []; + final List records = response.data['result'] ?? []; + final List allRecords = []; - for (var record in records) { + for (final record in records) { if (record['zone_name'] == domainName) { allRecords.add(DnsRecord( name: record['name'], @@ -135,7 +133,7 @@ class CloudflareApi extends ApiMap { content: record['content'], ttl: record['ttl'], proxied: record['proxied'], - )); + ),); } } @@ -144,17 +142,17 @@ class CloudflareApi extends ApiMap { } Future createMultipleDnsRecords({ - String? ip4, - required ServerDomain cloudFlareDomain, + required final ServerDomain cloudFlareDomain, + final String? ip4, }) async { - var domainName = cloudFlareDomain.domainName; - var domainZoneId = cloudFlareDomain.zoneId; - var listDnsRecords = projectDnsRecords(domainName, ip4); - var allCreateFutures = []; + final String domainName = cloudFlareDomain.domainName; + final String domainZoneId = cloudFlareDomain.zoneId; + final List listDnsRecords = projectDnsRecords(domainName, ip4); + final List allCreateFutures = []; - var client = await getClient(); + final Dio client = await getClient(); try { - for (var record in listDnsRecords) { + for (final DnsRecord record in listDnsRecords) { allCreateFutures.add( client.post( '/zones/$domainZoneId/dns_records', @@ -171,26 +169,26 @@ class CloudflareApi extends ApiMap { } } - List projectDnsRecords(String? domainName, String? ip4) { - var domainA = DnsRecord(type: 'A', name: domainName, content: ip4); + List projectDnsRecords(final String? domainName, final String? ip4) { + final DnsRecord domainA = DnsRecord(type: 'A', name: domainName, content: ip4); - var mx = DnsRecord(type: 'MX', name: '@', content: domainName); - var apiA = DnsRecord(type: 'A', name: 'api', content: ip4); - var cloudA = DnsRecord(type: 'A', name: 'cloud', content: ip4); - var gitA = DnsRecord(type: 'A', name: 'git', content: ip4); - var meetA = DnsRecord(type: 'A', name: 'meet', content: ip4); - var passwordA = DnsRecord(type: 'A', name: 'password', content: ip4); - var socialA = DnsRecord(type: 'A', name: 'social', content: ip4); - var vpn = DnsRecord(type: 'A', name: 'vpn', content: ip4); + final DnsRecord mx = DnsRecord(type: 'MX', name: '@', content: domainName); + final DnsRecord apiA = DnsRecord(type: 'A', name: 'api', content: ip4); + final DnsRecord cloudA = DnsRecord(type: 'A', name: 'cloud', content: ip4); + final DnsRecord gitA = DnsRecord(type: 'A', name: 'git', content: ip4); + final DnsRecord meetA = DnsRecord(type: 'A', name: 'meet', content: ip4); + final DnsRecord passwordA = DnsRecord(type: 'A', name: 'password', content: ip4); + final DnsRecord socialA = DnsRecord(type: 'A', name: 'social', content: ip4); + final DnsRecord vpn = DnsRecord(type: 'A', name: 'vpn', content: ip4); - var txt1 = DnsRecord( + final DnsRecord txt1 = DnsRecord( type: 'TXT', name: '_dmarc', content: 'v=DMARC1; p=none', ttl: 18000, ); - var txt2 = DnsRecord( + final DnsRecord txt2 = DnsRecord( type: 'TXT', name: domainName, content: 'v=spf1 a mx ip4:$ip4 -all', @@ -213,18 +211,18 @@ class CloudflareApi extends ApiMap { } Future setDkim( - String dkimRecordString, ServerDomain cloudFlareDomain) async { - final domainZoneId = cloudFlareDomain.zoneId; - final url = '$rootAddress/zones/$domainZoneId/dns_records'; + final String dkimRecordString, final ServerDomain cloudFlareDomain,) async { + final String domainZoneId = cloudFlareDomain.zoneId; + final String url = '$rootAddress/zones/$domainZoneId/dns_records'; - final dkimRecord = DnsRecord( + final DnsRecord dkimRecord = DnsRecord( type: 'TXT', name: 'selector._domainkey', content: dkimRecordString, ttl: 18000, ); - var client = await getClient(); + final Dio client = await getClient(); await client.post( url, data: dkimRecord.toJson(), @@ -234,17 +232,17 @@ class CloudflareApi extends ApiMap { } Future> domainList() async { - var url = '$rootAddress/zones'; - var client = await getClient(); + final String url = '$rootAddress/zones'; + final Dio client = await getClient(); - var response = await client.get( + final Response response = await client.get( url, queryParameters: {'per_page': 50}, ); close(client); return response.data['result'] - .map((el) => el['name'] as String) + .map((final el) => el['name'] as String) .toList(); } } diff --git a/lib/logic/api_maps/hetzner.dart b/lib/logic/api_maps/hetzner.dart index aa1d19b6..3901cb40 100644 --- a/lib/logic/api_maps/hetzner.dart +++ b/lib/logic/api_maps/hetzner.dart @@ -1,3 +1,5 @@ +// ignore_for_file: always_specify_types + import 'dart:convert'; import 'dart:io'; @@ -10,18 +12,18 @@ import 'package:selfprivacy/logic/models/hive/user.dart'; import 'package:selfprivacy/utils/password_generator.dart'; class HetznerApi extends ApiMap { + + HetznerApi({this.hasLogger = false, this.isWithToken = true}); @override bool hasLogger; @override bool isWithToken; - HetznerApi({this.hasLogger = false, this.isWithToken = true}); - @override BaseOptions get options { - var options = BaseOptions(baseUrl: rootAddress); + final BaseOptions options = BaseOptions(baseUrl: rootAddress); if (isWithToken) { - var token = getIt().hetznerKey; + final String? token = getIt().hetznerKey; assert(token != null); options.headers = {'Authorization': 'Bearer $token'}; } @@ -36,12 +38,10 @@ class HetznerApi extends ApiMap { @override String rootAddress = 'https://api.hetzner.cloud/v1'; - Future isValid(String token) async { - validateStatus = (status) { - return status == HttpStatus.ok || status == HttpStatus.unauthorized; - }; - var client = await getClient(); - Response response = await client.get( + Future isValid(final String token) async { + validateStatus = (final int? status) => status == HttpStatus.ok || status == HttpStatus.unauthorized; + final Dio client = await getClient(); + final Response response = await client.get( '/servers', options: Options( headers: {'Authorization': 'Bearer $token'}, @@ -59,8 +59,8 @@ class HetznerApi extends ApiMap { } Future createVolume() async { - var client = await getClient(); - Response dbCreateResponse = await client.post( + final Dio client = await getClient(); + final Response dbCreateResponse = await client.post( '/volumes', data: { 'size': 10, @@ -71,7 +71,7 @@ class HetznerApi extends ApiMap { 'format': 'ext4' }, ); - var dbId = dbCreateResponse.data['volume']['id']; + final dbId = dbCreateResponse.data['volume']['id']; return ServerVolume( id: dbId, name: dbCreateResponse.data['volume']['name'], @@ -79,21 +79,21 @@ class HetznerApi extends ApiMap { } Future createServer({ - required String cloudFlareKey, - required User rootUser, - required String domainName, - required ServerVolume dataBase, + required final String cloudFlareKey, + required final User rootUser, + required final String domainName, + required final ServerVolume dataBase, }) async { - var client = await getClient(); + final Dio client = await getClient(); - var dbPassword = StringGenerators.dbPassword(); - var dbId = dataBase.id; + final String dbPassword = StringGenerators.dbPassword(); + final int dbId = dataBase.id; - final apiToken = StringGenerators.apiToken(); + final String apiToken = StringGenerators.apiToken(); - final hostname = getHostnameFromDomain(domainName); + final String hostname = getHostnameFromDomain(domainName); - final base64Password = + final String base64Password = base64.encode(utf8.encode(rootUser.password ?? 'PASS')); print('hostname: $hostname'); @@ -101,11 +101,11 @@ class HetznerApi extends ApiMap { /// add ssh key when you need it: e.g. "ssh_keys":["kherel"] /// check the branch name, it could be "development" or "master". /// - final userdataString = + final String userdataString = "#cloud-config\nruncmd:\n- curl https://git.selfprivacy.org/SelfPrivacy/selfprivacy-nixos-infect/raw/branch/master/nixos-infect | PROVIDER=hetzner NIX_CHANNEL=nixos-21.05 DOMAIN='$domainName' LUSER='${rootUser.login}' ENCODED_PASSWORD='$base64Password' CF_TOKEN=$cloudFlareKey DB_PASSWORD=$dbPassword API_TOKEN=$apiToken HOSTNAME=$hostname bash 2>&1 | tee /tmp/infect.log"; print(userdataString); - final data = { + final Map data = { 'name': hostname, 'server_type': 'cx11', 'start_after_create': false, @@ -119,7 +119,7 @@ class HetznerApi extends ApiMap { }; print('Decoded data: $data'); - Response serverCreateResponse = await client.post( + final Response serverCreateResponse = await client.post( '/servers', data: data, ); @@ -136,9 +136,9 @@ class HetznerApi extends ApiMap { ); } - static String getHostnameFromDomain(String domain) { + static String getHostnameFromDomain(final String domain) { // Replace all non-alphanumeric characters with an underscore - var hostname = + String hostname = domain.split('.')[0].replaceAll(RegExp(r'[^a-zA-Z0-9]'), '-'); if (hostname.endsWith('-')) { hostname = hostname.substring(0, hostname.length - 1); @@ -154,24 +154,24 @@ class HetznerApi extends ApiMap { } Future deleteSelfprivacyServerAndAllVolumes({ - required String domainName, + required final String domainName, }) async { - var client = await getClient(); + final Dio client = await getClient(); - final hostname = getHostnameFromDomain(domainName); + final String hostname = getHostnameFromDomain(domainName); - Response serversReponse = await client.get('/servers'); - List servers = serversReponse.data['servers']; - Map server = servers.firstWhere((el) => el['name'] == hostname); - List volumes = server['volumes']; - var laterFutures = []; + final Response serversReponse = await client.get('/servers'); + final List servers = serversReponse.data['servers']; + final Map server = servers.firstWhere((final el) => el['name'] == hostname); + final List volumes = server['volumes']; + final List laterFutures = []; - for (var volumeId in volumes) { + for (final volumeId in volumes) { await client.post('/volumes/$volumeId/actions/detach'); } await Future.delayed(const Duration(seconds: 10)); - for (var volumeId in volumes) { + for (final volumeId in volumes) { laterFutures.add(client.delete('/volumes/$volumeId')); } laterFutures.add(client.delete('/servers/${server['id']}')); @@ -181,9 +181,9 @@ class HetznerApi extends ApiMap { } Future reset() async { - var server = getIt().serverDetails!; + final ServerHostingDetails server = getIt().serverDetails!; - var client = await getClient(); + final Dio client = await getClient(); await client.post('/servers/${server.id}/actions/reset'); close(client); @@ -191,9 +191,9 @@ class HetznerApi extends ApiMap { } Future powerOn() async { - var server = getIt().serverDetails!; + final ServerHostingDetails server = getIt().serverDetails!; - var client = await getClient(); + final Dio client = await getClient(); await client.post('/servers/${server.id}/actions/poweron'); close(client); @@ -201,16 +201,16 @@ class HetznerApi extends ApiMap { } Future> getMetrics( - DateTime start, DateTime end, String type) async { - var hetznerServer = getIt().serverDetails; - var client = await getClient(); + final DateTime start, final DateTime end, final String type,) async { + final ServerHostingDetails? hetznerServer = getIt().serverDetails; + final Dio client = await getClient(); - Map queryParameters = { + final Map queryParameters = { 'start': start.toUtc().toIso8601String(), 'end': end.toUtc().toIso8601String(), 'type': type }; - var res = await client.get( + final Response res = await client.get( '/servers/${hetznerServer!.id}/metrics', queryParameters: queryParameters, ); @@ -219,30 +219,30 @@ class HetznerApi extends ApiMap { } Future getInfo() async { - var hetznerServer = getIt().serverDetails; - var client = await getClient(); - Response response = await client.get('/servers/${hetznerServer!.id}'); + final ServerHostingDetails? hetznerServer = getIt().serverDetails; + final Dio client = await getClient(); + final Response response = await client.get('/servers/${hetznerServer!.id}'); close(client); return HetznerServerInfo.fromJson(response.data!['server']); } Future> getServers() async { - var client = await getClient(); - Response response = await client.get('/servers'); + final Dio client = await getClient(); + final Response response = await client.get('/servers'); close(client); return (response.data!['servers'] as List) - .map((e) => HetznerServerInfo.fromJson(e)) + .map((final e) => HetznerServerInfo.fromJson(e)) .toList(); } Future createReverseDns({ - required String ip4, - required String domainName, + required final String ip4, + required final String domainName, }) async { - var hetznerServer = getIt().serverDetails; - var client = await getClient(); + final ServerHostingDetails? hetznerServer = getIt().serverDetails; + final Dio client = await getClient(); await client.post( '/servers/${hetznerServer!.id}/actions/change_dns_ptr', data: { diff --git a/lib/logic/api_maps/server.dart b/lib/logic/api_maps/server.dart index 71bd03f1..522ee528 100644 --- a/lib/logic/api_maps/server.dart +++ b/lib/logic/api_maps/server.dart @@ -1,3 +1,5 @@ +// ignore_for_file: always_specify_types + import 'dart:async'; import 'dart:convert'; import 'dart:io'; @@ -6,6 +8,7 @@ import 'package:dio/dio.dart'; import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/logic/common_enum/common_enum.dart'; import 'package:selfprivacy/logic/models/hive/backblaze_bucket.dart'; +import 'package:selfprivacy/logic/models/hive/server_domain.dart'; import 'package:selfprivacy/logic/models/hive/user.dart'; import 'package:selfprivacy/logic/models/json/api_token.dart'; import 'package:selfprivacy/logic/models/json/auto_upgrade_settings.dart'; @@ -14,23 +17,29 @@ import 'package:selfprivacy/logic/models/json/device_token.dart'; import 'package:selfprivacy/logic/models/json/recovery_token_status.dart'; import 'package:selfprivacy/logic/models/timezone_settings.dart'; -import 'api_map.dart'; +import 'package:selfprivacy/logic/api_maps/api_map.dart'; class ApiResponse { + + ApiResponse({ + required this.statusCode, + required this.data, + this.errorMessage, + }); final int statusCode; final String? errorMessage; final D data; bool get isSuccess => statusCode >= 200 && statusCode < 300; - - ApiResponse({ - required this.statusCode, - this.errorMessage, - required this.data, - }); } class ServerApi extends ApiMap { + + ServerApi( + {this.hasLogger = false, + this.isWithToken = true, + this.overrideDomain, + this.customToken,}); @override bool hasLogger; @override @@ -38,24 +47,18 @@ class ServerApi extends ApiMap { String? overrideDomain; String? customToken; - ServerApi( - {this.hasLogger = false, - this.isWithToken = true, - this.overrideDomain, - this.customToken}); - @override BaseOptions get options { - var options = BaseOptions(); + BaseOptions options = BaseOptions(); if (isWithToken) { - var cloudFlareDomain = getIt().serverDomain; - var domainName = cloudFlareDomain!.domainName; - var apiToken = getIt().serverDetails?.apiToken; + final ServerDomain? cloudFlareDomain = getIt().serverDomain; + final String domainName = cloudFlareDomain!.domainName; + final String? apiToken = getIt().serverDetails?.apiToken; options = BaseOptions(baseUrl: 'https://api.$domainName', headers: { 'Authorization': 'Bearer $apiToken', - }); + },); } if (overrideDomain != null) { @@ -73,7 +76,7 @@ class ServerApi extends ApiMap { Future getApiVersion() async { Response response; - var client = await getClient(); + final Dio client = await getClient(); String? apiVersion; try { @@ -91,7 +94,7 @@ class ServerApi extends ApiMap { bool res = false; Response response; - var client = await getClient(); + final Dio client = await getClient(); try { response = await client.get('/services/status'); res = response.statusCode == HttpStatus.ok; @@ -103,10 +106,10 @@ class ServerApi extends ApiMap { return res; } - Future> createUser(User user) async { + Future> createUser(final User user) async { Response response; - var client = await getClient(); + final Dio client = await getClient(); try { response = await client.post( '/users', @@ -154,15 +157,15 @@ class ServerApi extends ApiMap { ); } - Future>> getUsersList({withMainUser = false}) async { - List res = []; + Future>> getUsersList({final withMainUser = false}) async { + final List res = []; Response response; - var client = await getClient(); + final Dio client = await getClient(); try { response = await client.get('/users', - queryParameters: withMainUser ? {'withMainUser': 'true'} : null); - for (var user in response.data) { + queryParameters: withMainUser ? {'withMainUser': 'true'} : null,); + for (final user in response.data) { res.add(user.toString()); } } on DioError catch (e) { @@ -191,10 +194,10 @@ class ServerApi extends ApiMap { ); } - Future> addUserSshKey(User user, String sshKey) async { + Future> addUserSshKey(final User user, final String sshKey) async { late Response response; - var client = await getClient(); + final Dio client = await getClient(); try { response = await client.post( '/services/ssh/keys/${user.login}', @@ -221,10 +224,10 @@ class ServerApi extends ApiMap { ); } - Future> addRootSshKey(String ssh) async { + Future> addRootSshKey(final String ssh) async { late Response response; - var client = await getClient(); + final Dio client = await getClient(); try { response = await client.put( '/services/ssh/key/send', @@ -249,14 +252,14 @@ class ServerApi extends ApiMap { ); } - Future>> getUserSshKeys(User user) async { + Future>> getUserSshKeys(final User user) async { List res; Response response; - var client = await getClient(); + final Dio client = await getClient(); try { response = await client.get('/services/ssh/keys/${user.login}'); - res = (response.data as List).map((e) => e as String).toList(); + res = (response.data as List).map((final e) => e as String).toList(); } on DioError catch (e) { print(e.message); return ApiResponse>( @@ -287,10 +290,10 @@ class ServerApi extends ApiMap { ); } - Future> deleteUserSshKey(User user, String sshKey) async { + Future> deleteUserSshKey(final User user, final String sshKey) async { Response response; - var client = await getClient(); + final Dio client = await getClient(); try { response = await client.delete( '/services/ssh/keys/${user.login}', @@ -318,11 +321,11 @@ class ServerApi extends ApiMap { ); } - Future deleteUser(User user) async { + Future deleteUser(final User user) async { bool res = false; Response response; - var client = await getClient(); + final Dio client = await getClient(); try { response = await client.delete('/users/${user.login}'); res = response.statusCode == HttpStatus.ok || @@ -344,7 +347,7 @@ class ServerApi extends ApiMap { bool res = false; Response response; - var client = await getClient(); + final Dio client = await getClient(); try { response = await client.get('/system/configuration/apply'); res = response.statusCode == HttpStatus.ok; @@ -357,8 +360,8 @@ class ServerApi extends ApiMap { return res; } - Future switchService(ServiceTypes type, bool needToTurnOn) async { - var client = await getClient(); + Future switchService(final ServiceTypes type, final bool needToTurnOn) async { + final Dio client = await getClient(); try { client.post( '/services/${type.url}/${needToTurnOn ? 'enable' : 'disable'}', @@ -373,7 +376,7 @@ class ServerApi extends ApiMap { Future> servicesPowerCheck() async { Response response; - var client = await getClient(); + final Dio client = await getClient(); try { response = await client.get('/services/status'); } on DioError catch (e) { @@ -392,8 +395,8 @@ class ServerApi extends ApiMap { }; } - Future uploadBackblazeConfig(BackblazeBucket bucket) async { - var client = await getClient(); + Future uploadBackblazeConfig(final BackblazeBucket bucket) async { + final Dio client = await getClient(); try { client.put( '/services/restic/backblaze/config', @@ -411,7 +414,7 @@ class ServerApi extends ApiMap { } Future startBackup() async { - var client = await getClient(); + final Dio client = await getClient(); try { client.put('/services/restic/backup/create'); } on DioError catch (e) { @@ -425,10 +428,10 @@ class ServerApi extends ApiMap { Response response; List backups = []; - var client = await getClient(); + final Dio client = await getClient(); try { response = await client.get('/services/restic/backup/list'); - backups = response.data.map((e) => Backup.fromJson(e)).toList(); + backups = response.data.map((final e) => Backup.fromJson(e)).toList(); } on DioError catch (e) { print(e.message); } catch (e) { @@ -447,7 +450,7 @@ class ServerApi extends ApiMap { progress: 0, ); - var client = await getClient(); + final Dio client = await getClient(); try { response = await client.get('/services/restic/backup/status'); status = BackupStatus.fromJson(response.data); @@ -460,7 +463,7 @@ class ServerApi extends ApiMap { } Future forceBackupListReload() async { - var client = await getClient(); + final Dio client = await getClient(); try { client.get('/services/restic/backup/reload'); } on DioError catch (e) { @@ -470,8 +473,8 @@ class ServerApi extends ApiMap { } } - Future restoreBackup(String backupId) async { - var client = await getClient(); + Future restoreBackup(final String backupId) async { + final Dio client = await getClient(); try { client.put( '/services/restic/backup/restore', @@ -488,7 +491,7 @@ class ServerApi extends ApiMap { Response response; bool result = false; - var client = await getClient(); + final Dio client = await getClient(); try { response = await client.get('/system/configuration/pull'); result = (response.statusCode != null) @@ -506,7 +509,7 @@ class ServerApi extends ApiMap { Response response; bool result = false; - var client = await getClient(); + final Dio client = await getClient(); try { response = await client.get('/system/reboot'); result = (response.statusCode != null) @@ -524,7 +527,7 @@ class ServerApi extends ApiMap { Response response; bool result = false; - var client = await getClient(); + final Dio client = await getClient(); try { response = await client.get('/system/configuration/upgrade'); result = (response.statusCode != null) @@ -545,7 +548,7 @@ class ServerApi extends ApiMap { allowReboot: false, ); - var client = await getClient(); + final Dio client = await getClient(); try { response = await client.get('/system/configuration/autoUpgrade'); if (response.data != null) { @@ -559,8 +562,8 @@ class ServerApi extends ApiMap { return settings; } - Future updateAutoUpgradeSettings(AutoUpgradeSettings settings) async { - var client = await getClient(); + Future updateAutoUpgradeSettings(final AutoUpgradeSettings settings) async { + final Dio client = await getClient(); try { await client.put( '/system/configuration/autoUpgrade', @@ -575,15 +578,15 @@ class ServerApi extends ApiMap { Future getServerTimezone() async { // I am not sure how to initialize TimeZoneSettings with default value... - var client = await getClient(); - Response response = await client.get('/system/configuration/timezone'); + final Dio client = await getClient(); + final Response response = await client.get('/system/configuration/timezone'); close(client); return TimeZoneSettings.fromString(response.data); } - Future updateServerTimezone(TimeZoneSettings settings) async { - var client = await getClient(); + Future updateServerTimezone(final TimeZoneSettings settings) async { + final Dio client = await getClient(); try { await client.put( '/system/configuration/timezone', @@ -599,7 +602,7 @@ class ServerApi extends ApiMap { Future getDkim() async { Response response; - var client = await getClient(); + final Dio client = await getClient(); try { response = await client.get('/services/mailserver/dkim'); } on DioError catch (e) { @@ -621,7 +624,7 @@ class ServerApi extends ApiMap { return ''; } - final base64toString = utf8.fuse(base64); + final Codec base64toString = utf8.fuse(base64); return base64toString .decode(response.data) @@ -633,7 +636,7 @@ class ServerApi extends ApiMap { Future> getRecoveryTokenStatus() async { Response response; - var client = await getClient(); + final Dio client = await getClient(); try { response = await client.get('/auth/recovery_token'); } on DioError catch (e) { @@ -641,7 +644,7 @@ class ServerApi extends ApiMap { return ApiResponse( errorMessage: e.message, statusCode: e.response?.statusCode ?? HttpStatus.internalServerError, - data: const RecoveryKeyStatus(exists: false, valid: false)); + data: const RecoveryKeyStatus(exists: false, valid: false),); } finally { close(client); } @@ -652,17 +655,17 @@ class ServerApi extends ApiMap { statusCode: code, data: response.data != null ? RecoveryKeyStatus.fromJson(response.data) - : null); + : null,); } Future> generateRecoveryToken( - DateTime? expiration, - int? uses, + final DateTime? expiration, + final int? uses, ) async { Response response; - var client = await getClient(); - var data = {}; + final Dio client = await getClient(); + final Map data = {}; if (expiration != null) { data['expiration'] = '${expiration.toIso8601String()}Z'; print(data['expiration']); @@ -680,7 +683,7 @@ class ServerApi extends ApiMap { return ApiResponse( errorMessage: e.message, statusCode: e.response?.statusCode ?? HttpStatus.internalServerError, - data: ''); + data: '',); } finally { close(client); } @@ -689,13 +692,13 @@ class ServerApi extends ApiMap { return ApiResponse( statusCode: code, - data: response.data != null ? response.data['token'] : ''); + data: response.data != null ? response.data['token'] : '',); } - Future> useRecoveryToken(DeviceToken token) async { + Future> useRecoveryToken(final DeviceToken token) async { Response response; - var client = await getClient(); + final Dio client = await getClient(); try { response = await client.post( '/auth/recovery_token/use', @@ -709,7 +712,7 @@ class ServerApi extends ApiMap { return ApiResponse( errorMessage: e.message, statusCode: e.response?.statusCode ?? HttpStatus.internalServerError, - data: ''); + data: '',); } finally { client.close(); } @@ -718,13 +721,13 @@ class ServerApi extends ApiMap { return ApiResponse( statusCode: code, - data: response.data != null ? response.data['token'] : ''); + data: response.data != null ? response.data['token'] : '',); } - Future> authorizeDevice(DeviceToken token) async { + Future> authorizeDevice(final DeviceToken token) async { Response response; - var client = await getClient(); + final Dio client = await getClient(); try { response = await client.post( '/auth/new_device/authorize', @@ -738,7 +741,7 @@ class ServerApi extends ApiMap { return ApiResponse( errorMessage: e.message, statusCode: e.response?.statusCode ?? HttpStatus.internalServerError, - data: ''); + data: '',); } finally { client.close(); } @@ -751,7 +754,7 @@ class ServerApi extends ApiMap { Future> createDeviceToken() async { Response response; - var client = await getClient(); + final Dio client = await getClient(); try { response = await client.post('/auth/new_device'); } on DioError catch (e) { @@ -759,7 +762,7 @@ class ServerApi extends ApiMap { return ApiResponse( errorMessage: e.message, statusCode: e.response?.statusCode ?? HttpStatus.internalServerError, - data: ''); + data: '',); } finally { client.close(); } @@ -768,13 +771,13 @@ class ServerApi extends ApiMap { return ApiResponse( statusCode: code, - data: response.data != null ? response.data['token'] : ''); + data: response.data != null ? response.data['token'] : '',); } Future> deleteDeviceToken() async { Response response; - var client = await getClient(); + final Dio client = await getClient(); try { response = await client.delete('/auth/new_device'); } on DioError catch (e) { @@ -782,7 +785,7 @@ class ServerApi extends ApiMap { return ApiResponse( errorMessage: e.message, statusCode: e.response?.statusCode ?? HttpStatus.internalServerError, - data: ''); + data: '',); } finally { client.close(); } @@ -795,7 +798,7 @@ class ServerApi extends ApiMap { Future>> getApiTokens() async { Response response; - var client = await getClient(); + final Dio client = await getClient(); try { response = await client.get('/auth/tokens'); } on DioError catch (e) { @@ -803,7 +806,7 @@ class ServerApi extends ApiMap { return ApiResponse( errorMessage: e.message, statusCode: e.response?.statusCode ?? HttpStatus.internalServerError, - data: []); + data: [],); } finally { client.close(); } @@ -813,14 +816,14 @@ class ServerApi extends ApiMap { return ApiResponse( statusCode: code, data: (response.data != null) - ? response.data.map((e) => ApiToken.fromJson(e)).toList() - : []); + ? response.data.map((final e) => ApiToken.fromJson(e)).toList() + : [],); } Future> refreshCurrentApiToken() async { Response response; - var client = await getClient(); + final Dio client = await getClient(); try { response = await client.post('/auth/tokens'); } on DioError catch (e) { @@ -828,7 +831,7 @@ class ServerApi extends ApiMap { return ApiResponse( errorMessage: e.message, statusCode: e.response?.statusCode ?? HttpStatus.internalServerError, - data: ''); + data: '',); } finally { client.close(); } @@ -837,12 +840,12 @@ class ServerApi extends ApiMap { return ApiResponse( statusCode: code, - data: response.data != null ? response.data['token'] : ''); + data: response.data != null ? response.data['token'] : '',); } - Future> deleteApiToken(String device) async { + Future> deleteApiToken(final String device) async { Response response; - var client = await getClient(); + final Dio client = await getClient(); try { response = await client.delete( '/auth/tokens', @@ -855,7 +858,7 @@ class ServerApi extends ApiMap { return ApiResponse( errorMessage: e.message, statusCode: e.response?.statusCode ?? HttpStatus.internalServerError, - data: null); + data: null,); } finally { client.close(); } diff --git a/lib/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart b/lib/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart index 080fd684..68705b5b 100644 --- a/lib/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart +++ b/lib/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart @@ -1,3 +1,5 @@ +// ignore_for_file: always_specify_types + import 'dart:async'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:equatable/equatable.dart'; @@ -11,14 +13,14 @@ abstract class ServerInstallationDependendCubit< T extends ServerInstallationDependendState> extends Cubit { ServerInstallationDependendCubit( this.serverInstallationCubit, - T initState, + final T initState, ) : super(initState) { authCubitSubscription = serverInstallationCubit.stream.listen(checkAuthStatus); checkAuthStatus(serverInstallationCubit.state); } - void checkAuthStatus(ServerInstallationState state) { + void checkAuthStatus(final ServerInstallationState state) { if (state is ServerInstallationFinished) { load(); } else if (state is ServerInstallationEmpty) { diff --git a/lib/logic/cubit/app_settings/app_settings_cubit.dart b/lib/logic/cubit/app_settings/app_settings_cubit.dart index bafc1d96..7c2d55e3 100644 --- a/lib/logic/cubit/app_settings/app_settings_cubit.dart +++ b/lib/logic/cubit/app_settings/app_settings_cubit.dart @@ -1,3 +1,5 @@ +// ignore_for_file: always_specify_types + import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:hive/hive.dart'; @@ -9,8 +11,8 @@ part 'app_settings_state.dart'; class AppSettingsCubit extends Cubit { AppSettingsCubit({ - required bool isDarkModeOn, - required bool isOnboardingShowing, + required final bool isDarkModeOn, + required final bool isOnboardingShowing, }) : super( AppSettingsState( isDarkModeOn: isDarkModeOn, @@ -21,15 +23,15 @@ class AppSettingsCubit extends Cubit { Box box = Hive.box(BNames.appSettingsBox); void load() { - bool? isDarkModeOn = box.get(BNames.isDarkModeOn); - bool? isOnboardingShowing = box.get(BNames.isOnboardingShowing); + final bool? isDarkModeOn = box.get(BNames.isDarkModeOn); + final bool? isOnboardingShowing = box.get(BNames.isOnboardingShowing); emit(state.copyWith( isDarkModeOn: isDarkModeOn, isOnboardingShowing: isOnboardingShowing, - )); + ),); } - void updateDarkMode({required bool isDarkModeOn}) { + void updateDarkMode({required final bool isDarkModeOn}) { box.put(BNames.isDarkModeOn, isDarkModeOn); emit(state.copyWith(isDarkModeOn: isDarkModeOn)); } diff --git a/lib/logic/cubit/app_settings/app_settings_state.dart b/lib/logic/cubit/app_settings/app_settings_state.dart index 1300dcf4..6000fc55 100644 --- a/lib/logic/cubit/app_settings/app_settings_state.dart +++ b/lib/logic/cubit/app_settings/app_settings_state.dart @@ -1,3 +1,5 @@ +// ignore_for_file: always_specify_types + part of 'app_settings_cubit.dart'; class AppSettingsState extends Equatable { @@ -9,7 +11,7 @@ class AppSettingsState extends Equatable { final bool isDarkModeOn; final bool isOnboardingShowing; - AppSettingsState copyWith({isDarkModeOn, isOnboardingShowing}) => + AppSettingsState copyWith({final isDarkModeOn, final isOnboardingShowing}) => AppSettingsState( isDarkModeOn: isDarkModeOn ?? this.isDarkModeOn, isOnboardingShowing: isOnboardingShowing ?? this.isOnboardingShowing, diff --git a/lib/logic/cubit/backups/backups_cubit.dart b/lib/logic/cubit/backups/backups_cubit.dart index 45e24717..e77156db 100644 --- a/lib/logic/cubit/backups/backups_cubit.dart +++ b/lib/logic/cubit/backups/backups_cubit.dart @@ -1,3 +1,5 @@ +// ignore_for_file: always_specify_types + import 'dart:async'; import 'package:easy_localization/easy_localization.dart'; @@ -11,22 +13,22 @@ import 'package:selfprivacy/logic/models/json/backup.dart'; part 'backups_state.dart'; class BackupsCubit extends ServerInstallationDependendCubit { - BackupsCubit(ServerInstallationCubit serverInstallationCubit) + BackupsCubit(final ServerInstallationCubit serverInstallationCubit) : super( - serverInstallationCubit, const BackupsState(preventActions: true)); + serverInstallationCubit, const BackupsState(preventActions: true),); - final api = ServerApi(); - final backblaze = BackblazeApi(); + final ServerApi api = ServerApi(); + final BackblazeApi backblaze = BackblazeApi(); @override Future load() async { if (serverInstallationCubit.state is ServerInstallationFinished) { - final bucket = getIt().backblazeBucket; + final BackblazeBucket? bucket = getIt().backblazeBucket; if (bucket == null) { emit(const BackupsState( - isInitialized: false, preventActions: false, refreshing: false)); + isInitialized: false, preventActions: false, refreshing: false,),); } else { - final status = await api.getBackupStatus(); + final BackupStatus status = await api.getBackupStatus(); switch (status.status) { case BackupStatusEnum.noKey: case BackupStatusEnum.notInitialized: @@ -37,7 +39,7 @@ class BackupsCubit extends ServerInstallationDependendCubit { progress: 0, status: status.status, refreshing: false, - )); + ),); break; case BackupStatusEnum.initializing: emit(BackupsState( @@ -48,11 +50,11 @@ class BackupsCubit extends ServerInstallationDependendCubit { status: status.status, refreshTimer: const Duration(seconds: 10), refreshing: false, - )); + ),); break; case BackupStatusEnum.initialized: case BackupStatusEnum.error: - final backups = await api.getBackups(); + final List backups = await api.getBackups(); emit(BackupsState( backups: backups, isInitialized: true, @@ -61,11 +63,11 @@ class BackupsCubit extends ServerInstallationDependendCubit { status: status.status, error: status.errorMessage ?? '', refreshing: false, - )); + ),); break; case BackupStatusEnum.backingUp: case BackupStatusEnum.restoring: - final backups = await api.getBackups(); + final List backups = await api.getBackups(); emit(BackupsState( backups: backups, isInitialized: true, @@ -75,7 +77,7 @@ class BackupsCubit extends ServerInstallationDependendCubit { error: status.errorMessage ?? '', refreshTimer: const Duration(seconds: 5), refreshing: false, - )); + ),); break; default: emit(const BackupsState()); @@ -87,22 +89,22 @@ class BackupsCubit extends ServerInstallationDependendCubit { Future createBucket() async { emit(state.copyWith(preventActions: true)); - final domain = serverInstallationCubit.state.serverDomain!.domainName + final String domain = serverInstallationCubit.state.serverDomain!.domainName .replaceAll(RegExp(r'[^a-zA-Z0-9]'), '-'); - final serverId = serverInstallationCubit.state.serverDetails!.id; - var bucketName = 'selfprivacy-$domain-$serverId'; + final int serverId = serverInstallationCubit.state.serverDetails!.id; + String bucketName = 'selfprivacy-$domain-$serverId'; // If bucket name is too long, shorten it if (bucketName.length > 49) { bucketName = bucketName.substring(0, 49); } - final bucketId = await backblaze.createBucket(bucketName); + final String bucketId = await backblaze.createBucket(bucketName); - final key = await backblaze.createKey(bucketId); - final bucket = BackblazeBucket( + final BackblazeApplicationKey key = await backblaze.createKey(bucketId); + final BackblazeBucket bucket = BackblazeBucket( bucketId: bucketId, bucketName: bucketName, applicationKey: key.applicationKey, - applicationKeyId: key.applicationKeyId); + applicationKeyId: key.applicationKeyId,); await getIt().storeBackblazeBucket(bucket); await api.uploadBackblazeConfig(bucket); @@ -113,7 +115,7 @@ class BackupsCubit extends ServerInstallationDependendCubit { Future reuploadKey() async { emit(state.copyWith(preventActions: true)); - final bucket = getIt().backblazeBucket; + final BackblazeBucket? bucket = getIt().backblazeBucket; if (bucket == null) { emit(state.copyWith(isInitialized: false)); } else { @@ -123,7 +125,7 @@ class BackupsCubit extends ServerInstallationDependendCubit { } } - Duration refreshTimeFromState(BackupStatusEnum status) { + Duration refreshTimeFromState(final BackupStatusEnum status) { switch (status) { case BackupStatusEnum.backingUp: case BackupStatusEnum.restoring: @@ -135,10 +137,10 @@ class BackupsCubit extends ServerInstallationDependendCubit { } } - Future updateBackups({bool useTimer = false}) async { + Future updateBackups({final bool useTimer = false}) async { emit(state.copyWith(refreshing: true)); - final backups = await api.getBackups(); - final status = await api.getBackupStatus(); + final List backups = await api.getBackups(); + final BackupStatus status = await api.getBackupStatus(); emit(state.copyWith( backups: backups, progress: status.progress, @@ -146,7 +148,7 @@ class BackupsCubit extends ServerInstallationDependendCubit { error: status.errorMessage, refreshTimer: refreshTimeFromState(status.status), refreshing: false, - )); + ),); if (useTimer) { Timer(state.refreshTimer, () => updateBackups(useTimer: true)); } @@ -167,7 +169,7 @@ class BackupsCubit extends ServerInstallationDependendCubit { emit(state.copyWith(preventActions: false)); } - Future restoreBackup(String backupId) async { + Future restoreBackup(final String backupId) async { emit(state.copyWith(preventActions: true)); await api.restoreBackup(backupId); emit(state.copyWith(preventActions: false)); diff --git a/lib/logic/cubit/backups/backups_state.dart b/lib/logic/cubit/backups/backups_state.dart index 3f0e2c3f..3600c7a2 100644 --- a/lib/logic/cubit/backups/backups_state.dart +++ b/lib/logic/cubit/backups/backups_state.dart @@ -1,3 +1,5 @@ +// ignore_for_file: always_specify_types + part of 'backups_cubit.dart'; class BackupsState extends ServerInstallationDependendState { @@ -34,14 +36,14 @@ class BackupsState extends ServerInstallationDependendState { ]; BackupsState copyWith({ - bool? isInitialized, - List? backups, - double? progress, - BackupStatusEnum? status, - bool? preventActions, - String? error, - Duration? refreshTimer, - bool? refreshing, + final bool? isInitialized, + final List? backups, + final double? progress, + final BackupStatusEnum? status, + final bool? preventActions, + final String? error, + final Duration? refreshTimer, + final bool? refreshing, }) => BackupsState( isInitialized: isInitialized ?? this.isInitialized, diff --git a/lib/logic/cubit/devices/devices_cubit.dart b/lib/logic/cubit/devices/devices_cubit.dart index 4ec51d84..ec302477 100644 --- a/lib/logic/cubit/devices/devices_cubit.dart +++ b/lib/logic/cubit/devices/devices_cubit.dart @@ -1,3 +1,5 @@ +// ignore_for_file: always_specify_types + import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/logic/api_maps/server.dart'; import 'package:selfprivacy/logic/common_enum/common_enum.dart'; @@ -8,16 +10,16 @@ part 'devices_state.dart'; class ApiDevicesCubit extends ServerInstallationDependendCubit { - ApiDevicesCubit(ServerInstallationCubit serverInstallationCubit) + ApiDevicesCubit(final ServerInstallationCubit serverInstallationCubit) : super(serverInstallationCubit, const ApiDevicesState.initial()); - final api = ServerApi(); + final ServerApi api = ServerApi(); @override void load() async { if (serverInstallationCubit.state is ServerInstallationFinished) { emit(const ApiDevicesState([], LoadingStatus.refreshing)); - final devices = await _getApiTokens(); + final List? devices = await _getApiTokens(); if (devices != null) { emit(ApiDevicesState(devices, LoadingStatus.success)); } else { @@ -28,7 +30,7 @@ class ApiDevicesCubit Future refresh() async { emit(const ApiDevicesState([], LoadingStatus.refreshing)); - final devices = await _getApiTokens(); + final List? devices = await _getApiTokens(); if (devices != null) { emit(ApiDevicesState(devices, LoadingStatus.success)); } else { @@ -37,7 +39,7 @@ class ApiDevicesCubit } Future?> _getApiTokens() async { - final response = await api.getApiTokens(); + final ApiResponse> response = await api.getApiTokens(); if (response.isSuccess) { return response.data; } else { @@ -45,12 +47,12 @@ class ApiDevicesCubit } } - Future deleteDevice(ApiToken device) async { - final response = await api.deleteApiToken(device.name); + Future deleteDevice(final ApiToken device) async { + final ApiResponse response = await api.deleteApiToken(device.name); if (response.isSuccess) { emit(ApiDevicesState( - state.devices.where((d) => d.name != device.name).toList(), - LoadingStatus.success)); + state.devices.where((final d) => d.name != device.name).toList(), + LoadingStatus.success,),); } else { getIt() .showSnackBar(response.errorMessage ?? 'Error deleting device'); @@ -58,12 +60,12 @@ class ApiDevicesCubit } Future getNewDeviceKey() async { - final response = await api.createDeviceToken(); + final ApiResponse response = await api.createDeviceToken(); if (response.isSuccess) { return response.data; } else { getIt().showSnackBar( - response.errorMessage ?? 'Error getting new device key'); + response.errorMessage ?? 'Error getting new device key',); return null; } } diff --git a/lib/logic/cubit/devices/devices_state.dart b/lib/logic/cubit/devices/devices_state.dart index bccc5e29..ba7d7e90 100644 --- a/lib/logic/cubit/devices/devices_state.dart +++ b/lib/logic/cubit/devices/devices_state.dart @@ -1,3 +1,5 @@ +// ignore_for_file: always_specify_types + part of 'devices_cubit.dart'; class ApiDevicesState extends ServerInstallationDependendState { @@ -8,25 +10,23 @@ class ApiDevicesState extends ServerInstallationDependendState { final LoadingStatus status; List get devices => _devices; - ApiToken get thisDevice => _devices.firstWhere((device) => device.isCaller, + ApiToken get thisDevice => _devices.firstWhere((final device) => device.isCaller, orElse: () => ApiToken( name: 'Error fetching device', isCaller: true, date: DateTime.now(), - )); + ),); List get otherDevices => - _devices.where((device) => !device.isCaller).toList(); + _devices.where((final device) => !device.isCaller).toList(); ApiDevicesState copyWith({ - List? devices, - LoadingStatus? status, - }) { - return ApiDevicesState( + final List? devices, + final LoadingStatus? status, + }) => ApiDevicesState( devices ?? _devices, status ?? this.status, ); - } @override List get props => [_devices]; diff --git a/lib/logic/cubit/dns_records/dns_records_cubit.dart b/lib/logic/cubit/dns_records/dns_records_cubit.dart index 9d9bdf8e..d6faba8e 100644 --- a/lib/logic/cubit/dns_records/dns_records_cubit.dart +++ b/lib/logic/cubit/dns_records/dns_records_cubit.dart @@ -1,28 +1,30 @@ +// ignore_for_file: always_specify_types + import 'package:cubit_form/cubit_form.dart'; import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart'; import 'package:selfprivacy/logic/models/hive/server_domain.dart'; import 'package:selfprivacy/logic/models/json/dns_records.dart'; -import '../../api_maps/cloudflare.dart'; -import '../../api_maps/server.dart'; +import 'package:selfprivacy/logic/api_maps/cloudflare.dart'; +import 'package:selfprivacy/logic/api_maps/server.dart'; part 'dns_records_state.dart'; class DnsRecordsCubit extends ServerInstallationDependendCubit { - DnsRecordsCubit(ServerInstallationCubit serverInstallationCubit) + DnsRecordsCubit(final ServerInstallationCubit serverInstallationCubit) : super(serverInstallationCubit, - const DnsRecordsState(dnsState: DnsRecordsStatus.refreshing)); + const DnsRecordsState(dnsState: DnsRecordsStatus.refreshing),); - final api = ServerApi(); - final cloudflare = CloudflareApi(); + final ServerApi api = ServerApi(); + final CloudflareApi cloudflare = CloudflareApi(); @override Future load() async { emit(DnsRecordsState( dnsState: DnsRecordsStatus.refreshing, dnsRecords: _getDesiredDnsRecords( - serverInstallationCubit.state.serverDomain?.domainName, '', ''))); + serverInstallationCubit.state.serverDomain?.domainName, '', '',),),); print('Loading DNS status'); if (serverInstallationCubit.state is ServerInstallationFinished) { final ServerDomain? domain = serverInstallationCubit.state.serverDomain; @@ -31,37 +33,37 @@ class DnsRecordsCubit if (domain != null && ipAddress != null) { final List records = await cloudflare.getDnsRecords(cloudFlareDomain: domain); - final dkimPublicKey = await api.getDkim(); - final desiredRecords = + final String? dkimPublicKey = await api.getDkim(); + final List desiredRecords = _getDesiredDnsRecords(domain.domainName, ipAddress, dkimPublicKey); - List foundRecords = []; - for (final record in desiredRecords) { + final List foundRecords = []; + for (final DesiredDnsRecord record in desiredRecords) { if (record.description == 'providers.domain.record_description.dkim') { - final foundRecord = records.firstWhere( - (r) => r.name == record.name && r.type == record.type, + final DnsRecord foundRecord = records.firstWhere( + (final r) => r.name == record.name && r.type == record.type, orElse: () => DnsRecord( name: record.name, type: record.type, content: '', ttl: 800, - proxied: false)); + proxied: false,),); // remove all spaces and tabulators from // the foundRecord.content and the record.content // to compare them - final foundContent = + final String? foundContent = foundRecord.content?.replaceAll(RegExp(r'\s+'), ''); - final content = record.content.replaceAll(RegExp(r'\s+'), ''); + final String content = record.content.replaceAll(RegExp(r'\s+'), ''); if (foundContent == content) { foundRecords.add(record.copyWith(isSatisfied: true)); } else { foundRecords.add(record.copyWith(isSatisfied: false)); } } else { - if (records.any((r) => + if (records.any((final r) => r.name == record.name && r.type == record.type && - r.content == record.content)) { + r.content == record.content,)) { foundRecords.add(record.copyWith(isSatisfied: true)); } else { foundRecords.add(record.copyWith(isSatisfied: false)); @@ -70,10 +72,10 @@ class DnsRecordsCubit } emit(DnsRecordsState( dnsRecords: foundRecords, - dnsState: foundRecords.any((r) => r.isSatisfied == false) + dnsState: foundRecords.any((final r) => r.isSatisfied == false) ? DnsRecordsStatus.error : DnsRecordsStatus.good, - )); + ),); } else { emit(const DnsRecordsState()); } @@ -81,7 +83,7 @@ class DnsRecordsCubit } @override - void onChange(Change change) { + void onChange(final Change change) { // print(change); super.onChange(change); } @@ -103,13 +105,13 @@ class DnsRecordsCubit final String? dkimPublicKey = await api.getDkim(); await cloudflare.removeSimilarRecords(cloudFlareDomain: domain!); await cloudflare.createMultipleDnsRecords( - cloudFlareDomain: domain, ip4: ipAddress); + cloudFlareDomain: domain, ip4: ipAddress,); await cloudflare.setDkim(dkimPublicKey ?? '', domain); await load(); } List _getDesiredDnsRecords( - String? domainName, String? ipAddress, String? dkimPublicKey) { + final String? domainName, final String? ipAddress, final String? dkimPublicKey,) { if (domainName == null || ipAddress == null || dkimPublicKey == null) { return []; } diff --git a/lib/logic/cubit/dns_records/dns_records_state.dart b/lib/logic/cubit/dns_records/dns_records_state.dart index 59c266b7..d6d0c67b 100644 --- a/lib/logic/cubit/dns_records/dns_records_state.dart +++ b/lib/logic/cubit/dns_records/dns_records_state.dart @@ -1,3 +1,5 @@ +// ignore_for_file: always_specify_types + part of 'dns_records_cubit.dart'; enum DnsRecordsStatus { @@ -29,21 +31,19 @@ class DnsRecordsState extends ServerInstallationDependendState { ]; DnsRecordsState copyWith({ - DnsRecordsStatus? dnsState, - List? dnsRecords, - }) { - return DnsRecordsState( + final DnsRecordsStatus? dnsState, + final List? dnsRecords, + }) => DnsRecordsState( dnsState: dnsState ?? this.dnsState, dnsRecords: dnsRecords ?? this.dnsRecords, ); - } } class DesiredDnsRecord { const DesiredDnsRecord({ required this.name, - this.type = 'A', required this.content, + this.type = 'A', this.description = '', this.category = DnsRecordsCategory.services, this.isSatisfied = false, @@ -57,14 +57,13 @@ class DesiredDnsRecord { final bool isSatisfied; DesiredDnsRecord copyWith({ - String? name, - String? type, - String? content, - String? description, - DnsRecordsCategory? category, - bool? isSatisfied, - }) { - return DesiredDnsRecord( + final String? name, + final String? type, + final String? content, + final String? description, + final DnsRecordsCategory? category, + final bool? isSatisfied, + }) => DesiredDnsRecord( name: name ?? this.name, type: type ?? this.type, content: content ?? this.content, @@ -72,5 +71,4 @@ class DesiredDnsRecord { category: category ?? this.category, isSatisfied: isSatisfied ?? this.isSatisfied, ); - } } diff --git a/lib/logic/cubit/forms/factories/field_cubit_factory.dart b/lib/logic/cubit/forms/factories/field_cubit_factory.dart index 86f3b70b..d71f3a7b 100644 --- a/lib/logic/cubit/forms/factories/field_cubit_factory.dart +++ b/lib/logic/cubit/forms/factories/field_cubit_factory.dart @@ -1,3 +1,5 @@ +// ignore_for_file: always_specify_types + import 'package:flutter/material.dart'; import 'package:selfprivacy/logic/cubit/forms/validations/validations.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -16,21 +18,21 @@ class FieldCubitFactory { /// - Must not be a reserved root login /// - Must be unique FieldCubit createUserLoginField() { - final userAllowedRegExp = RegExp(r'^[a-z_][a-z0-9_]+$'); - const userMaxLength = 31; + final RegExp userAllowedRegExp = RegExp(r'^[a-z_][a-z0-9_]+$'); + const int userMaxLength = 31; return FieldCubit( initalValue: '', validations: [ ValidationModel( - (s) => s.toLowerCase() == 'root', 'validations.root_name'.tr()), + (final String s) => s.toLowerCase() == 'root', 'validations.root_name'.tr(),), ValidationModel( - (login) => context.read().state.isLoginRegistered(login), + (final String login) => context.read().state.isLoginRegistered(login), 'validations.user_already_exist'.tr(), ), RequiredStringValidation('validations.required'.tr()), LengthStringLongerValidation(userMaxLength), - ValidationModel((s) => !userAllowedRegExp.hasMatch(s), - 'validations.invalid_format'.tr()), + ValidationModel((final String s) => !userAllowedRegExp.hasMatch(s), + 'validations.invalid_format'.tr(),), ], ); } @@ -40,26 +42,24 @@ class FieldCubitFactory { /// - Must fail on the regural expression of invalid matches: [\n\r\s]+ /// - Must not be empty FieldCubit createUserPasswordField() { - var passwordForbiddenRegExp = RegExp(r'[\n\r\s]+'); + final RegExp passwordForbiddenRegExp = RegExp(r'[\n\r\s]+'); return FieldCubit( initalValue: '', validations: [ RequiredStringValidation('validations.required'.tr()), ValidationModel( - (password) => passwordForbiddenRegExp.hasMatch(password), - 'validations.invalid_format'.tr()), + passwordForbiddenRegExp.hasMatch, + 'validations.invalid_format'.tr(),), ], ); } - FieldCubit createRequiredStringField() { - return FieldCubit( + FieldCubit createRequiredStringField() => FieldCubit( initalValue: '', validations: [ RequiredStringValidation('validations.required'.tr()), ], ); - } final BuildContext context; } diff --git a/lib/logic/cubit/forms/setup/initializing/backblaze_form_cubit.dart b/lib/logic/cubit/forms/setup/initializing/backblaze_form_cubit.dart index 9958effa..0ac87e30 100644 --- a/lib/logic/cubit/forms/setup/initializing/backblaze_form_cubit.dart +++ b/lib/logic/cubit/forms/setup/initializing/backblaze_form_cubit.dart @@ -1,3 +1,5 @@ +// ignore_for_file: always_specify_types + import 'dart:async'; import 'package:cubit_form/cubit_form.dart'; import 'package:selfprivacy/logic/api_maps/backblaze.dart'; @@ -41,10 +43,10 @@ class BackblazeFormCubit extends FormCubit { @override FutureOr asyncValidation() async { late bool isKeyValid; - BackblazeApi apiClient = BackblazeApi(isWithToken: false); + final BackblazeApi apiClient = BackblazeApi(isWithToken: false); try { - String encodedApiKey = encodedBackblazeKey( + final String encodedApiKey = encodedBackblazeKey( keyId.state.value, applicationKey.state.value, ); diff --git a/lib/logic/cubit/forms/setup/initializing/cloudflare_form_cubit.dart b/lib/logic/cubit/forms/setup/initializing/cloudflare_form_cubit.dart index fd700633..c8dc14d1 100644 --- a/lib/logic/cubit/forms/setup/initializing/cloudflare_form_cubit.dart +++ b/lib/logic/cubit/forms/setup/initializing/cloudflare_form_cubit.dart @@ -1,3 +1,5 @@ +// ignore_for_file: always_specify_types + import 'dart:async'; import 'package:cubit_form/cubit_form.dart'; @@ -8,13 +10,13 @@ import 'package:selfprivacy/logic/cubit/forms/validations/validations.dart'; class CloudFlareFormCubit extends FormCubit { CloudFlareFormCubit(this.initializingCubit) { - var regExp = RegExp(r'\s+|[!$%^&*()@+|~=`{}\[\]:<>?,.\/]'); + final RegExp regExp = RegExp(r'\s+|[!$%^&*()@+|~=`{}\[\]:<>?,.\/]'); apiKey = FieldCubit( initalValue: '', validations: [ RequiredStringValidation('validations.required'.tr()), ValidationModel( - (s) => regExp.hasMatch(s), 'validations.key_format'.tr()), + regExp.hasMatch, 'validations.key_format'.tr(),), LengthStringNotEqualValidation(40) ], ); @@ -34,7 +36,7 @@ class CloudFlareFormCubit extends FormCubit { @override FutureOr asyncValidation() async { late bool isKeyValid; - CloudflareApi apiClient = CloudflareApi(isWithToken: false); + final CloudflareApi apiClient = CloudflareApi(isWithToken: false); try { isKeyValid = await apiClient.isValid(apiKey.state.value); diff --git a/lib/logic/cubit/forms/setup/initializing/domain_cloudflare.dart b/lib/logic/cubit/forms/setup/initializing/domain_cloudflare.dart index bf9e1eb0..db7044f4 100644 --- a/lib/logic/cubit/forms/setup/initializing/domain_cloudflare.dart +++ b/lib/logic/cubit/forms/setup/initializing/domain_cloudflare.dart @@ -10,9 +10,9 @@ class DomainSetupCubit extends Cubit { Future load() async { emit(Loading(LoadingTypes.loadingDomain)); - var api = CloudflareApi(); + final CloudflareApi api = CloudflareApi(); - var list = await api.domainList(); + final List list = await api.domainList(); if (list.isEmpty) { emit(Empty()); } else if (list.length == 1) { @@ -23,20 +23,18 @@ class DomainSetupCubit extends Cubit { } @override - Future close() { - return super.close(); - } + Future close() => super.close(); Future saveDomain() async { assert(state is Loaded, 'wrong state'); - var domainName = (state as Loaded).domain; - var api = CloudflareApi(); + final String domainName = (state as Loaded).domain; + final CloudflareApi api = CloudflareApi(); emit(Loading(LoadingTypes.saving)); - var zoneId = await api.getZoneId(domainName); + final String zoneId = await api.getZoneId(domainName); - var domain = ServerDomain( + final ServerDomain domain = ServerDomain( domainName: domainName, zoneId: zoneId, provider: DnsProvider.cloudflare, @@ -63,9 +61,9 @@ class Loading extends DomainSetupState { enum LoadingTypes { loadingDomain, saving } class Loaded extends DomainSetupState { - final String domain; Loaded(this.domain); + final String domain; } class DomainSet extends DomainSetupState {} diff --git a/lib/logic/cubit/forms/setup/initializing/hetzner_form_cubit.dart b/lib/logic/cubit/forms/setup/initializing/hetzner_form_cubit.dart index 0d343191..a7e2bb1d 100644 --- a/lib/logic/cubit/forms/setup/initializing/hetzner_form_cubit.dart +++ b/lib/logic/cubit/forms/setup/initializing/hetzner_form_cubit.dart @@ -1,3 +1,5 @@ +// ignore_for_file: always_specify_types + import 'dart:async'; import 'package:cubit_form/cubit_form.dart'; @@ -8,13 +10,13 @@ import 'package:selfprivacy/logic/cubit/forms/validations/validations.dart'; class HetznerFormCubit extends FormCubit { HetznerFormCubit(this.serverInstallationCubit) { - var regExp = RegExp(r'\s+|[-!$%^&*()@+|~=`{}\[\]:<>?,.\/]'); + final RegExp regExp = RegExp(r'\s+|[-!$%^&*()@+|~=`{}\[\]:<>?,.\/]'); apiKey = FieldCubit( initalValue: '', validations: [ RequiredStringValidation('validations.required'.tr()), ValidationModel( - (s) => regExp.hasMatch(s), 'validations.key_format'.tr()), + regExp.hasMatch, 'validations.key_format'.tr(),), LengthStringNotEqualValidation(64) ], ); @@ -34,7 +36,7 @@ class HetznerFormCubit extends FormCubit { @override FutureOr asyncValidation() async { late bool isKeyValid; - HetznerApi apiClient = HetznerApi(isWithToken: false); + final HetznerApi apiClient = HetznerApi(isWithToken: false); try { isKeyValid = await apiClient.isValid(apiKey.state.value); diff --git a/lib/logic/cubit/forms/setup/initializing/root_user_form_cubit.dart b/lib/logic/cubit/forms/setup/initializing/root_user_form_cubit.dart index 6e3e5c3d..0914d86b 100644 --- a/lib/logic/cubit/forms/setup/initializing/root_user_form_cubit.dart +++ b/lib/logic/cubit/forms/setup/initializing/root_user_form_cubit.dart @@ -1,3 +1,5 @@ +// ignore_for_file: always_specify_types + import 'dart:async'; import 'package:cubit_form/cubit_form.dart'; @@ -7,7 +9,7 @@ import 'package:selfprivacy/logic/models/hive/user.dart'; class RootUserFormCubit extends FormCubit { RootUserFormCubit( - this.serverInstallationCubit, final FieldCubitFactory fieldFactory) { + this.serverInstallationCubit, final FieldCubitFactory fieldFactory,) { userName = fieldFactory.createUserLoginField(); password = fieldFactory.createUserPasswordField(); @@ -18,7 +20,7 @@ class RootUserFormCubit extends FormCubit { @override FutureOr onSubmit() async { - var user = User( + final User user = User( login: userName.state.value, password: password.state.value, ); diff --git a/lib/logic/cubit/forms/setup/recovering/recovery_device_form_cubit.dart b/lib/logic/cubit/forms/setup/recovering/recovery_device_form_cubit.dart index 98c08f5c..00f5b685 100644 --- a/lib/logic/cubit/forms/setup/recovering/recovery_device_form_cubit.dart +++ b/lib/logic/cubit/forms/setup/recovering/recovery_device_form_cubit.dart @@ -1,3 +1,5 @@ +// ignore_for_file: always_specify_types + import 'dart:async'; import 'package:cubit_form/cubit_form.dart'; @@ -6,7 +8,7 @@ import 'package:selfprivacy/logic/cubit/forms/factories/field_cubit_factory.dart class RecoveryDeviceFormCubit extends FormCubit { RecoveryDeviceFormCubit(this.installationCubit, - final FieldCubitFactory fieldFactory, this.recoveryMethod) { + final FieldCubitFactory fieldFactory, this.recoveryMethod,) { tokenField = fieldFactory.createRequiredStringField(); super.addFields([tokenField]); diff --git a/lib/logic/cubit/forms/setup/recovering/recovery_domain_form_cubit.dart b/lib/logic/cubit/forms/setup/recovering/recovery_domain_form_cubit.dart index 0064cae8..d2bea806 100644 --- a/lib/logic/cubit/forms/setup/recovering/recovery_domain_form_cubit.dart +++ b/lib/logic/cubit/forms/setup/recovering/recovery_domain_form_cubit.dart @@ -1,3 +1,5 @@ +// ignore_for_file: always_specify_types + import 'dart:async'; import 'package:cubit_form/cubit_form.dart'; @@ -8,7 +10,7 @@ import 'package:selfprivacy/logic/cubit/forms/factories/field_cubit_factory.dart class RecoveryDomainFormCubit extends FormCubit { RecoveryDomainFormCubit( - this.initializingCubit, final FieldCubitFactory fieldFactory) { + this.initializingCubit, final FieldCubitFactory fieldFactory,) { serverDomainField = fieldFactory.createRequiredStringField(); super.addFields([serverDomainField]); @@ -22,10 +24,10 @@ class RecoveryDomainFormCubit extends FormCubit { @override FutureOr asyncValidation() async { - var api = ServerApi( + final ServerApi api = ServerApi( hasLogger: false, isWithToken: false, - overrideDomain: serverDomainField.state.value); + overrideDomain: serverDomainField.state.value,); // API version doesn't require access token, // so if the entered domain is indeed valid @@ -40,7 +42,7 @@ class RecoveryDomainFormCubit extends FormCubit { return domainValid; } - FutureOr setCustomError(String error) { + FutureOr setCustomError(final String error) { serverDomainField.setError(error); } diff --git a/lib/logic/cubit/forms/user/ssh_form_cubit.dart b/lib/logic/cubit/forms/user/ssh_form_cubit.dart index bebbbcd5..4262939e 100644 --- a/lib/logic/cubit/forms/user/ssh_form_cubit.dart +++ b/lib/logic/cubit/forms/user/ssh_form_cubit.dart @@ -1,3 +1,5 @@ +// ignore_for_file: always_specify_types + import 'dart:async'; import 'package:cubit_form/cubit_form.dart'; @@ -11,22 +13,22 @@ class SshFormCubit extends FormCubit { required this.jobsCubit, required this.user, }) { - var keyRegExp = RegExp( - r'^(ssh-rsa AAAAB3NzaC1yc2|ssh-ed25519 AAAAC3NzaC1lZDI1NTE5)[0-9A-Za-z+/]+[=]{0,3}( .*)?$'); + final RegExp keyRegExp = RegExp( + r'^(ssh-rsa AAAAB3NzaC1yc2|ssh-ed25519 AAAAC3NzaC1lZDI1NTE5)[0-9A-Za-z+/]+[=]{0,3}( .*)?$',); key = FieldCubit( initalValue: '', validations: [ ValidationModel( - (newKey) => user.sshKeys.any((key) => key == newKey), + (final String newKey) => user.sshKeys.any((final String key) => key == newKey), 'validations.key_already_exists'.tr(), ), RequiredStringValidation('validations.required'.tr()), - ValidationModel((s) { + ValidationModel((final String s) { print(s); print(keyRegExp.hasMatch(s)); return !keyRegExp.hasMatch(s); - }, 'validations.invalid_format'.tr()), + }, 'validations.invalid_format'.tr(),), ], ); diff --git a/lib/logic/cubit/forms/user/user_form_cubit.dart b/lib/logic/cubit/forms/user/user_form_cubit.dart index dbe19ad5..30d83e26 100644 --- a/lib/logic/cubit/forms/user/user_form_cubit.dart +++ b/lib/logic/cubit/forms/user/user_form_cubit.dart @@ -1,3 +1,5 @@ +// ignore_for_file: always_specify_types + import 'dart:async'; import 'package:cubit_form/cubit_form.dart'; @@ -10,23 +12,23 @@ import 'package:selfprivacy/utils/password_generator.dart'; class UserFormCubit extends FormCubit { UserFormCubit({ required this.jobsCubit, - required FieldCubitFactory fieldFactory, - User? user, + required final FieldCubitFactory fieldFactory, + final User? user, }) { - var isEdit = user != null; + final bool isEdit = user != null; login = fieldFactory.createUserLoginField(); login.setValue(isEdit ? user.login : ''); password = fieldFactory.createUserPasswordField(); password.setValue( - isEdit ? (user.password ?? '') : StringGenerators.userPassword()); + isEdit ? (user.password ?? '') : StringGenerators.userPassword(),); super.addFields([login, password]); } @override FutureOr onSubmit() { - var user = User( + final User user = User( login: login.state.value, password: password.state.value, ); diff --git a/lib/logic/cubit/forms/validations/validations.dart b/lib/logic/cubit/forms/validations/validations.dart index 800ca77b..dd2e653b 100644 --- a/lib/logic/cubit/forms/validations/validations.dart +++ b/lib/logic/cubit/forms/validations/validations.dart @@ -1,28 +1,29 @@ +// ignore_for_file: always_specify_types + import 'package:cubit_form/cubit_form.dart'; import 'package:easy_localization/easy_localization.dart'; abstract class LengthStringValidation extends ValidationModel { - LengthStringValidation(bool Function(String) predicate, String errorMessage) - : super(predicate, errorMessage); + LengthStringValidation(super.predicate, super.errorMessage); @override - String? check(String val) { - var length = val.length; - var errorMessage = errorMassage.replaceAll('[]', length.toString()); + String? check(final String val) { + final int length = val.length; + final String errorMessage = errorMassage.replaceAll('[]', length.toString()); return test(val) ? errorMessage : null; } } class LengthStringNotEqualValidation extends LengthStringValidation { /// String must be equal to [length] - LengthStringNotEqualValidation(int length) - : super((n) => n.length != length, - 'validations.length_not_equal'.tr(args: [length.toString()])); + LengthStringNotEqualValidation(final int length) + : super((final n) => n.length != length, + 'validations.length_not_equal'.tr(args: [length.toString()]),); } class LengthStringLongerValidation extends LengthStringValidation { /// String must be shorter than or equal to [length] - LengthStringLongerValidation(int length) - : super((n) => n.length > length, - 'validations.length_longer'.tr(args: [length.toString()])); + LengthStringLongerValidation(final int length) + : super((final n) => n.length > length, + 'validations.length_longer'.tr(args: [length.toString()]),); } diff --git a/lib/logic/cubit/hetzner_metrics/hetzner_metrics_cubit.dart b/lib/logic/cubit/hetzner_metrics/hetzner_metrics_cubit.dart index d16b13b0..aaae36c5 100644 --- a/lib/logic/cubit/hetzner_metrics/hetzner_metrics_cubit.dart +++ b/lib/logic/cubit/hetzner_metrics/hetzner_metrics_cubit.dart @@ -5,19 +5,19 @@ import 'package:equatable/equatable.dart'; import 'package:selfprivacy/logic/common_enum/common_enum.dart'; import 'package:selfprivacy/logic/models/hetzner_metrics.dart'; -import 'hetzner_metrics_repository.dart'; +import 'package:selfprivacy/logic/cubit/hetzner_metrics/hetzner_metrics_repository.dart'; part 'hetzner_metrics_state.dart'; class HetznerMetricsCubit extends Cubit { HetznerMetricsCubit() : super(const HetznerMetricsLoading(Period.day)); - final repository = HetznerMetricsRepository(); + final HetznerMetricsRepository repository = HetznerMetricsRepository(); Timer? timer; @override - close() { + Future close() { closeTimer(); return super.close(); } @@ -28,7 +28,7 @@ class HetznerMetricsCubit extends Cubit { } } - void changePeriod(Period period) async { + void changePeriod(final Period period) async { closeTimer(); emit(HetznerMetricsLoading(period)); load(period); @@ -38,8 +38,8 @@ class HetznerMetricsCubit extends Cubit { load(state.period); } - void load(Period period) async { - var newState = await repository.getMetrics(period); + void load(final Period period) async { + final HetznerMetricsLoaded newState = await repository.getMetrics(period); timer = Timer( Duration(seconds: newState.stepInSeconds.toInt()), () => load(newState.period), diff --git a/lib/logic/cubit/hetzner_metrics/hetzner_metrics_repository.dart b/lib/logic/cubit/hetzner_metrics/hetzner_metrics_repository.dart index fe601cc6..f9bcf15f 100644 --- a/lib/logic/cubit/hetzner_metrics/hetzner_metrics_repository.dart +++ b/lib/logic/cubit/hetzner_metrics/hetzner_metrics_repository.dart @@ -1,12 +1,14 @@ +// ignore_for_file: always_specify_types + import 'package:selfprivacy/logic/api_maps/hetzner.dart'; import 'package:selfprivacy/logic/common_enum/common_enum.dart'; import 'package:selfprivacy/logic/models/hetzner_metrics.dart'; -import 'hetzner_metrics_cubit.dart'; +import 'package:selfprivacy/logic/cubit/hetzner_metrics/hetzner_metrics_cubit.dart'; class HetznerMetricsRepository { - Future getMetrics(Period period) async { - var end = DateTime.now(); + Future getMetrics(final Period period) async { + final DateTime end = DateTime.now(); DateTime start; switch (period) { @@ -21,15 +23,15 @@ class HetznerMetricsRepository { break; } - var api = HetznerApi(hasLogger: true); + final HetznerApi api = HetznerApi(hasLogger: true); - var results = await Future.wait([ + final List> results = await Future.wait([ api.getMetrics(start, end, 'cpu'), api.getMetrics(start, end, 'network'), ]); - var cpuMetricsData = results[0]['metrics']; - var networkMetricsData = results[1]['metrics']; + final cpuMetricsData = results[0]['metrics']; + final networkMetricsData = results[1]['metrics']; return HetznerMetricsLoaded( period: period, @@ -50,7 +52,7 @@ class HetznerMetricsRepository { } List timeSeriesSerializer( - Map json, String type) { - List list = json['time_series'][type]['values']; - return list.map((el) => TimeSeriesData(el[0], double.parse(el[1]))).toList(); + final Map json, final String type,) { + final List list = json['time_series'][type]['values']; + return list.map((final el) => TimeSeriesData(el[0], double.parse(el[1]))).toList(); } diff --git a/lib/logic/cubit/hetzner_metrics/hetzner_metrics_state.dart b/lib/logic/cubit/hetzner_metrics/hetzner_metrics_state.dart index b6204db9..793bfdfc 100644 --- a/lib/logic/cubit/hetzner_metrics/hetzner_metrics_state.dart +++ b/lib/logic/cubit/hetzner_metrics/hetzner_metrics_state.dart @@ -1,3 +1,5 @@ +// ignore_for_file: always_specify_types + part of 'hetzner_metrics_cubit.dart'; abstract class HetznerMetricsState extends Equatable { diff --git a/lib/logic/cubit/jobs/jobs_cubit.dart b/lib/logic/cubit/jobs/jobs_cubit.dart index f2ce57d1..7f116f1d 100644 --- a/lib/logic/cubit/jobs/jobs_cubit.dart +++ b/lib/logic/cubit/jobs/jobs_cubit.dart @@ -1,3 +1,5 @@ +// ignore_for_file: always_specify_types + import 'package:easy_localization/easy_localization.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -17,12 +19,12 @@ class JobsCubit extends Cubit { required this.servicesCubit, }) : super(JobsStateEmpty()); - final api = ServerApi(); + final ServerApi api = ServerApi(); final UsersCubit usersCubit; final ServicesCubit servicesCubit; - void addJob(Job job) { - var newJobsList = []; + void addJob(final Job job) { + final List newJobsList = []; if (state is JobsStateWithJobs) { newJobsList.addAll((state as JobsStateWithJobs).jobList); } @@ -31,21 +33,21 @@ class JobsCubit extends Cubit { emit(JobsStateWithJobs(newJobsList)); } - void removeJob(String id) { - final newState = (state as JobsStateWithJobs).removeById(id); + void removeJob(final String id) { + final JobsState newState = (state as JobsStateWithJobs).removeById(id); emit(newState); } - void createOrRemoveServiceToggleJob(ToggleJob job) { - var newJobsList = []; + void createOrRemoveServiceToggleJob(final ToggleJob job) { + final List newJobsList = []; if (state is JobsStateWithJobs) { newJobsList.addAll((state as JobsStateWithJobs).jobList); } - var needToRemoveJob = - newJobsList.any((el) => el is ServiceToggleJob && el.type == job.type); + final bool needToRemoveJob = + newJobsList.any((final el) => el is ServiceToggleJob && el.type == job.type); if (needToRemoveJob) { - var removingJob = newJobsList - .firstWhere(((el) => el is ServiceToggleJob && el.type == job.type)); + final Job removingJob = newJobsList + .firstWhere((final el) => el is ServiceToggleJob && el.type == job.type); removeJob(removingJob.id); } else { newJobsList.add(job); @@ -54,12 +56,12 @@ class JobsCubit extends Cubit { } } - void createShhJobIfNotExist(CreateSSHKeyJob job) { - var newJobsList = []; + void createShhJobIfNotExist(final CreateSSHKeyJob job) { + final List newJobsList = []; if (state is JobsStateWithJobs) { newJobsList.addAll((state as JobsStateWithJobs).jobList); } - var isExistInJobList = newJobsList.any((el) => el is CreateSSHKeyJob); + final bool isExistInJobList = newJobsList.any((final el) => el is CreateSSHKeyJob); if (!isExistInJobList) { newJobsList.add(job); getIt().showSnackBar('jobs.jobAdded'.tr()); @@ -69,7 +71,7 @@ class JobsCubit extends Cubit { Future rebootServer() async { emit(JobsStateLoading()); - final isSuccessful = await api.reboot(); + final bool isSuccessful = await api.reboot(); if (isSuccessful) { getIt().showSnackBar('jobs.rebootSuccess'.tr()); } else { @@ -80,8 +82,8 @@ class JobsCubit extends Cubit { Future upgradeServer() async { emit(JobsStateLoading()); - final isPullSuccessful = await api.pullConfigurationUpdate(); - final isSuccessful = await api.upgrade(); + final bool isPullSuccessful = await api.pullConfigurationUpdate(); + final bool isSuccessful = await api.upgrade(); if (isSuccessful) { if (!isPullSuccessful) { getIt().showSnackBar('jobs.configPullFailed'.tr()); @@ -96,10 +98,10 @@ class JobsCubit extends Cubit { Future applyAll() async { if (state is JobsStateWithJobs) { - var jobs = (state as JobsStateWithJobs).jobList; + final List jobs = (state as JobsStateWithJobs).jobList; emit(JobsStateLoading()); - var hasServiceJobs = false; - for (var job in jobs) { + bool hasServiceJobs = false; + for (final Job job in jobs) { if (job is CreateUserJob) { await usersCubit.createUser(job.user); } diff --git a/lib/logic/cubit/jobs/jobs_state.dart b/lib/logic/cubit/jobs/jobs_state.dart index 972f4b3d..0817dfbf 100644 --- a/lib/logic/cubit/jobs/jobs_state.dart +++ b/lib/logic/cubit/jobs/jobs_state.dart @@ -1,3 +1,5 @@ +// ignore_for_file: always_specify_types + part of 'jobs_cubit.dart'; abstract class JobsState extends Equatable { @@ -13,8 +15,8 @@ class JobsStateWithJobs extends JobsState { JobsStateWithJobs(this.jobList); final List jobList; - JobsState removeById(String id) { - var newJobsList = jobList.where((element) => element.id != id).toList(); + JobsState removeById(final String id) { + final List newJobsList = jobList.where((final element) => element.id != id).toList(); if (newJobsList.isEmpty) { return JobsStateEmpty(); diff --git a/lib/logic/cubit/providers/providers_cubit.dart b/lib/logic/cubit/providers/providers_cubit.dart index 5d48ecda..eb33046f 100644 --- a/lib/logic/cubit/providers/providers_cubit.dart +++ b/lib/logic/cubit/providers/providers_cubit.dart @@ -11,8 +11,8 @@ part 'providers_state.dart'; class ProvidersCubit extends Cubit { ProvidersCubit() : super(InitialProviderState()); - void connect(ProviderModel provider) { - var newState = state.updateElement(provider, StateType.stable); + void connect(final ProviderModel provider) { + final ProvidersState newState = state.updateElement(provider, StateType.stable); emit(newState); } } diff --git a/lib/logic/cubit/providers/providers_state.dart b/lib/logic/cubit/providers/providers_state.dart index 8297699d..89951b60 100644 --- a/lib/logic/cubit/providers/providers_state.dart +++ b/lib/logic/cubit/providers/providers_state.dart @@ -1,3 +1,5 @@ +// ignore_for_file: always_specify_types + part of 'providers_cubit.dart'; class ProvidersState extends Equatable { @@ -5,18 +7,18 @@ class ProvidersState extends Equatable { final List all; - ProvidersState updateElement(ProviderModel provider, StateType newState) { - var newList = [...all]; - var index = newList.indexOf(provider); + ProvidersState updateElement(final ProviderModel provider, final StateType newState) { + final List newList = [...all]; + final int index = newList.indexOf(provider); newList[index] = provider.updateState(newState); return ProvidersState(newList); } List get connected => - all.where((service) => service.state != StateType.uninitialized).toList(); + all.where((final service) => service.state != StateType.uninitialized).toList(); List get uninitialized => - all.where((service) => service.state == StateType.uninitialized).toList(); + all.where((final service) => service.state == StateType.uninitialized).toList(); bool get isFullyInitialized => uninitialized.isEmpty; @@ -29,7 +31,7 @@ class InitialProviderState extends ProvidersState { : super( ProviderType.values .map( - (type) => ProviderModel( + (final type) => ProviderModel( state: StateType.uninitialized, type: type, ), diff --git a/lib/logic/cubit/recovery_key/recovery_key_cubit.dart b/lib/logic/cubit/recovery_key/recovery_key_cubit.dart index 6b120d0e..032fd31b 100644 --- a/lib/logic/cubit/recovery_key/recovery_key_cubit.dart +++ b/lib/logic/cubit/recovery_key/recovery_key_cubit.dart @@ -7,20 +7,20 @@ part 'recovery_key_state.dart'; class RecoveryKeyCubit extends ServerInstallationDependendCubit { - RecoveryKeyCubit(ServerInstallationCubit serverInstallationCubit) + RecoveryKeyCubit(final ServerInstallationCubit serverInstallationCubit) : super(serverInstallationCubit, const RecoveryKeyState.initial()); - final api = ServerApi(); + final ServerApi api = ServerApi(); @override void load() async { if (serverInstallationCubit.state is ServerInstallationFinished) { - final status = await _getRecoveryKeyStatus(); + final RecoveryKeyStatus? status = await _getRecoveryKeyStatus(); if (status == null) { emit(state.copyWith(loadingStatus: LoadingStatus.error)); } else { emit(state.copyWith( - status: status, loadingStatus: LoadingStatus.success)); + status: status, loadingStatus: LoadingStatus.success,),); } } else { emit(state.copyWith(loadingStatus: LoadingStatus.uninitialized)); @@ -39,18 +39,18 @@ class RecoveryKeyCubit Future refresh() async { emit(state.copyWith(loadingStatus: LoadingStatus.refreshing)); - final status = await _getRecoveryKeyStatus(); + final RecoveryKeyStatus? status = await _getRecoveryKeyStatus(); if (status == null) { emit(state.copyWith(loadingStatus: LoadingStatus.error)); } else { emit( - state.copyWith(status: status, loadingStatus: LoadingStatus.success)); + state.copyWith(status: status, loadingStatus: LoadingStatus.success),); } } Future generateRecoveryKey({ - DateTime? expirationDate, - int? numberOfUses, + final DateTime? expirationDate, + final int? numberOfUses, }) async { final ApiResponse response = await api.generateRecoveryToken(expirationDate, numberOfUses); @@ -69,7 +69,7 @@ class RecoveryKeyCubit } class GenerationError extends Error { - final String message; GenerationError(this.message); + final String message; } diff --git a/lib/logic/cubit/recovery_key/recovery_key_state.dart b/lib/logic/cubit/recovery_key/recovery_key_state.dart index f5eb1090..bd6664ef 100644 --- a/lib/logic/cubit/recovery_key/recovery_key_state.dart +++ b/lib/logic/cubit/recovery_key/recovery_key_state.dart @@ -1,3 +1,5 @@ +// ignore_for_file: always_specify_types + part of 'recovery_key_cubit.dart'; class RecoveryKeyState extends ServerInstallationDependendState { @@ -5,7 +7,7 @@ class RecoveryKeyState extends ServerInstallationDependendState { const RecoveryKeyState.initial() : this(const RecoveryKeyStatus(exists: false, valid: false), - LoadingStatus.refreshing); + LoadingStatus.refreshing,); final RecoveryKeyStatus _status; final LoadingStatus loadingStatus; @@ -19,12 +21,10 @@ class RecoveryKeyState extends ServerInstallationDependendState { List get props => [_status, loadingStatus]; RecoveryKeyState copyWith({ - RecoveryKeyStatus? status, - LoadingStatus? loadingStatus, - }) { - return RecoveryKeyState( + final RecoveryKeyStatus? status, + final LoadingStatus? loadingStatus, + }) => RecoveryKeyState( status ?? _status, loadingStatus ?? this.loadingStatus, ); - } } diff --git a/lib/logic/cubit/server_detailed_info/server_detailed_info_cubit.dart b/lib/logic/cubit/server_detailed_info/server_detailed_info_cubit.dart index dea0081a..ac5cbec5 100644 --- a/lib/logic/cubit/server_detailed_info/server_detailed_info_cubit.dart +++ b/lib/logic/cubit/server_detailed_info/server_detailed_info_cubit.dart @@ -14,16 +14,16 @@ class ServerDetailsCubit extends Cubit { ServerDetailsRepository repository = ServerDetailsRepository(); void check() async { - var isReadyToCheck = getIt().serverDetails != null; + final bool isReadyToCheck = getIt().serverDetails != null; if (isReadyToCheck) { emit(ServerDetailsLoading()); - var data = await repository.load(); + final ServerDetailsRepositoryDto data = await repository.load(); emit(Loaded( serverInfo: data.hetznerServerInfo, autoUpgradeSettings: data.autoUpgradeSettings, serverTimezone: data.serverTimezone, checkTime: DateTime.now(), - )); + ),); } else { emit(ServerDetailsNotReady()); } diff --git a/lib/logic/cubit/server_detailed_info/server_detailed_info_repository.dart b/lib/logic/cubit/server_detailed_info/server_detailed_info_repository.dart index 842fa527..00bd9ec4 100644 --- a/lib/logic/cubit/server_detailed_info/server_detailed_info_repository.dart +++ b/lib/logic/cubit/server_detailed_info/server_detailed_info_repository.dart @@ -5,8 +5,8 @@ import 'package:selfprivacy/logic/models/json/hetzner_server_info.dart'; import 'package:selfprivacy/logic/models/timezone_settings.dart'; class ServerDetailsRepository { - var hetznerAPi = HetznerApi(); - var selfprivacyServer = ServerApi(); + HetznerApi hetznerAPi = HetznerApi(); + ServerApi selfprivacyServer = ServerApi(); Future load() async { print('load'); @@ -19,15 +19,15 @@ class ServerDetailsRepository { } class ServerDetailsRepositoryDto { - final HetznerServerInfo hetznerServerInfo; - - final TimeZoneSettings serverTimezone; - - final AutoUpgradeSettings autoUpgradeSettings; ServerDetailsRepositoryDto({ required this.hetznerServerInfo, required this.serverTimezone, required this.autoUpgradeSettings, }); + final HetznerServerInfo hetznerServerInfo; + + final TimeZoneSettings serverTimezone; + + final AutoUpgradeSettings autoUpgradeSettings; } diff --git a/lib/logic/cubit/server_detailed_info/server_detailed_info_state.dart b/lib/logic/cubit/server_detailed_info/server_detailed_info_state.dart index 034d2a47..6a984328 100644 --- a/lib/logic/cubit/server_detailed_info/server_detailed_info_state.dart +++ b/lib/logic/cubit/server_detailed_info/server_detailed_info_state.dart @@ -1,3 +1,5 @@ +// ignore_for_file: always_specify_types + part of 'server_detailed_info_cubit.dart'; abstract class ServerDetailsState extends Equatable { @@ -16,12 +18,6 @@ class ServerDetailsNotReady extends ServerDetailsState {} class Loading extends ServerDetailsState {} class Loaded extends ServerDetailsState { - final HetznerServerInfo serverInfo; - - final TimeZoneSettings serverTimezone; - - final AutoUpgradeSettings autoUpgradeSettings; - final DateTime checkTime; const Loaded({ required this.serverInfo, @@ -29,6 +25,12 @@ class Loaded extends ServerDetailsState { required this.autoUpgradeSettings, required this.checkTime, }); + final HetznerServerInfo serverInfo; + + final TimeZoneSettings serverTimezone; + + final AutoUpgradeSettings autoUpgradeSettings; + final DateTime checkTime; @override List get props => [ diff --git a/lib/logic/cubit/server_installation/server_installation_cubit.dart b/lib/logic/cubit/server_installation/server_installation_cubit.dart index 18367eb0..3f156e4b 100644 --- a/lib/logic/cubit/server_installation/server_installation_cubit.dart +++ b/lib/logic/cubit/server_installation/server_installation_cubit.dart @@ -10,7 +10,7 @@ import 'package:selfprivacy/logic/models/hive/server_domain.dart'; import 'package:selfprivacy/logic/models/hive/user.dart'; import 'package:selfprivacy/logic/models/server_basic_info.dart'; -import '../server_installation/server_installation_repository.dart'; +import 'package:selfprivacy/logic/cubit/server_installation/server_installation_repository.dart'; export 'package:provider/provider.dart'; @@ -19,12 +19,13 @@ part '../server_installation/server_installation_state.dart'; class ServerInstallationCubit extends Cubit { ServerInstallationCubit() : super(const ServerInstallationEmpty()); - final repository = ServerInstallationRepository(); + final ServerInstallationRepository repository = + ServerInstallationRepository(); Timer? timer; Future load() async { - var state = await repository.load(); + final ServerInstallationState state = await repository.load(); if (state is ServerInstallationFinished) { emit(state); @@ -48,33 +49,38 @@ class ServerInstallationCubit extends Cubit { } } - void setHetznerKey(String hetznerKey) async { + void setHetznerKey(final String hetznerKey) async { await repository.saveHetznerKey(hetznerKey); if (state is ServerInstallationRecovery) { - emit((state as ServerInstallationRecovery).copyWith( - hetznerKey: hetznerKey, - currentStep: RecoveryStep.serverSelection, - )); + emit( + (state as ServerInstallationRecovery).copyWith( + hetznerKey: hetznerKey, + currentStep: RecoveryStep.serverSelection, + ), + ); return; } - emit((state as ServerInstallationNotFinished) - .copyWith(hetznerKey: hetznerKey)); + emit( + (state as ServerInstallationNotFinished).copyWith(hetznerKey: hetznerKey), + ); } - void setCloudflareKey(String cloudFlareKey) async { + void setCloudflareKey(final String cloudFlareKey) async { if (state is ServerInstallationRecovery) { setAndValidateCloudflareToken(cloudFlareKey); return; } await repository.saveCloudFlareKey(cloudFlareKey); - emit((state as ServerInstallationNotFinished) - .copyWith(cloudFlareKey: cloudFlareKey)); + emit( + (state as ServerInstallationNotFinished) + .copyWith(cloudFlareKey: cloudFlareKey), + ); } - void setBackblazeKey(String keyId, String applicationKey) async { - var backblazeCredential = BackblazeCredential( + void setBackblazeKey(final String keyId, final String applicationKey) async { + final BackblazeCredential backblazeCredential = BackblazeCredential( keyId: keyId, applicationKey: applicationKey, ); @@ -83,38 +89,45 @@ class ServerInstallationCubit extends Cubit { finishRecoveryProcess(backblazeCredential); return; } - emit((state as ServerInstallationNotFinished) - .copyWith(backblazeCredential: backblazeCredential)); + emit( + (state as ServerInstallationNotFinished) + .copyWith(backblazeCredential: backblazeCredential), + ); } - void setDomain(ServerDomain serverDomain) async { + void setDomain(final ServerDomain serverDomain) async { await repository.saveDomain(serverDomain); - emit((state as ServerInstallationNotFinished) - .copyWith(serverDomain: serverDomain)); + emit( + (state as ServerInstallationNotFinished) + .copyWith(serverDomain: serverDomain), + ); } - void setRootUser(User rootUser) async { + void setRootUser(final User rootUser) async { await repository.saveRootUser(rootUser); emit((state as ServerInstallationNotFinished).copyWith(rootUser: rootUser)); } void createServerAndSetDnsRecords() async { - ServerInstallationNotFinished stateCopy = + final ServerInstallationNotFinished stateCopy = state as ServerInstallationNotFinished; - onCancel() => emit( - (state as ServerInstallationNotFinished).copyWith(isLoading: false)); + void onCancel() => emit( + (state as ServerInstallationNotFinished).copyWith(isLoading: false), + ); - onSuccess(ServerHostingDetails serverDetails) async { + Future onSuccess(final ServerHostingDetails serverDetails) async { await repository.createDnsRecords( serverDetails.ip4, state.serverDomain!, onCancel: onCancel, ); - emit((state as ServerInstallationNotFinished).copyWith( - isLoading: false, - serverDetails: serverDetails, - )); + emit( + (state as ServerInstallationNotFinished).copyWith( + isLoading: false, + serverDetails: serverDetails, + ), + ); runDelayed(startServerIfDnsIsOkay, const Duration(seconds: 30), null); } @@ -133,125 +146,149 @@ class ServerInstallationCubit extends Cubit { } } - void startServerIfDnsIsOkay({ServerInstallationNotFinished? state}) async { - final dataState = state ?? this.state as ServerInstallationNotFinished; + void startServerIfDnsIsOkay( + {final ServerInstallationNotFinished? state,}) async { + final ServerInstallationNotFinished dataState = + state ?? this.state as ServerInstallationNotFinished; emit(TimerState(dataState: dataState, isLoading: true)); - var ip4 = dataState.serverDetails!.ip4; - var domainName = dataState.serverDomain!.domainName; + final String ip4 = dataState.serverDetails!.ip4; + final String domainName = dataState.serverDomain!.domainName; - var matches = await repository.isDnsAddressesMatch( - domainName, ip4, dataState.dnsMatches); + final Map matches = await repository.isDnsAddressesMatch( + domainName, + ip4, + dataState.dnsMatches, + ); - if (matches.values.every((value) => value)) { - var server = await repository.startServer( + if (matches.values.every((final bool value) => value)) { + final ServerHostingDetails server = await repository.startServer( dataState.serverDetails!, ); await repository.saveServerDetails(server); await repository.saveIsServerStarted(true); - emit( - dataState.copyWith( - isServerStarted: true, - isLoading: false, - serverDetails: server, - ), + final ServerInstallationNotFinished newState = dataState.copyWith( + isServerStarted: true, + isLoading: false, + serverDetails: server, ); + emit(newState); runDelayed( - resetServerIfServerIsOkay, const Duration(seconds: 60), dataState); + resetServerIfServerIsOkay, + const Duration(seconds: 60), + newState, + ); } else { - emit( - dataState.copyWith( - isLoading: false, - dnsMatches: matches, - ), + final ServerInstallationNotFinished newState = dataState.copyWith( + isLoading: false, + dnsMatches: matches, ); + emit(newState); runDelayed( - startServerIfDnsIsOkay, const Duration(seconds: 30), dataState); + startServerIfDnsIsOkay, + const Duration(seconds: 30), + newState, + ); } } - void oneMoreReset({ServerInstallationNotFinished? state}) async { - final dataState = state ?? this.state as ServerInstallationNotFinished; + void resetServerIfServerIsOkay({ + final ServerInstallationNotFinished? state, + }) async { + final ServerInstallationNotFinished dataState = + state ?? this.state as ServerInstallationNotFinished; emit(TimerState(dataState: dataState, isLoading: true)); - var isServerWorking = await repository.isHttpServerWorking(); + final bool isServerWorking = await repository.isHttpServerWorking(); if (isServerWorking) { - var pauseDuration = const Duration(seconds: 30); - emit(TimerState( - dataState: dataState, - timerStart: DateTime.now(), - isLoading: false, - duration: pauseDuration, - )); + const Duration pauseDuration = Duration(seconds: 30); + emit( + TimerState( + dataState: dataState, + timerStart: DateTime.now(), + isLoading: false, + duration: pauseDuration, + ), + ); timer = Timer(pauseDuration, () async { - var hetznerServerDetails = await repository.restart(); + final ServerHostingDetails hetznerServerDetails = + await repository.restart(); + await repository.saveIsServerResetedFirstTime(true); + await repository.saveServerDetails(hetznerServerDetails); + + final ServerInstallationNotFinished newState = dataState.copyWith( + isServerResetedFirstTime: true, + serverDetails: hetznerServerDetails, + isLoading: false, + ); + + emit(newState); + runDelayed(oneMoreReset, const Duration(seconds: 60), newState); + }); + } else { + runDelayed( + resetServerIfServerIsOkay, + const Duration(seconds: 60), + dataState, + ); + } + } + + void oneMoreReset({final ServerInstallationNotFinished? state}) async { + final ServerInstallationNotFinished dataState = + state ?? this.state as ServerInstallationNotFinished; + + emit(TimerState(dataState: dataState, isLoading: true)); + + final bool isServerWorking = await repository.isHttpServerWorking(); + + if (isServerWorking) { + const Duration pauseDuration = Duration(seconds: 30); + emit( + TimerState( + dataState: dataState, + timerStart: DateTime.now(), + isLoading: false, + duration: pauseDuration, + ), + ); + timer = Timer(pauseDuration, () async { + final ServerHostingDetails hetznerServerDetails = + await repository.restart(); await repository.saveIsServerResetedSecondTime(true); await repository.saveServerDetails(hetznerServerDetails); - emit( - dataState.copyWith( - isServerResetedSecondTime: true, - serverDetails: hetznerServerDetails, - isLoading: false, - ), + final ServerInstallationNotFinished newState = dataState.copyWith( + isServerResetedSecondTime: true, + serverDetails: hetznerServerDetails, + isLoading: false, ); + + emit(newState); runDelayed( - finishCheckIfServerIsOkay, const Duration(seconds: 60), dataState); + finishCheckIfServerIsOkay, + const Duration(seconds: 60), + newState, + ); }); } else { runDelayed(oneMoreReset, const Duration(seconds: 60), dataState); } } - void resetServerIfServerIsOkay({ - ServerInstallationNotFinished? state, - }) async { - final dataState = state ?? this.state as ServerInstallationNotFinished; - - emit(TimerState(dataState: dataState, isLoading: true)); - - var isServerWorking = await repository.isHttpServerWorking(); - - if (isServerWorking) { - var pauseDuration = const Duration(seconds: 30); - emit(TimerState( - dataState: dataState, - timerStart: DateTime.now(), - isLoading: false, - duration: pauseDuration, - )); - timer = Timer(pauseDuration, () async { - var hetznerServerDetails = await repository.restart(); - await repository.saveIsServerResetedFirstTime(true); - await repository.saveServerDetails(hetznerServerDetails); - - emit( - dataState.copyWith( - isServerResetedFirstTime: true, - serverDetails: hetznerServerDetails, - isLoading: false, - ), - ); - runDelayed(oneMoreReset, const Duration(seconds: 60), dataState); - }); - } else { - runDelayed( - resetServerIfServerIsOkay, const Duration(seconds: 60), dataState); - } - } - void finishCheckIfServerIsOkay({ - ServerInstallationNotFinished? state, + final ServerInstallationNotFinished? state, }) async { - final dataState = state ?? this.state as ServerInstallationNotFinished; + final ServerInstallationNotFinished dataState = + state ?? this.state as ServerInstallationNotFinished; emit(TimerState(dataState: dataState, isLoading: true)); - var isServerWorking = await repository.isHttpServerWorking(); + final bool isServerWorking = await repository.isHttpServerWorking(); if (isServerWorking) { await repository.createDkimRecord(dataState.serverDomain!); @@ -260,51 +297,67 @@ class ServerInstallationCubit extends Cubit { emit(dataState.finish()); } else { runDelayed( - finishCheckIfServerIsOkay, const Duration(seconds: 60), dataState); + finishCheckIfServerIsOkay, + const Duration(seconds: 60), + dataState, + ); } } - void runDelayed(void Function() work, Duration delay, - ServerInstallationNotFinished? state) async { - final dataState = state ?? this.state as ServerInstallationNotFinished; + void runDelayed( + final void Function() work, + final Duration delay, + final ServerInstallationNotFinished? state, + ) async { + final ServerInstallationNotFinished dataState = + state ?? this.state as ServerInstallationNotFinished; - emit(TimerState( - dataState: dataState, - timerStart: DateTime.now(), - duration: delay, - isLoading: false, - )); + emit( + TimerState( + dataState: dataState, + timerStart: DateTime.now(), + duration: delay, + isLoading: false, + ), + ); timer = Timer(delay, work); } - void submitDomainForAccessRecovery(String domain) async { - var serverDomain = ServerDomain( + void submitDomainForAccessRecovery(final String domain) async { + final ServerDomain serverDomain = ServerDomain( domainName: domain, provider: DnsProvider.unknown, zoneId: '', ); - final recoveryCapabilities = + final ServerRecoveryCapabilities recoveryCapabilities = await repository.getRecoveryCapabilities(serverDomain); await repository.saveDomain(serverDomain); await repository.saveIsRecoveringServer(true); - emit(ServerInstallationRecovery( - serverDomain: serverDomain, - recoveryCapabilities: recoveryCapabilities, - currentStep: RecoveryStep.selecting, - )); + emit( + ServerInstallationRecovery( + serverDomain: serverDomain, + recoveryCapabilities: recoveryCapabilities, + currentStep: RecoveryStep.selecting, + ), + ); } - void tryToRecover(String token, ServerRecoveryMethods method) async { - final dataState = state as ServerInstallationRecovery; - final serverDomain = dataState.serverDomain; + void tryToRecover( + final String token, final ServerRecoveryMethods method,) async { + final ServerInstallationRecovery dataState = + state as ServerInstallationRecovery; + final ServerDomain? serverDomain = dataState.serverDomain; if (serverDomain == null) { return; } try { Future Function( - ServerDomain, String, ServerRecoveryCapabilities) recoveryFunction; + ServerDomain, + String, + ServerRecoveryCapabilities, + ) recoveryFunction; switch (method) { case ServerRecoveryMethods.newDeviceKey: recoveryFunction = repository.authorizeByNewDeviceKey; @@ -318,16 +371,18 @@ class ServerInstallationCubit extends Cubit { default: throw Exception('Unknown recovery method'); } - final serverDetails = await recoveryFunction( + final ServerHostingDetails serverDetails = await recoveryFunction( serverDomain, token, dataState.recoveryCapabilities, ); await repository.saveServerDetails(serverDetails); - emit(dataState.copyWith( - serverDetails: serverDetails, - currentStep: RecoveryStep.hetznerToken, - )); + emit( + dataState.copyWith( + serverDetails: serverDetails, + currentStep: RecoveryStep.hetznerToken, + ), + ); } on ServerAuthorizationException { getIt() .showSnackBar('recovering.authorization_failed'.tr()); @@ -340,7 +395,8 @@ class ServerInstallationCubit extends Cubit { } void revertRecoveryStep() { - final dataState = state as ServerInstallationRecovery; + final ServerInstallationRecovery dataState = + state as ServerInstallationRecovery; switch (dataState.currentStep) { case RecoveryStep.selecting: repository.deleteDomain(); @@ -349,15 +405,19 @@ class ServerInstallationCubit extends Cubit { case RecoveryStep.recoveryKey: case RecoveryStep.newDeviceKey: case RecoveryStep.oldToken: - emit(dataState.copyWith( - currentStep: RecoveryStep.selecting, - )); + emit( + dataState.copyWith( + currentStep: RecoveryStep.selecting, + ), + ); break; case RecoveryStep.serverSelection: repository.deleteHetznerKey(); - emit(dataState.copyWith( - currentStep: RecoveryStep.hetznerToken, - )); + emit( + dataState.copyWith( + currentStep: RecoveryStep.hetznerToken, + ), + ); break; // We won't revert steps after client is authorized default: @@ -365,48 +425,60 @@ class ServerInstallationCubit extends Cubit { } } - void selectRecoveryMethod(ServerRecoveryMethods method) { - final dataState = state as ServerInstallationRecovery; + void selectRecoveryMethod(final ServerRecoveryMethods method) { + final ServerInstallationRecovery dataState = + state as ServerInstallationRecovery; switch (method) { case ServerRecoveryMethods.newDeviceKey: - emit(dataState.copyWith( - currentStep: RecoveryStep.newDeviceKey, - )); + emit( + dataState.copyWith( + currentStep: RecoveryStep.newDeviceKey, + ), + ); break; case ServerRecoveryMethods.recoveryKey: - emit(dataState.copyWith( - currentStep: RecoveryStep.recoveryKey, - )); + emit( + dataState.copyWith( + currentStep: RecoveryStep.recoveryKey, + ), + ); break; case ServerRecoveryMethods.oldToken: - emit(dataState.copyWith( - currentStep: RecoveryStep.oldToken, - )); + emit( + dataState.copyWith( + currentStep: RecoveryStep.oldToken, + ), + ); break; } } Future> getServersOnHetznerAccount() async { - final dataState = state as ServerInstallationRecovery; - final servers = await repository.getServersOnHetznerAccount(); - final validated = servers - .map((server) => ServerBasicInfoWithValidators.fromServerBasicInfo( - serverBasicInfo: server, - isIpValid: server.ip == dataState.serverDetails?.ip4, - isReverseDnsValid: - server.reverseDns == dataState.serverDomain?.domainName, - )); + final ServerInstallationRecovery dataState = + state as ServerInstallationRecovery; + final List servers = + await repository.getServersOnHetznerAccount(); + final Iterable validated = servers.map( + (final ServerBasicInfo server) => + ServerBasicInfoWithValidators.fromServerBasicInfo( + serverBasicInfo: server, + isIpValid: server.ip == dataState.serverDetails?.ip4, + isReverseDnsValid: + server.reverseDns == dataState.serverDomain?.domainName, + ), + ); return validated.toList(); } - Future setServerId(ServerBasicInfo server) async { - final dataState = state as ServerInstallationRecovery; - final serverDomain = dataState.serverDomain; + Future setServerId(final ServerBasicInfo server) async { + final ServerInstallationRecovery dataState = + state as ServerInstallationRecovery; + final ServerDomain? serverDomain = dataState.serverDomain; if (serverDomain == null) { return; } - final serverDetails = ServerHostingDetails( + final ServerHostingDetails serverDetails = ServerHostingDetails( ip4: server.ip, id: server.id, createTime: server.created, @@ -419,50 +491,60 @@ class ServerInstallationCubit extends Cubit { ); await repository.saveDomain(serverDomain); await repository.saveServerDetails(serverDetails); - emit(dataState.copyWith( - serverDetails: serverDetails, - currentStep: RecoveryStep.cloudflareToken, - )); + emit( + dataState.copyWith( + serverDetails: serverDetails, + currentStep: RecoveryStep.cloudflareToken, + ), + ); } - Future setAndValidateCloudflareToken(String token) async { - final dataState = state as ServerInstallationRecovery; - final serverDomain = dataState.serverDomain; + Future setAndValidateCloudflareToken(final String token) async { + final ServerInstallationRecovery dataState = + state as ServerInstallationRecovery; + final ServerDomain? serverDomain = dataState.serverDomain; if (serverDomain == null) { return; } - final zoneId = await repository.getDomainId(token, serverDomain.domainName); + final String? zoneId = + await repository.getDomainId(token, serverDomain.domainName); if (zoneId == null) { getIt() .showSnackBar('recovering.domain_not_available_on_token'.tr()); return; } - await repository.saveDomain(ServerDomain( - domainName: serverDomain.domainName, - zoneId: zoneId, - provider: DnsProvider.cloudflare, - )); - await repository.saveCloudFlareKey(token); - emit(dataState.copyWith( - serverDomain: ServerDomain( + await repository.saveDomain( + ServerDomain( domainName: serverDomain.domainName, zoneId: zoneId, provider: DnsProvider.cloudflare, ), - cloudFlareKey: token, - currentStep: RecoveryStep.backblazeToken, - )); + ); + await repository.saveCloudFlareKey(token); + emit( + dataState.copyWith( + serverDomain: ServerDomain( + domainName: serverDomain.domainName, + zoneId: zoneId, + provider: DnsProvider.cloudflare, + ), + cloudFlareKey: token, + currentStep: RecoveryStep.backblazeToken, + ), + ); } - void finishRecoveryProcess(BackblazeCredential backblazeCredential) async { + void finishRecoveryProcess( + final BackblazeCredential backblazeCredential,) async { await repository.saveIsServerStarted(true); await repository.saveIsServerResetedFirstTime(true); await repository.saveIsServerResetedSecondTime(true); await repository.saveHasFinalChecked(true); await repository.saveIsRecoveringServer(false); - final mainUser = await repository.getMainUser(); + final User mainUser = await repository.getMainUser(); await repository.saveRootUser(mainUser); - final updatedState = (state as ServerInstallationRecovery).copyWith( + final ServerInstallationRecovery updatedState = + (state as ServerInstallationRecovery).copyWith( backblazeCredential: backblazeCredential, rootUser: mainUser, ); @@ -470,7 +552,7 @@ class ServerInstallationCubit extends Cubit { } @override - void onChange(Change change) { + void onChange(final Change change) { super.onChange(change); print('================================'); print('ServerInstallationState changed!'); @@ -481,9 +563,11 @@ class ServerInstallationCubit extends Cubit { print('BackblazeCredential: ${change.nextState.backblazeCredential}'); if (change.nextState is ServerInstallationRecovery) { print( - 'Recovery Step: ${(change.nextState as ServerInstallationRecovery).currentStep}'); + 'Recovery Step: ${(change.nextState as ServerInstallationRecovery).currentStep}', + ); print( - 'Recovery Capabilities: ${(change.nextState as ServerInstallationRecovery).recoveryCapabilities}'); + 'Recovery Capabilities: ${(change.nextState as ServerInstallationRecovery).recoveryCapabilities}', + ); } if (change.nextState is TimerState) { print('Timer: ${(change.nextState as TimerState).duration}'); @@ -504,23 +588,25 @@ class ServerInstallationCubit extends Cubit { await repository.deleteServer(state.serverDomain!); } await repository.deleteServerRelatedRecords(); - emit(ServerInstallationNotFinished( - hetznerKey: state.hetznerKey, - serverDomain: state.serverDomain, - cloudFlareKey: state.cloudFlareKey, - backblazeCredential: state.backblazeCredential, - rootUser: state.rootUser, - serverDetails: null, - isServerStarted: false, - isServerResetedFirstTime: false, - isServerResetedSecondTime: false, - isLoading: false, - dnsMatches: null, - )); + emit( + ServerInstallationNotFinished( + hetznerKey: state.hetznerKey, + serverDomain: state.serverDomain, + cloudFlareKey: state.cloudFlareKey, + backblazeCredential: state.backblazeCredential, + rootUser: state.rootUser, + serverDetails: null, + isServerStarted: false, + isServerResetedFirstTime: false, + isServerResetedSecondTime: false, + isLoading: false, + dnsMatches: null, + ), + ); } @override - close() { + Future close() { closeTimer(); return super.close(); } diff --git a/lib/logic/cubit/server_installation/server_installation_repository.dart b/lib/logic/cubit/server_installation/server_installation_repository.dart index 3152c39a..075e357b 100644 --- a/lib/logic/cubit/server_installation/server_installation_repository.dart +++ b/lib/logic/cubit/server_installation/server_installation_repository.dart @@ -1,3 +1,5 @@ +// ignore_for_file: always_specify_types + import 'dart:io'; import 'package:basic_utils/basic_utils.dart'; @@ -12,39 +14,41 @@ import 'package:selfprivacy/config/hive_config.dart'; import 'package:selfprivacy/logic/api_maps/cloudflare.dart'; import 'package:selfprivacy/logic/api_maps/hetzner.dart'; import 'package:selfprivacy/logic/api_maps/server.dart'; +import 'package:selfprivacy/logic/common_enum/common_enum.dart'; import 'package:selfprivacy/logic/models/hive/backblaze_credential.dart'; import 'package:selfprivacy/logic/models/hive/server_details.dart'; import 'package:selfprivacy/logic/models/hive/server_domain.dart'; import 'package:selfprivacy/logic/models/hive/user.dart'; import 'package:selfprivacy/logic/models/json/device_token.dart'; +import 'package:selfprivacy/logic/models/json/hetzner_server_info.dart'; import 'package:selfprivacy/logic/models/message.dart'; import 'package:selfprivacy/logic/models/server_basic_info.dart'; import 'package:selfprivacy/ui/components/action_button/action_button.dart'; import 'package:selfprivacy/ui/components/brand_alert/brand_alert.dart'; -import '../server_installation/server_installation_cubit.dart'; +import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; class IpNotFoundException implements Exception { - final String message; IpNotFoundException(this.message); + final String message; } class ServerAuthorizationException implements Exception { - final String message; ServerAuthorizationException(this.message); + final String message; } class ServerInstallationRepository { Box box = Hive.box(BNames.serverInstallationBox); Future load() async { - final hetznerToken = getIt().hetznerKey; - final cloudflareToken = getIt().cloudFlareKey; - final serverDomain = getIt().serverDomain; - final backblazeCredential = getIt().backblazeCredential; - final serverDetails = getIt().serverDetails; + final String? hetznerToken = getIt().hetznerKey; + final String? cloudflareToken = getIt().cloudFlareKey; + final ServerDomain? serverDomain = getIt().serverDomain; + final BackblazeCredential? backblazeCredential = getIt().backblazeCredential; + final ServerHostingDetails? serverDetails = getIt().serverDetails; if (box.get(BNames.hasFinalChecked, defaultValue: false)) { return ServerInstallationFinished( @@ -72,7 +76,7 @@ class ServerInstallationRepository { serverDetails: serverDetails, rootUser: box.get(BNames.rootUser), currentStep: _getCurrentRecoveryStep( - hetznerToken, cloudflareToken, serverDomain, serverDetails), + hetznerToken, cloudflareToken, serverDomain, serverDetails,), recoveryCapabilities: await getRecoveryCapabilities(serverDomain), ); } @@ -95,10 +99,10 @@ class ServerInstallationRepository { } RecoveryStep _getCurrentRecoveryStep( - String? hetznerToken, - String? cloudflareToken, - ServerDomain serverDomain, - ServerHostingDetails? serverDetails, + final String? hetznerToken, + final String? cloudflareToken, + final ServerDomain serverDomain, + final ServerHostingDetails? serverDetails, ) { if (serverDetails != null) { if (hetznerToken != null) { @@ -120,31 +124,31 @@ class ServerInstallationRepository { } Future startServer( - ServerHostingDetails hetznerServer, + final ServerHostingDetails hetznerServer, ) async { - var hetznerApi = HetznerApi(); - var serverDetails = await hetznerApi.powerOn(); + final HetznerApi hetznerApi = HetznerApi(); + final ServerHostingDetails serverDetails = await hetznerApi.powerOn(); return serverDetails; } - Future getDomainId(String token, String domain) async { - var cloudflareApi = CloudflareApi( + Future getDomainId(final String token, final String domain) async { + final CloudflareApi cloudflareApi = CloudflareApi( isWithToken: false, customToken: token, ); try { - final domainId = await cloudflareApi.getZoneId(domain); + final String domainId = await cloudflareApi.getZoneId(domain); return domainId; } on DomainNotFoundException { return null; } } - Future> isDnsAddressesMatch(String? domainName, String? ip4, - Map? skippedMatches) async { - var addresses = [ + Future> isDnsAddressesMatch(final String? domainName, final String? ip4, + final Map? skippedMatches,) async { + final List addresses = [ '$domainName', 'api.$domainName', 'cloud.$domainName', @@ -152,14 +156,14 @@ class ServerInstallationRepository { 'password.$domainName' ]; - var matches = {}; + final Map matches = {}; - for (var address in addresses) { - if (skippedMatches != null && skippedMatches[address] == true) { + for (final String address in addresses) { + if (skippedMatches![address] ?? false) { matches[address] = true; continue; } - var lookupRecordRes = await DnsUtils.lookupRecord( + final List? lookupRecordRes = await DnsUtils.lookupRecord( address, RRecordType.A, provider: DnsApiProvider.CLOUDFLARE, @@ -189,21 +193,21 @@ class ServerInstallationRepository { } Future createServer( - User rootUser, - String domainName, - String cloudFlareKey, - BackblazeCredential backblazeCredential, { - required void Function() onCancel, - required Future Function(ServerHostingDetails serverDetails) + final User rootUser, + final String domainName, + final String cloudFlareKey, + final BackblazeCredential backblazeCredential, { + required final void Function() onCancel, + required final Future Function(ServerHostingDetails serverDetails) onSuccess, }) async { - var hetznerApi = HetznerApi(); + final HetznerApi hetznerApi = HetznerApi(); late ServerVolume dataBase; try { dataBase = await hetznerApi.createVolume(); - var serverDetails = await hetznerApi.createServer( + final ServerHostingDetails serverDetails = await hetznerApi.createServer( cloudFlareKey: cloudFlareKey, rootUser: rootUser, domainName: domainName, @@ -213,7 +217,7 @@ class ServerInstallationRepository { onSuccess(serverDetails); } on DioError catch (e) { if (e.response!.data['error']['code'] == 'uniqueness_error') { - var nav = getIt.get(); + final NavigationService nav = getIt.get(); nav.showPopUpDialog( BrandAlert( title: 'modals.1'.tr(), @@ -224,9 +228,9 @@ class ServerInstallationRepository { isRed: true, onPressed: () async { await hetznerApi.deleteSelfprivacyServerAndAllVolumes( - domainName: domainName); + domainName: domainName,); - var serverDetails = await hetznerApi.createServer( + final ServerHostingDetails serverDetails = await hetznerApi.createServer( cloudFlareKey: cloudFlareKey, rootUser: rootUser, domainName: domainName, @@ -239,9 +243,7 @@ class ServerInstallationRepository { ), ActionButton( text: 'basis.cancel'.tr(), - onPressed: () { - onCancel(); - }, + onPressed: onCancel, ), ], ), @@ -251,11 +253,11 @@ class ServerInstallationRepository { } Future createDnsRecords( - String ip4, - ServerDomain cloudFlareDomain, { - required void Function() onCancel, + final String ip4, + final ServerDomain cloudFlareDomain, { + required final void Function() onCancel, }) async { - var cloudflareApi = CloudflareApi(); + final CloudflareApi cloudflareApi = CloudflareApi(); await cloudflareApi.removeSimilarRecords( ip4: ip4, @@ -268,8 +270,8 @@ class ServerInstallationRepository { cloudFlareDomain: cloudFlareDomain, ); } on DioError catch (e) { - var hetznerApi = HetznerApi(); - var nav = getIt.get(); + final HetznerApi hetznerApi = HetznerApi(); + final NavigationService nav = getIt.get(); nav.showPopUpDialog( BrandAlert( title: e.response!.data['errors'][0]['code'] == 1038 @@ -282,16 +284,14 @@ class ServerInstallationRepository { isRed: true, onPressed: () async { await hetznerApi.deleteSelfprivacyServerAndAllVolumes( - domainName: cloudFlareDomain.domainName); + domainName: cloudFlareDomain.domainName,); onCancel(); }, ), ActionButton( text: 'basis.cancel'.tr(), - onPressed: () { - onCancel(); - }, + onPressed: onCancel, ), ], ), @@ -304,18 +304,18 @@ class ServerInstallationRepository { ); } - Future createDkimRecord(ServerDomain cloudFlareDomain) async { - var cloudflareApi = CloudflareApi(); - var api = ServerApi(); + Future createDkimRecord(final ServerDomain cloudFlareDomain) async { + final CloudflareApi cloudflareApi = CloudflareApi(); + final ServerApi api = ServerApi(); - var dkimRecordString = await api.getDkim(); + final String? dkimRecordString = await api.getDkim(); await cloudflareApi.setDkim(dkimRecordString ?? '', cloudFlareDomain); } Future isHttpServerWorking() async { - var api = ServerApi(); - var isHttpServerWorking = await api.isHttpServerWorking(); + final ServerApi api = ServerApi(); + final bool isHttpServerWorking = await api.isHttpServerWorking(); try { await api.getDkim(); } catch (e) { @@ -325,28 +325,28 @@ class ServerInstallationRepository { } Future restart() async { - var hetznerApi = HetznerApi(); - return await hetznerApi.reset(); + final HetznerApi hetznerApi = HetznerApi(); + return hetznerApi.reset(); } Future powerOn() async { - var hetznerApi = HetznerApi(); - return await hetznerApi.powerOn(); + final HetznerApi hetznerApi = HetznerApi(); + return hetznerApi.powerOn(); } Future getRecoveryCapabilities( - ServerDomain serverDomain, + final ServerDomain serverDomain, ) async { - var serverApi = ServerApi( + final ServerApi serverApi = ServerApi( isWithToken: false, overrideDomain: serverDomain.domainName, ); - final serverApiVersion = await serverApi.getApiVersion(); + final String? serverApiVersion = await serverApi.getApiVersion(); if (serverApiVersion == null) { return ServerRecoveryCapabilities.none; } try { - final parsedVersion = Version.parse(serverApiVersion); + final Version parsedVersion = Version.parse(serverApiVersion); if (!VersionConstraint.parse('>=1.2.0').allows(parsedVersion)) { return ServerRecoveryCapabilities.legacy; } @@ -356,10 +356,10 @@ class ServerInstallationRepository { } } - Future getServerIpFromDomain(ServerDomain serverDomain) async { - final lookup = await DnsUtils.lookupRecord( + Future getServerIpFromDomain(final ServerDomain serverDomain) async { + final List? lookup = await DnsUtils.lookupRecord( serverDomain.domainName, RRecordType.A, - provider: DnsApiProvider.CLOUDFLARE); + provider: DnsApiProvider.CLOUDFLARE,); if (lookup == null || lookup.isEmpty) { throw IpNotFoundException('No IP found for domain $serverDomain'); } @@ -369,39 +369,39 @@ class ServerInstallationRepository { Future getDeviceName() async { final DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); if (kIsWeb) { - return await deviceInfo.webBrowserInfo - .then((value) => '${value.browserName} ${value.platform}'); + return deviceInfo.webBrowserInfo + .then((final WebBrowserInfo value) => '${value.browserName} ${value.platform}'); } else { if (Platform.isAndroid) { - return await deviceInfo.androidInfo - .then((value) => '${value.model} ${value.version.release}'); + return deviceInfo.androidInfo + .then((final AndroidDeviceInfo value) => '${value.model} ${value.version.release}'); } else if (Platform.isIOS) { - return await deviceInfo.iosInfo.then((value) => - '${value.utsname.machine} ${value.systemName} ${value.systemVersion}'); + return deviceInfo.iosInfo.then((final IosDeviceInfo value) => + '${value.utsname.machine} ${value.systemName} ${value.systemVersion}',); } else if (Platform.isLinux) { - return await deviceInfo.linuxInfo.then((value) => value.prettyName); + return deviceInfo.linuxInfo.then((final LinuxDeviceInfo value) => value.prettyName); } else if (Platform.isMacOS) { - return await deviceInfo.macOsInfo - .then((value) => '${value.hostName} ${value.computerName}'); + return deviceInfo.macOsInfo + .then((final MacOsDeviceInfo value) => '${value.hostName} ${value.computerName}'); } else if (Platform.isWindows) { - return await deviceInfo.windowsInfo.then((value) => value.computerName); + return deviceInfo.windowsInfo.then((final WindowsDeviceInfo value) => value.computerName); } } return 'Unidentified'; } Future authorizeByNewDeviceKey( - ServerDomain serverDomain, - String newDeviceKey, - ServerRecoveryCapabilities recoveryCapabilities, + final ServerDomain serverDomain, + final String newDeviceKey, + final ServerRecoveryCapabilities recoveryCapabilities, ) async { - var serverApi = ServerApi( + final ServerApi serverApi = ServerApi( isWithToken: false, overrideDomain: serverDomain.domainName, ); - final serverIp = await getServerIpFromDomain(serverDomain); - final apiResponse = await serverApi.authorizeDevice( - DeviceToken(device: await getDeviceName(), token: newDeviceKey)); + final String serverIp = await getServerIpFromDomain(serverDomain); + final ApiResponse apiResponse = await serverApi.authorizeDevice( + DeviceToken(device: await getDeviceName(), token: newDeviceKey),); if (apiResponse.isSuccess) { return ServerHostingDetails( @@ -424,17 +424,17 @@ class ServerInstallationRepository { } Future authorizeByRecoveryKey( - ServerDomain serverDomain, - String recoveryKey, - ServerRecoveryCapabilities recoveryCapabilities, + final ServerDomain serverDomain, + final String recoveryKey, + final ServerRecoveryCapabilities recoveryCapabilities, ) async { - var serverApi = ServerApi( + final ServerApi serverApi = ServerApi( isWithToken: false, overrideDomain: serverDomain.domainName, ); - final serverIp = await getServerIpFromDomain(serverDomain); - final apiResponse = await serverApi.useRecoveryToken( - DeviceToken(device: await getDeviceName(), token: recoveryKey)); + final String serverIp = await getServerIpFromDomain(serverDomain); + final ApiResponse apiResponse = await serverApi.useRecoveryToken( + DeviceToken(device: await getDeviceName(), token: recoveryKey),); if (apiResponse.isSuccess) { return ServerHostingDetails( @@ -457,18 +457,18 @@ class ServerInstallationRepository { } Future authorizeByApiToken( - ServerDomain serverDomain, - String apiToken, - ServerRecoveryCapabilities recoveryCapabilities, + final ServerDomain serverDomain, + final String apiToken, + final ServerRecoveryCapabilities recoveryCapabilities, ) async { - var serverApi = ServerApi( + final ServerApi serverApi = ServerApi( isWithToken: false, overrideDomain: serverDomain.domainName, customToken: apiToken, ); - final serverIp = await getServerIpFromDomain(serverDomain); + final String serverIp = await getServerIpFromDomain(serverDomain); if (recoveryCapabilities == ServerRecoveryCapabilities.legacy) { - final apiResponse = await serverApi.servicesPowerCheck(); + final Map apiResponse = await serverApi.servicesPowerCheck(); if (apiResponse.isNotEmpty) { return ServerHostingDetails( apiToken: apiToken, @@ -484,13 +484,13 @@ class ServerInstallationRepository { ); } else { throw ServerAuthorizationException( - 'Couldn\'t connect to server with this token', + "Couldn't connect to server with this token", ); } } - final deviceAuthKey = await serverApi.createDeviceToken(); - final apiResponse = await serverApi.authorizeDevice( - DeviceToken(device: await getDeviceName(), token: deviceAuthKey.data)); + final ApiResponse deviceAuthKey = await serverApi.createDeviceToken(); + final ApiResponse apiResponse = await serverApi.authorizeDevice( + DeviceToken(device: await getDeviceName(), token: deviceAuthKey.data),); if (apiResponse.isSuccess) { return ServerHostingDetails( @@ -513,21 +513,21 @@ class ServerInstallationRepository { } Future getMainUser() async { - var serverApi = ServerApi(); - const fallbackUser = User( + final ServerApi serverApi = ServerApi(); + const User fallbackUser = User( isFoundOnServer: false, - note: 'Couldn\'t find main user on server, API is outdated', + note: "Couldn't find main user on server, API is outdated", login: 'UNKNOWN', sshKeys: [], ); - final serverApiVersion = await serverApi.getApiVersion(); - final users = await serverApi.getUsersList(withMainUser: true); + final String? serverApiVersion = await serverApi.getApiVersion(); + final ApiResponse> users = await serverApi.getUsersList(withMainUser: true); if (serverApiVersion == null || !users.isSuccess) { return fallbackUser; } try { - final parsedVersion = Version.parse(serverApiVersion); + final Version parsedVersion = Version.parse(serverApiVersion); if (!VersionConstraint.parse('>=1.2.5').allows(parsedVersion)) { return fallbackUser; } @@ -541,25 +541,25 @@ class ServerInstallationRepository { } Future> getServersOnHetznerAccount() async { - var hetznerApi = HetznerApi(); - final servers = await hetznerApi.getServers(); + final HetznerApi hetznerApi = HetznerApi(); + final List servers = await hetznerApi.getServers(); return servers - .map((server) => ServerBasicInfo( + .map((final HetznerServerInfo server) => ServerBasicInfo( id: server.id, name: server.name, ip: server.publicNet.ipv4.ip, reverseDns: server.publicNet.ipv4.reverseDns, created: server.created, volumeId: server.volumes.isNotEmpty ? server.volumes[0] : 0, - )) + ),) .toList(); } - Future saveServerDetails(ServerHostingDetails serverDetails) async { + Future saveServerDetails(final ServerHostingDetails serverDetails) async { await getIt().storeServerDetails(serverDetails); } - Future saveHetznerKey(String key) async { + Future saveHetznerKey(final String key) async { print('saved'); await getIt().storeHetznerKey(key); } @@ -569,15 +569,15 @@ class ServerInstallationRepository { getIt().init(); } - Future saveBackblazeKey(BackblazeCredential backblazeCredential) async { + Future saveBackblazeKey(final BackblazeCredential backblazeCredential) async { await getIt().storeBackblazeCredential(backblazeCredential); } - Future saveCloudFlareKey(String key) async { + Future saveCloudFlareKey(final String key) async { await getIt().storeCloudFlareKey(key); } - Future saveDomain(ServerDomain serverDomain) async { + Future saveDomain(final ServerDomain serverDomain) async { await getIt().storeServerDomain(serverDomain); } @@ -586,33 +586,33 @@ class ServerInstallationRepository { getIt().init(); } - Future saveIsServerStarted(bool value) async { + Future saveIsServerStarted(final bool value) async { await box.put(BNames.isServerStarted, value); } - Future saveIsServerResetedFirstTime(bool value) async { + Future saveIsServerResetedFirstTime(final bool value) async { await box.put(BNames.isServerResetedFirstTime, value); } - Future saveIsServerResetedSecondTime(bool value) async { + Future saveIsServerResetedSecondTime(final bool value) async { await box.put(BNames.isServerResetedSecondTime, value); } - Future saveRootUser(User rootUser) async { + Future saveRootUser(final User rootUser) async { await box.put(BNames.rootUser, rootUser); } - Future saveIsRecoveringServer(bool value) async { + Future saveIsRecoveringServer(final bool value) async { await box.put(BNames.isRecoveringServer, value); } - Future saveHasFinalChecked(bool value) async { + Future saveHasFinalChecked(final bool value) async { await box.put(BNames.hasFinalChecked, value); } - Future deleteServer(ServerDomain serverDomain) async { - var hetznerApi = HetznerApi(); - var cloudFlare = CloudflareApi(); + Future deleteServer(final ServerDomain serverDomain) async { + final HetznerApi hetznerApi = HetznerApi(); + final CloudflareApi cloudFlare = CloudflareApi(); await hetznerApi.deleteSelfprivacyServerAndAllVolumes( domainName: serverDomain.domainName, diff --git a/lib/logic/cubit/server_installation/server_installation_state.dart b/lib/logic/cubit/server_installation/server_installation_state.dart index e6979ff4..8cd0c2e1 100644 --- a/lib/logic/cubit/server_installation/server_installation_state.dart +++ b/lib/logic/cubit/server_installation/server_installation_state.dart @@ -1,3 +1,5 @@ +// ignore_for_file: always_specify_types + part of '../server_installation/server_installation_cubit.dart'; abstract class ServerInstallationState extends Equatable { @@ -42,9 +44,9 @@ abstract class ServerInstallationState extends Equatable { bool get isUserFilled => rootUser != null; bool get isServerCreated => serverDetails != null; - bool get isFullyInitilized => _fulfilementList.every((el) => el!); + bool get isFullyInitilized => _fulfilementList.every((final el) => el!); ServerSetupProgress get progress => - ServerSetupProgress.values[_fulfilementList.where((el) => el!).length]; + ServerSetupProgress.values[_fulfilementList.where((final el) => el!).length]; int get porgressBar { if (progress.index < 6) { @@ -57,7 +59,7 @@ abstract class ServerInstallationState extends Equatable { } List get _fulfilementList { - var res = [ + final List res = [ isHetznerFilled, isCloudFlareFilled, isBackblazeFilled, @@ -76,9 +78,9 @@ abstract class ServerInstallationState extends Equatable { class TimerState extends ServerInstallationNotFinished { TimerState({ required this.dataState, + required final super.isLoading, this.timerStart, this.duration, - required bool isLoading, }) : super( hetznerKey: dataState.hetznerKey, cloudFlareKey: dataState.cloudFlareKey, @@ -89,7 +91,6 @@ class TimerState extends ServerInstallationNotFinished { isServerStarted: dataState.isServerStarted, isServerResetedFirstTime: dataState.isServerResetedFirstTime, isServerResetedSecondTime: dataState.isServerResetedSecondTime, - isLoading: isLoading, dnsMatches: dataState.dnsMatches, ); @@ -119,32 +120,22 @@ enum ServerSetupProgress { } class ServerInstallationNotFinished extends ServerInstallationState { - final bool isLoading; - final Map? dnsMatches; const ServerInstallationNotFinished({ - String? hetznerKey, - String? cloudFlareKey, - BackblazeCredential? backblazeCredential, - ServerDomain? serverDomain, - User? rootUser, - ServerHostingDetails? serverDetails, - required bool isServerStarted, - required bool isServerResetedFirstTime, - required bool isServerResetedSecondTime, - required this.isLoading, + required final super.isServerStarted, + required final super.isServerResetedFirstTime, + required final super.isServerResetedSecondTime, + required final this.isLoading, required this.dnsMatches, - }) : super( - hetznerKey: hetznerKey, - cloudFlareKey: cloudFlareKey, - backblazeCredential: backblazeCredential, - serverDomain: serverDomain, - rootUser: rootUser, - serverDetails: serverDetails, - isServerStarted: isServerStarted, - isServerResetedFirstTime: isServerResetedFirstTime, - isServerResetedSecondTime: isServerResetedSecondTime, - ); + final super.hetznerKey, + final super.cloudFlareKey, + final super.backblazeCredential, + final super.serverDomain, + final super.rootUser, + final super.serverDetails, + }); + final bool isLoading; + final Map? dnsMatches; @override List get props => [ @@ -161,17 +152,17 @@ class ServerInstallationNotFinished extends ServerInstallationState { ]; ServerInstallationNotFinished copyWith({ - String? hetznerKey, - String? cloudFlareKey, - BackblazeCredential? backblazeCredential, - ServerDomain? serverDomain, - User? rootUser, - ServerHostingDetails? serverDetails, - bool? isServerStarted, - bool? isServerResetedFirstTime, - bool? isServerResetedSecondTime, - bool? isLoading, - Map? dnsMatches, + final String? hetznerKey, + final String? cloudFlareKey, + final BackblazeCredential? backblazeCredential, + final ServerDomain? serverDomain, + final User? rootUser, + final ServerHostingDetails? serverDetails, + final bool? isServerStarted, + final bool? isServerResetedFirstTime, + final bool? isServerResetedSecondTime, + final bool? isLoading, + final Map? dnsMatches, }) => ServerInstallationNotFinished( hetznerKey: hetznerKey ?? this.hetznerKey, @@ -221,26 +212,16 @@ class ServerInstallationEmpty extends ServerInstallationNotFinished { class ServerInstallationFinished extends ServerInstallationState { const ServerInstallationFinished({ - required String hetznerKey, - required String cloudFlareKey, - required BackblazeCredential backblazeCredential, - required ServerDomain serverDomain, - required User rootUser, - required ServerHostingDetails serverDetails, - required bool isServerStarted, - required bool isServerResetedFirstTime, - required bool isServerResetedSecondTime, - }) : super( - hetznerKey: hetznerKey, - cloudFlareKey: cloudFlareKey, - backblazeCredential: backblazeCredential, - serverDomain: serverDomain, - rootUser: rootUser, - serverDetails: serverDetails, - isServerStarted: isServerStarted, - isServerResetedFirstTime: isServerResetedFirstTime, - isServerResetedSecondTime: isServerResetedSecondTime, - ); + required final String super.hetznerKey, + required final String super.cloudFlareKey, + required final BackblazeCredential super.backblazeCredential, + required final ServerDomain super.serverDomain, + required final User super.rootUser, + required final ServerHostingDetails super.serverDetails, + required final super.isServerStarted, + required final super.isServerResetedFirstTime, + required final super.isServerResetedSecondTime, + }); @override List get props => [ @@ -279,29 +260,23 @@ enum ServerRecoveryMethods { } class ServerInstallationRecovery extends ServerInstallationState { - final RecoveryStep currentStep; - final ServerRecoveryCapabilities recoveryCapabilities; const ServerInstallationRecovery({ - String? hetznerKey, - String? cloudFlareKey, - BackblazeCredential? backblazeCredential, - ServerDomain? serverDomain, - User? rootUser, - ServerHostingDetails? serverDetails, required this.currentStep, required this.recoveryCapabilities, + final super.hetznerKey, + final super.cloudFlareKey, + final super.backblazeCredential, + final super.serverDomain, + final super.rootUser, + final super.serverDetails, }) : super( - hetznerKey: hetznerKey, - cloudFlareKey: cloudFlareKey, - backblazeCredential: backblazeCredential, - serverDomain: serverDomain, - rootUser: rootUser, - serverDetails: serverDetails, isServerStarted: true, isServerResetedFirstTime: true, isServerResetedSecondTime: true, ); + final RecoveryStep currentStep; + final ServerRecoveryCapabilities recoveryCapabilities; @override List get props => [ @@ -317,14 +292,14 @@ class ServerInstallationRecovery extends ServerInstallationState { ]; ServerInstallationRecovery copyWith({ - String? hetznerKey, - String? cloudFlareKey, - BackblazeCredential? backblazeCredential, - ServerDomain? serverDomain, - User? rootUser, - ServerHostingDetails? serverDetails, - RecoveryStep? currentStep, - ServerRecoveryCapabilities? recoveryCapabilities, + final String? hetznerKey, + final String? cloudFlareKey, + final BackblazeCredential? backblazeCredential, + final ServerDomain? serverDomain, + final User? rootUser, + final ServerHostingDetails? serverDetails, + final RecoveryStep? currentStep, + final ServerRecoveryCapabilities? recoveryCapabilities, }) => ServerInstallationRecovery( hetznerKey: hetznerKey ?? this.hetznerKey, @@ -337,8 +312,7 @@ class ServerInstallationRecovery extends ServerInstallationState { recoveryCapabilities: recoveryCapabilities ?? this.recoveryCapabilities, ); - ServerInstallationFinished finish() { - return ServerInstallationFinished( + ServerInstallationFinished finish() => ServerInstallationFinished( hetznerKey: hetznerKey!, cloudFlareKey: cloudFlareKey!, backblazeCredential: backblazeCredential!, @@ -349,5 +323,4 @@ class ServerInstallationRecovery extends ServerInstallationState { isServerResetedFirstTime: true, isServerResetedSecondTime: true, ); - } } diff --git a/lib/logic/cubit/services/services_cubit.dart b/lib/logic/cubit/services/services_cubit.dart index 83b22086..f83a2a9a 100644 --- a/lib/logic/cubit/services/services_cubit.dart +++ b/lib/logic/cubit/services/services_cubit.dart @@ -5,13 +5,13 @@ import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_depe part 'services_state.dart'; class ServicesCubit extends ServerInstallationDependendCubit { - ServicesCubit(ServerInstallationCubit serverInstallationCubit) + ServicesCubit(final ServerInstallationCubit serverInstallationCubit) : super(serverInstallationCubit, ServicesState.allOff()); - final api = ServerApi(); + final ServerApi api = ServerApi(); @override Future load() async { if (serverInstallationCubit.state is ServerInstallationFinished) { - var statuses = await api.servicesPowerCheck(); + final Map statuses = await api.servicesPowerCheck(); emit( ServicesState( isPasswordManagerEnable: statuses[ServiceTypes.passwordManager]!, diff --git a/lib/logic/cubit/services/services_state.dart b/lib/logic/cubit/services/services_state.dart index dba7accc..bec81fe1 100644 --- a/lib/logic/cubit/services/services_state.dart +++ b/lib/logic/cubit/services/services_state.dart @@ -1,6 +1,23 @@ +// ignore_for_file: always_specify_types + part of 'services_cubit.dart'; class ServicesState extends ServerInstallationDependendState { + factory ServicesState.allOn() => const ServicesState( + isPasswordManagerEnable: true, + isCloudEnable: true, + isGitEnable: true, + isSocialNetworkEnable: true, + isVpnEnable: true, + ); + + factory ServicesState.allOff() => const ServicesState( + isPasswordManagerEnable: false, + isCloudEnable: false, + isGitEnable: false, + isSocialNetworkEnable: false, + isVpnEnable: false, + ); const ServicesState({ required this.isPasswordManagerEnable, required this.isCloudEnable, @@ -15,23 +32,8 @@ class ServicesState extends ServerInstallationDependendState { final bool isSocialNetworkEnable; final bool isVpnEnable; - factory ServicesState.allOff() => const ServicesState( - isPasswordManagerEnable: false, - isCloudEnable: false, - isGitEnable: false, - isSocialNetworkEnable: false, - isVpnEnable: false, - ); - factory ServicesState.allOn() => const ServicesState( - isPasswordManagerEnable: true, - isCloudEnable: true, - isGitEnable: true, - isSocialNetworkEnable: true, - isVpnEnable: true, - ); - ServicesState enableList( - List list, + final List list, ) => ServicesState( isPasswordManagerEnable: list.contains(ServiceTypes.passwordManager) @@ -48,7 +50,7 @@ class ServicesState extends ServerInstallationDependendState { ); ServicesState disableList( - List list, + final List list, ) => ServicesState( isPasswordManagerEnable: list.contains(ServiceTypes.passwordManager) @@ -74,7 +76,7 @@ class ServicesState extends ServerInstallationDependendState { isVpnEnable ]; - bool isEnableByType(ServiceTypes type) { + bool isEnableByType(final ServiceTypes type) { switch (type) { case ServiceTypes.passwordManager: return isPasswordManagerEnable; diff --git a/lib/logic/cubit/users/users_cubit.dart b/lib/logic/cubit/users/users_cubit.dart index 61d0a789..b96c5683 100644 --- a/lib/logic/cubit/users/users_cubit.dart +++ b/lib/logic/cubit/users/users_cubit.dart @@ -1,78 +1,80 @@ +// ignore_for_file: always_specify_types + import 'package:hive/hive.dart'; import 'package:selfprivacy/config/hive_config.dart'; import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart'; import 'package:selfprivacy/logic/models/hive/user.dart'; -import '../../api_maps/server.dart'; +import 'package:selfprivacy/logic/api_maps/server.dart'; export 'package:provider/provider.dart'; part 'users_state.dart'; class UsersCubit extends ServerInstallationDependendCubit { - UsersCubit(ServerInstallationCubit serverInstallationCubit) + UsersCubit(final ServerInstallationCubit serverInstallationCubit) : super( serverInstallationCubit, const UsersState( - [], User(login: 'root'), User(login: 'loading...'))); + [], User(login: 'root'), User(login: 'loading...'),),); Box box = Hive.box(BNames.usersBox); Box serverInstallationBox = Hive.box(BNames.serverInstallationBox); - final api = ServerApi(); + final ServerApi api = ServerApi(); @override Future load() async { if (serverInstallationCubit.state is ServerInstallationFinished) { - var loadedUsers = box.values.toList(); + final List loadedUsers = box.values.toList(); final primaryUser = serverInstallationBox.get(BNames.rootUser, - defaultValue: const User(login: 'loading...')); - List rootKeys = [ + defaultValue: const User(login: 'loading...'),); + final List rootKeys = [ ...serverInstallationBox.get(BNames.rootKeys, defaultValue: []) ]; if (loadedUsers.isNotEmpty) { emit(UsersState( - loadedUsers, User(login: 'root', sshKeys: rootKeys), primaryUser)); + loadedUsers, User(login: 'root', sshKeys: rootKeys), primaryUser,),); } - final usersFromServer = await api.getUsersList(); + final ApiResponse> usersFromServer = await api.getUsersList(); if (usersFromServer.isSuccess) { - final updatedList = + final List updatedList = mergeLocalAndServerUsers(loadedUsers, usersFromServer.data); emit(UsersState( - updatedList, User(login: 'root', sshKeys: rootKeys), primaryUser)); + updatedList, User(login: 'root', sshKeys: rootKeys), primaryUser,),); } - final usersWithSshKeys = await loadSshKeys(state.users); + final List usersWithSshKeys = await loadSshKeys(state.users); // Update the users it the box box.clear(); box.addAll(usersWithSshKeys); - final rootUserWithSshKeys = (await loadSshKeys([state.rootUser])).first; + final User rootUserWithSshKeys = (await loadSshKeys([state.rootUser])).first; serverInstallationBox.put(BNames.rootKeys, rootUserWithSshKeys.sshKeys); - final primaryUserWithSshKeys = + final User primaryUserWithSshKeys = (await loadSshKeys([state.primaryUser])).first; serverInstallationBox.put(BNames.rootUser, primaryUserWithSshKeys); emit(UsersState( - usersWithSshKeys, rootUserWithSshKeys, primaryUserWithSshKeys)); + usersWithSshKeys, rootUserWithSshKeys, primaryUserWithSshKeys,),); } } List mergeLocalAndServerUsers( - List localUsers, List serverUsers) { + final List localUsers, final List serverUsers,) { // If local user not exists on server, add it with isFoundOnServer = false // If server user not exists on local, add it - List mergedUsers = []; - List serverUsersCopy = List.from(serverUsers); + final List mergedUsers = []; + final List serverUsersCopy = List.from(serverUsers); - for (var localUser in localUsers) { + for (final User localUser in localUsers) { if (serverUsersCopy.contains(localUser.login)) { mergedUsers.add(User( login: localUser.login, isFoundOnServer: true, password: localUser.password, sshKeys: localUser.sshKeys, - )); + ),); serverUsersCopy.remove(localUser.login); } else { mergedUsers.add(User( @@ -80,28 +82,28 @@ class UsersCubit extends ServerInstallationDependendCubit { isFoundOnServer: false, password: localUser.password, note: localUser.note, - )); + ),); } } - for (var serverUser in serverUsersCopy) { + for (final String serverUser in serverUsersCopy) { mergedUsers.add(User( login: serverUser, isFoundOnServer: true, - )); + ),); } return mergedUsers; } - Future> loadSshKeys(List users) async { - List updatedUsers = []; + Future> loadSshKeys(final List users) async { + final List updatedUsers = []; - for (var user in users) { + for (final User user in users) { if (user.isFoundOnServer || user.login == 'root' || user.login == state.primaryUser.login) { - final sshKeys = await api.getUserSshKeys(user); + final ApiResponse> sshKeys = await api.getUserSshKeys(user); print('sshKeys for $user: ${sshKeys.data}'); if (sshKeys.isSuccess) { updatedUsers.add(User( @@ -110,14 +112,14 @@ class UsersCubit extends ServerInstallationDependendCubit { password: user.password, sshKeys: sshKeys.data, note: user.note, - )); + ),); } else { updatedUsers.add(User( login: user.login, isFoundOnServer: true, password: user.password, note: user.note, - )); + ),); } } else { updatedUsers.add(User( @@ -125,7 +127,7 @@ class UsersCubit extends ServerInstallationDependendCubit { isFoundOnServer: false, password: user.password, note: user.note, - )); + ),); } } return updatedUsers; @@ -133,27 +135,27 @@ class UsersCubit extends ServerInstallationDependendCubit { Future refresh() async { List updatedUsers = List.from(state.users); - final usersFromServer = await api.getUsersList(); + final ApiResponse> usersFromServer = await api.getUsersList(); if (usersFromServer.isSuccess) { updatedUsers = mergeLocalAndServerUsers(updatedUsers, usersFromServer.data); } - final usersWithSshKeys = await loadSshKeys(updatedUsers); + final List usersWithSshKeys = await loadSshKeys(updatedUsers); box.clear(); box.addAll(usersWithSshKeys); - final rootUserWithSshKeys = (await loadSshKeys([state.rootUser])).first; + final User rootUserWithSshKeys = (await loadSshKeys([state.rootUser])).first; serverInstallationBox.put(BNames.rootKeys, rootUserWithSshKeys.sshKeys); - final primaryUserWithSshKeys = + final User primaryUserWithSshKeys = (await loadSshKeys([state.primaryUser])).first; serverInstallationBox.put(BNames.rootUser, primaryUserWithSshKeys); emit(UsersState( - usersWithSshKeys, rootUserWithSshKeys, primaryUserWithSshKeys)); + usersWithSshKeys, rootUserWithSshKeys, primaryUserWithSshKeys,),); return; } - Future createUser(User user) async { + Future createUser(final User user) async { // If user exists on server, do nothing - if (state.users.any((u) => u.login == user.login && u.isFoundOnServer)) { + if (state.users.any((final User u) => u.login == user.login && u.isFoundOnServer)) { return; } // If user is root or primary user, do nothing @@ -161,41 +163,41 @@ class UsersCubit extends ServerInstallationDependendCubit { return; } // If API returned error, do nothing - final result = await api.createUser(user); + final ApiResponse result = await api.createUser(user); if (!result.isSuccess) { return; } - var loadedUsers = List.from(state.users); + final List loadedUsers = List.from(state.users); loadedUsers.add(result.data); await box.clear(); await box.addAll(loadedUsers); emit(state.copyWith(users: loadedUsers)); } - Future deleteUser(User user) async { + Future deleteUser(final User user) async { // If user is primary or root, don't delete if (user.login == state.primaryUser.login || user.login == 'root') { return; } - var loadedUsers = List.from(state.users); - final result = await api.deleteUser(user); + final List loadedUsers = List.from(state.users); + final bool result = await api.deleteUser(user); if (result) { - loadedUsers.removeWhere((u) => u.login == user.login); + loadedUsers.removeWhere((final User u) => u.login == user.login); await box.clear(); await box.addAll(loadedUsers); emit(state.copyWith(users: loadedUsers)); } } - Future addSshKey(User user, String publicKey) async { + Future addSshKey(final User user, final String publicKey) async { // If adding root key, use api.addRootSshKey // Otherwise, use api.addUserSshKey if (user.login == 'root') { - final result = await api.addRootSshKey(publicKey); + final ApiResponse result = await api.addRootSshKey(publicKey); if (result.isSuccess) { // Add ssh key to the array of root keys - final rootKeys = serverInstallationBox + final List rootKeys = serverInstallationBox .get(BNames.rootKeys, defaultValue: []) as List; rootKeys.add(publicKey); serverInstallationBox.put(BNames.rootKeys, rootKeys); @@ -207,17 +209,17 @@ class UsersCubit extends ServerInstallationDependendCubit { sshKeys: rootKeys, note: state.rootUser.note, ), - )); + ),); } } else { - final result = await api.addUserSshKey(user, publicKey); + final ApiResponse result = await api.addUserSshKey(user, publicKey); if (result.isSuccess) { // If it is primary user, update primary user if (user.login == state.primaryUser.login) { - List primaryUserKeys = + final List primaryUserKeys = List.from(state.primaryUser.sshKeys); primaryUserKeys.add(publicKey); - final updatedUser = User( + final User updatedUser = User( login: state.primaryUser.login, isFoundOnServer: true, password: state.primaryUser.password, @@ -227,12 +229,12 @@ class UsersCubit extends ServerInstallationDependendCubit { serverInstallationBox.put(BNames.rootUser, updatedUser); emit(state.copyWith( primaryUser: updatedUser, - )); + ),); } else { // If it is not primary user, update user - List userKeys = List.from(user.sshKeys); + final List userKeys = List.from(user.sshKeys); userKeys.add(publicKey); - final updatedUser = User( + final User updatedUser = User( login: user.login, isFoundOnServer: true, password: user.password, @@ -242,23 +244,23 @@ class UsersCubit extends ServerInstallationDependendCubit { await box.putAt(box.values.toList().indexOf(user), updatedUser); emit(state.copyWith( users: box.values.toList(), - )); + ),); } } } } - Future deleteSshKey(User user, String publicKey) async { + Future deleteSshKey(final User user, final String publicKey) async { // All keys are deleted via api.deleteUserSshKey - final result = await api.deleteUserSshKey(user, publicKey); + final ApiResponse result = await api.deleteUserSshKey(user, publicKey); if (result.isSuccess) { // If it is root user, delete key from root keys // If it is primary user, update primary user // If it is not primary user, update user if (user.login == 'root') { - final rootKeys = serverInstallationBox + final List rootKeys = serverInstallationBox .get(BNames.rootKeys, defaultValue: []) as List; rootKeys.remove(publicKey); serverInstallationBox.put(BNames.rootKeys, rootKeys); @@ -270,14 +272,14 @@ class UsersCubit extends ServerInstallationDependendCubit { sshKeys: rootKeys, note: state.rootUser.note, ), - )); + ),); return; } if (user.login == state.primaryUser.login) { - List primaryUserKeys = + final List primaryUserKeys = List.from(state.primaryUser.sshKeys); primaryUserKeys.remove(publicKey); - final updatedUser = User( + final User updatedUser = User( login: state.primaryUser.login, isFoundOnServer: true, password: state.primaryUser.password, @@ -287,12 +289,12 @@ class UsersCubit extends ServerInstallationDependendCubit { serverInstallationBox.put(BNames.rootUser, updatedUser); emit(state.copyWith( primaryUser: updatedUser, - )); + ),); return; } - List userKeys = List.from(user.sshKeys); + final List userKeys = List.from(user.sshKeys); userKeys.remove(publicKey); - final updatedUser = User( + final User updatedUser = User( login: user.login, isFoundOnServer: true, password: user.password, @@ -302,13 +304,13 @@ class UsersCubit extends ServerInstallationDependendCubit { await box.putAt(box.values.toList().indexOf(user), updatedUser); emit(state.copyWith( users: box.values.toList(), - )); + ),); } } @override void clear() async { emit(const UsersState( - [], User(login: 'root'), User(login: 'loading...'))); + [], User(login: 'root'), User(login: 'loading...'),),); } } diff --git a/lib/logic/cubit/users/users_state.dart b/lib/logic/cubit/users/users_state.dart index 4262f26a..a02eb2c1 100644 --- a/lib/logic/cubit/users/users_state.dart +++ b/lib/logic/cubit/users/users_state.dart @@ -1,3 +1,5 @@ +// ignore_for_file: always_specify_types + part of 'users_cubit.dart'; class UsersState extends ServerInstallationDependendState { @@ -11,22 +13,18 @@ class UsersState extends ServerInstallationDependendState { List get props => [users, rootUser, primaryUser]; UsersState copyWith({ - List? users, - User? rootUser, - User? primaryUser, - }) { - return UsersState( + final List? users, + final User? rootUser, + final User? primaryUser, + }) => UsersState( users ?? this.users, rootUser ?? this.rootUser, primaryUser ?? this.primaryUser, ); - } - bool isLoginRegistered(String login) { - return users.any((user) => user.login == login) || + bool isLoginRegistered(final String login) => users.any((final User user) => user.login == login) || login == rootUser.login || login == primaryUser.login; - } bool get isEmpty => users.isEmpty; } diff --git a/lib/logic/get_it/api_config.dart b/lib/logic/get_it/api_config.dart index 3e7ea4b3..6a03e2ef 100644 --- a/lib/logic/get_it/api_config.dart +++ b/lib/logic/get_it/api_config.dart @@ -1,3 +1,5 @@ +// ignore_for_file: always_specify_types + import 'package:hive/hive.dart'; import 'package:selfprivacy/config/hive_config.dart'; import 'package:selfprivacy/logic/models/hive/backblaze_bucket.dart'; @@ -22,38 +24,38 @@ class ApiConfigModel { ServerDomain? _serverDomain; BackblazeBucket? _backblazeBucket; - Future storeHetznerKey(String value) async { + Future storeHetznerKey(final String value) async { await _box.put(BNames.hetznerKey, value); _hetznerKey = value; } - Future storeCloudFlareKey(String value) async { + Future storeCloudFlareKey(final String value) async { await _box.put(BNames.cloudFlareKey, value); _cloudFlareKey = value; } - Future storeBackblazeCredential(BackblazeCredential value) async { + Future storeBackblazeCredential(final BackblazeCredential value) async { await _box.put(BNames.backblazeCredential, value); _backblazeCredential = value; } - Future storeServerDomain(ServerDomain value) async { + Future storeServerDomain(final ServerDomain value) async { await _box.put(BNames.serverDomain, value); _serverDomain = value; } - Future storeServerDetails(ServerHostingDetails value) async { + Future storeServerDetails(final ServerHostingDetails value) async { await _box.put(BNames.serverDetails, value); _serverDetails = value; } - Future storeBackblazeBucket(BackblazeBucket value) async { + Future storeBackblazeBucket(final BackblazeBucket value) async { await _box.put(BNames.backblazeBucket, value); _backblazeBucket = value; } - clear() { + void clear() { _hetznerKey = null; _cloudFlareKey = null; _backblazeCredential = null; diff --git a/lib/logic/get_it/console.dart b/lib/logic/get_it/console.dart index 979895a1..2fc67b7d 100644 --- a/lib/logic/get_it/console.dart +++ b/lib/logic/get_it/console.dart @@ -1,3 +1,5 @@ +// ignore_for_file: always_specify_types + import 'package:flutter/material.dart'; import 'package:selfprivacy/logic/models/message.dart'; @@ -6,7 +8,7 @@ class ConsoleModel extends ChangeNotifier { List get messages => _messages; - void addMessage(Message message) { + void addMessage(final Message message) { messages.add(message); notifyListeners(); } diff --git a/lib/logic/get_it/navigation.dart b/lib/logic/get_it/navigation.dart index 0a235434..15adc982 100644 --- a/lib/logic/get_it/navigation.dart +++ b/lib/logic/get_it/navigation.dart @@ -9,18 +9,18 @@ class NavigationService { NavigatorState? get navigator => navigatorKey.currentState; - void showPopUpDialog(AlertDialog dialog) { - final context = navigatorKey.currentState!.overlay!.context; + void showPopUpDialog(final AlertDialog dialog) { + final BuildContext context = navigatorKey.currentState!.overlay!.context; showDialog( context: context, - builder: (_) => dialog, + builder: (final _) => dialog, ); } - void showSnackBar(String text) { - final state = scaffoldMessengerKey.currentState!; - final snack = SnackBar( + 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), duration: const Duration(seconds: 2), diff --git a/lib/logic/models/hive/backblaze_bucket.dart b/lib/logic/models/hive/backblaze_bucket.dart index 140f2122..ded04b65 100644 --- a/lib/logic/models/hive/backblaze_bucket.dart +++ b/lib/logic/models/hive/backblaze_bucket.dart @@ -8,7 +8,7 @@ class BackblazeBucket { {required this.bucketId, required this.bucketName, required this.applicationKeyId, - required this.applicationKey}); + required this.applicationKey,}); @HiveField(0) final String bucketId; @@ -23,7 +23,5 @@ class BackblazeBucket { final String bucketName; @override - String toString() { - return bucketName; - } + String toString() => bucketName; } diff --git a/lib/logic/models/hive/backblaze_credential.dart b/lib/logic/models/hive/backblaze_credential.dart index 3f0f3ea6..d7bf2d06 100644 --- a/lib/logic/models/hive/backblaze_credential.dart +++ b/lib/logic/models/hive/backblaze_credential.dart @@ -14,16 +14,14 @@ class BackblazeCredential { @HiveField(1) final String applicationKey; - get encodedApiKey => encodedBackblazeKey(keyId, applicationKey); + String get encodedApiKey => encodedBackblazeKey(keyId, applicationKey); @override - String toString() { - return '$keyId: $encodedApiKey'; - } + String toString() => '$keyId: $encodedApiKey'; } -String encodedBackblazeKey(String? keyId, String? applicationKey) { - String apiKey = '$keyId:$applicationKey'; - String encodedApiKey = base64.encode(utf8.encode(apiKey)); +String encodedBackblazeKey(final String? keyId, final String? applicationKey) { + final String apiKey = '$keyId:$applicationKey'; + final String encodedApiKey = base64.encode(utf8.encode(apiKey)); return encodedApiKey; } diff --git a/lib/logic/models/hive/server_details.dart b/lib/logic/models/hive/server_details.dart index a5e5a575..1924aab7 100644 --- a/lib/logic/models/hive/server_details.dart +++ b/lib/logic/models/hive/server_details.dart @@ -35,8 +35,7 @@ class ServerHostingDetails { @HiveField(6, defaultValue: ServerProvider.hetzner) final ServerProvider provider; - ServerHostingDetails copyWith({DateTime? startTime}) { - return ServerHostingDetails( + ServerHostingDetails copyWith({final DateTime? startTime}) => ServerHostingDetails( startTime: startTime ?? this.startTime, createTime: createTime, id: id, @@ -45,7 +44,6 @@ class ServerHostingDetails { apiToken: apiToken, provider: provider, ); - } @override String toString() => id.toString(); diff --git a/lib/logic/models/hive/server_domain.dart b/lib/logic/models/hive/server_domain.dart index 4fdd52c3..9b5d32c1 100644 --- a/lib/logic/models/hive/server_domain.dart +++ b/lib/logic/models/hive/server_domain.dart @@ -20,9 +20,7 @@ class ServerDomain { final DnsProvider provider; @override - String toString() { - return '$domainName: $zoneId'; - } + String toString() => '$domainName: $zoneId'; } @HiveType(typeId: 100) diff --git a/lib/logic/models/hive/user.dart b/lib/logic/models/hive/user.dart index 94ea993f..74809f7e 100644 --- a/lib/logic/models/hive/user.dart +++ b/lib/logic/models/hive/user.dart @@ -1,3 +1,5 @@ +// ignore_for_file: always_specify_types + import 'dart:ui'; import 'package:equatable/equatable.dart'; @@ -37,7 +39,5 @@ class User extends Equatable { Color get color => stringToColor(login); @override - String toString() { - return '$login, ${isFoundOnServer ? 'found' : 'not found'}, ${sshKeys.length} ssh keys, note: $note'; - } + String toString() => '$login, ${isFoundOnServer ? 'found' : 'not found'}, ${sshKeys.length} ssh keys, note: $note'; } diff --git a/lib/logic/models/hive/user.g.dart b/lib/logic/models/hive/user.g.dart index a1889dc1..57c08555 100644 --- a/lib/logic/models/hive/user.g.dart +++ b/lib/logic/models/hive/user.g.dart @@ -1,5 +1,7 @@ // GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: always_specify_types + part of 'user.dart'; // ************************************************************************** @@ -11,9 +13,9 @@ class UserAdapter extends TypeAdapter { final int typeId = 1; @override - User read(BinaryReader reader) { - final numOfFields = reader.readByte(); - final fields = { + User read(final BinaryReader reader) { + final int numOfFields = reader.readByte(); + final Map fields = { for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), }; return User( @@ -26,7 +28,7 @@ class UserAdapter extends TypeAdapter { } @override - void write(BinaryWriter writer, User obj) { + void write(final BinaryWriter writer, final User obj) { writer ..writeByte(5) ..writeByte(0) @@ -45,7 +47,7 @@ class UserAdapter extends TypeAdapter { int get hashCode => typeId.hashCode; @override - bool operator ==(Object other) => + bool operator ==(final Object other) => identical(this, other) || other is UserAdapter && runtimeType == other.runtimeType && diff --git a/lib/logic/models/job.dart b/lib/logic/models/job.dart index 7ee8e418..d64b79e2 100644 --- a/lib/logic/models/job.dart +++ b/lib/logic/models/job.dart @@ -1,16 +1,18 @@ +// ignore_for_file: always_specify_types + import 'package:easy_localization/easy_localization.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart'; import 'package:selfprivacy/logic/common_enum/common_enum.dart'; import 'package:selfprivacy/utils/password_generator.dart'; -import 'hive/user.dart'; +import 'package:selfprivacy/logic/models/hive/user.dart'; @immutable class Job extends Equatable { Job({ - String? id, required this.title, + final String? id, }) : id = id ?? StringGenerators.simpleId(); final String title; @@ -45,8 +47,8 @@ class DeleteUserJob extends Job { class ToggleJob extends Job { ToggleJob({ required this.type, - required String title, - }) : super(title: title); + required final super.title, + }); final dynamic type; @@ -56,12 +58,11 @@ class ToggleJob extends Job { class ServiceToggleJob extends ToggleJob { ServiceToggleJob({ - required ServiceTypes type, + required final ServiceTypes super.type, required this.needToTurnOn, }) : super( title: '${needToTurnOn ? "jobs.serviceTurnOn".tr() : "jobs.serviceTurnOff".tr()} ${type.title}', - type: type, ); final bool needToTurnOn; diff --git a/lib/logic/models/json/api_token.dart b/lib/logic/models/json/api_token.dart index 867e11d5..5e33fd4f 100644 --- a/lib/logic/models/json/api_token.dart +++ b/lib/logic/models/json/api_token.dart @@ -4,6 +4,9 @@ part 'api_token.g.dart'; @JsonSerializable() class ApiToken { + + factory ApiToken.fromJson(final Map json) => + _$ApiTokenFromJson(json); ApiToken({ required this.name, required this.date, @@ -14,7 +17,4 @@ class ApiToken { final DateTime date; @JsonKey(name: 'is_caller') final bool isCaller; - - factory ApiToken.fromJson(Map json) => - _$ApiTokenFromJson(json); } diff --git a/lib/logic/models/json/auto_upgrade_settings.dart b/lib/logic/models/json/auto_upgrade_settings.dart index 77c8905d..848c6437 100644 --- a/lib/logic/models/json/auto_upgrade_settings.dart +++ b/lib/logic/models/json/auto_upgrade_settings.dart @@ -1,3 +1,5 @@ +// ignore_for_file: always_specify_types + import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; @@ -5,18 +7,18 @@ part 'auto_upgrade_settings.g.dart'; @JsonSerializable(createToJson: true) class AutoUpgradeSettings extends Equatable { - final bool enable; - final bool allowReboot; + factory AutoUpgradeSettings.fromJson(final Map json) => + _$AutoUpgradeSettingsFromJson(json); const AutoUpgradeSettings({ required this.enable, required this.allowReboot, }); + final bool enable; + final bool allowReboot; @override List get props => [enable, allowReboot]; - factory AutoUpgradeSettings.fromJson(Map json) => - _$AutoUpgradeSettingsFromJson(json); Map toJson() => _$AutoUpgradeSettingsToJson(this); } diff --git a/lib/logic/models/json/backup.dart b/lib/logic/models/json/backup.dart index 95737897..7a5ad963 100644 --- a/lib/logic/models/json/backup.dart +++ b/lib/logic/models/json/backup.dart @@ -4,14 +4,14 @@ part 'backup.g.dart'; @JsonSerializable() class Backup { + + factory Backup.fromJson(final Map json) => _$BackupFromJson(json); Backup({required this.time, required this.id}); // Time of the backup final DateTime time; @JsonKey(name: 'short_id') final String id; - - factory Backup.fromJson(Map json) => _$BackupFromJson(json); } enum BackupStatusEnum { @@ -33,16 +33,16 @@ enum BackupStatusEnum { @JsonSerializable() class BackupStatus { + + factory BackupStatus.fromJson(final Map json) => + _$BackupStatusFromJson(json); BackupStatus( {required this.status, required this.progress, - required this.errorMessage}); + required this.errorMessage,}); final BackupStatusEnum status; final double progress; @JsonKey(name: 'error_message') final String? errorMessage; - - factory BackupStatus.fromJson(Map json) => - _$BackupStatusFromJson(json); } diff --git a/lib/logic/models/json/device_token.dart b/lib/logic/models/json/device_token.dart index d53299c5..53eac22f 100644 --- a/lib/logic/models/json/device_token.dart +++ b/lib/logic/models/json/device_token.dart @@ -4,6 +4,9 @@ part 'device_token.g.dart'; @JsonSerializable() class DeviceToken { + + factory DeviceToken.fromJson(final Map json) => + _$DeviceTokenFromJson(json); DeviceToken({ required this.device, required this.token, @@ -11,7 +14,4 @@ class DeviceToken { final String device; final String token; - - factory DeviceToken.fromJson(Map json) => - _$DeviceTokenFromJson(json); } diff --git a/lib/logic/models/json/dns_records.dart b/lib/logic/models/json/dns_records.dart index 25aad046..cd4867c3 100644 --- a/lib/logic/models/json/dns_records.dart +++ b/lib/logic/models/json/dns_records.dart @@ -20,5 +20,5 @@ class DnsRecord { final int priority; final bool proxied; - toJson() => _$DnsRecordToJson(this); + Map toJson() => _$DnsRecordToJson(this); } diff --git a/lib/logic/models/json/hetzner_server_info.dart b/lib/logic/models/json/hetzner_server_info.dart index 6e173181..b7e19346 100644 --- a/lib/logic/models/json/hetzner_server_info.dart +++ b/lib/logic/models/json/hetzner_server_info.dart @@ -1,9 +1,22 @@ +// ignore_for_file: always_specify_types + import 'package:json_annotation/json_annotation.dart'; part 'hetzner_server_info.g.dart'; @JsonSerializable() class HetznerServerInfo { + + HetznerServerInfo( + this.id, + this.name, + this.status, + this.created, + this.serverType, + this.location, + this.publicNet, + this.volumes, + ); final int id; final String name; final ServerStatus status; @@ -19,46 +32,35 @@ class HetznerServerInfo { @JsonKey(name: 'public_net') final HetznerPublicNetInfo publicNet; - static HetznerLocation locationFromJson(Map json) => + static HetznerLocation locationFromJson(final Map json) => HetznerLocation.fromJson(json['location']); - static HetznerServerInfo fromJson(Map json) => + static HetznerServerInfo fromJson(final Map json) => _$HetznerServerInfoFromJson(json); - - HetznerServerInfo( - this.id, - this.name, - this.status, - this.created, - this.serverType, - this.location, - this.publicNet, - this.volumes, - ); } @JsonSerializable() class HetznerPublicNetInfo { - final HetznerIp4 ipv4; - - static HetznerPublicNetInfo fromJson(Map json) => - _$HetznerPublicNetInfoFromJson(json); HetznerPublicNetInfo(this.ipv4); + final HetznerIp4 ipv4; + + static HetznerPublicNetInfo fromJson(final Map json) => + _$HetznerPublicNetInfoFromJson(json); } @JsonSerializable() class HetznerIp4 { + + HetznerIp4(this.id, this.ip, this.blocked, this.reverseDns); final bool blocked; @JsonKey(name: 'dns_ptr') final String reverseDns; final int id; final String ip; - static HetznerIp4 fromJson(Map json) => + static HetznerIp4 fromJson(final Map json) => _$HetznerIp4FromJson(json); - - HetznerIp4(this.id, this.ip, this.blocked, this.reverseDns); } enum ServerStatus { @@ -75,15 +77,15 @@ enum ServerStatus { @JsonSerializable() class HetznerServerTypeInfo { + + HetznerServerTypeInfo(this.cores, this.memory, this.disk, this.prices); final int cores; final num memory; final int disk; final List prices; - HetznerServerTypeInfo(this.cores, this.memory, this.disk, this.prices); - - static HetznerServerTypeInfo fromJson(Map json) => + static HetznerServerTypeInfo fromJson(final Map json) => _$HetznerServerTypeInfoFromJson(json); } @@ -97,14 +99,16 @@ class HetznerPriceInfo { @JsonKey(name: 'price_monthly', fromJson: HetznerPriceInfo.getPrice) final double monthly; - static HetznerPriceInfo fromJson(Map json) => + static HetznerPriceInfo fromJson(final Map json) => _$HetznerPriceInfoFromJson(json); - static double getPrice(Map json) => double.parse(json['gross'] as String); + static double getPrice(final Map json) => double.parse(json['gross'] as String); } @JsonSerializable() class HetznerLocation { + + HetznerLocation(this.country, this.city, this.description, this.zone); final String country; final String city; final String description; @@ -112,8 +116,6 @@ class HetznerLocation { @JsonKey(name: 'network_zone') final String zone; - HetznerLocation(this.country, this.city, this.description, this.zone); - - static HetznerLocation fromJson(Map json) => + static HetznerLocation fromJson(final Map json) => _$HetznerLocationFromJson(json); } diff --git a/lib/logic/models/json/recovery_token_status.dart b/lib/logic/models/json/recovery_token_status.dart index 6b27b028..2c455480 100644 --- a/lib/logic/models/json/recovery_token_status.dart +++ b/lib/logic/models/json/recovery_token_status.dart @@ -1,3 +1,5 @@ +// ignore_for_file: always_specify_types + import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; @@ -5,6 +7,9 @@ part 'recovery_token_status.g.dart'; @JsonSerializable() class RecoveryKeyStatus extends Equatable { + + factory RecoveryKeyStatus.fromJson(final Map json) => + _$RecoveryKeyStatusFromJson(json); const RecoveryKeyStatus({ required this.exists, required this.valid, @@ -20,9 +25,6 @@ class RecoveryKeyStatus extends Equatable { final int? usesLeft; final bool valid; - factory RecoveryKeyStatus.fromJson(Map json) => - _$RecoveryKeyStatusFromJson(json); - @override List get props => [ exists, diff --git a/lib/logic/models/json/server_configurations.dart b/lib/logic/models/json/server_configurations.dart index 73915566..91f5f65d 100644 --- a/lib/logic/models/json/server_configurations.dart +++ b/lib/logic/models/json/server_configurations.dart @@ -1,3 +1,5 @@ +// ignore_for_file: always_specify_types + import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; @@ -5,6 +7,9 @@ part 'server_configurations.g.dart'; @JsonSerializable(createToJson: true) class AutoUpgradeConfigurations extends Equatable { + + factory AutoUpgradeConfigurations.fromJson(final Map json) => + _$AutoUpgradeConfigurationsFromJson(json); const AutoUpgradeConfigurations({ required this.enable, required this.allowReboot, @@ -12,9 +17,6 @@ class AutoUpgradeConfigurations extends Equatable { final bool enable; final bool allowReboot; - - factory AutoUpgradeConfigurations.fromJson(Map json) => - _$AutoUpgradeConfigurationsFromJson(json); Map toJson() => _$AutoUpgradeConfigurationsToJson(this); @override diff --git a/lib/logic/models/message.dart b/lib/logic/models/message.dart index 176f2846..44f30b4d 100644 --- a/lib/logic/models/message.dart +++ b/lib/logic/models/message.dart @@ -1,6 +1,6 @@ import 'package:intl/intl.dart'; -final formatter = DateFormat('hh:mm'); +final DateFormat formatter = DateFormat('hh:mm'); class Message { Message({this.text, this.type = MessageType.normal}) : time = DateTime.now(); @@ -10,7 +10,7 @@ class Message { final MessageType type; String get timeString => formatter.format(time); - static Message warn({String? text}) => Message( + static Message warn({final String? text}) => Message( text: text, type: MessageType.warning, ); diff --git a/lib/logic/models/provider.dart b/lib/logic/models/provider.dart index 82af8656..4e3191eb 100644 --- a/lib/logic/models/provider.dart +++ b/lib/logic/models/provider.dart @@ -1,3 +1,5 @@ +// ignore_for_file: always_specify_types + import 'package:equatable/equatable.dart'; import 'package:flutter/widgets.dart'; import 'package:selfprivacy/logic/models/state_types.dart'; @@ -15,7 +17,7 @@ class ProviderModel extends Equatable { final StateType state; final ProviderType type; - ProviderModel updateState(StateType newState) => ProviderModel( + ProviderModel updateState(final StateType newState) => ProviderModel( state: newState, type: type, ); diff --git a/lib/logic/models/server_basic_info.dart b/lib/logic/models/server_basic_info.dart index fcd37ed4..7f0f2e1b 100644 --- a/lib/logic/models/server_basic_info.dart +++ b/lib/logic/models/server_basic_info.dart @@ -1,10 +1,6 @@ +// ignore_for_file: always_specify_types + class ServerBasicInfo { - final int id; - final String name; - final String reverseDns; - final String ip; - final DateTime created; - final int volumeId; ServerBasicInfo({ required this.id, @@ -14,34 +10,20 @@ class ServerBasicInfo { required this.created, required this.volumeId, }); + final int id; + final String name; + final String reverseDns; + final String ip; + final DateTime created; + final int volumeId; } class ServerBasicInfoWithValidators extends ServerBasicInfo { - final bool isIpValid; - final bool isReverseDnsValid; - - ServerBasicInfoWithValidators({ - required int id, - required String name, - required String reverseDns, - required String ip, - required DateTime created, - required int volumeId, - required this.isIpValid, - required this.isReverseDnsValid, - }) : super( - id: id, - name: name, - reverseDns: reverseDns, - ip: ip, - created: created, - volumeId: volumeId, - ); ServerBasicInfoWithValidators.fromServerBasicInfo({ - required ServerBasicInfo serverBasicInfo, - required isIpValid, - required isReverseDnsValid, + required final ServerBasicInfo serverBasicInfo, + required final isIpValid, + required final isReverseDnsValid, }) : this( id: serverBasicInfo.id, name: serverBasicInfo.name, @@ -52,4 +34,17 @@ class ServerBasicInfoWithValidators extends ServerBasicInfo { isIpValid: isIpValid, isReverseDnsValid: isReverseDnsValid, ); + + ServerBasicInfoWithValidators({ + required final super.id, + required final super.name, + required final super.reverseDns, + required final super.ip, + required final super.created, + required final super.volumeId, + required this.isIpValid, + required this.isReverseDnsValid, + }); + final bool isIpValid; + final bool isReverseDnsValid; } diff --git a/lib/logic/models/server_status.dart b/lib/logic/models/server_status.dart index 1405bde3..b191ee16 100644 --- a/lib/logic/models/server_status.dart +++ b/lib/logic/models/server_status.dart @@ -1,24 +1,22 @@ class ServerStatus { - final StatusTypes http; - final StatusTypes imap; - final StatusTypes smtp; ServerStatus({ required this.http, this.imap = StatusTypes.nodata, this.smtp = StatusTypes.nodata, }); + final StatusTypes http; + final StatusTypes imap; + final StatusTypes smtp; - ServerStatus fromJson(Map json) { - return ServerStatus( + ServerStatus fromJson(final Map json) => ServerStatus( http: statusTypeFromNumber(json['http']), imap: statusTypeFromNumber(json['imap']), smtp: statusTypeFromNumber(json['smtp']), ); - } } -StatusTypes statusTypeFromNumber(int? number) { +StatusTypes statusTypeFromNumber(final int? number) { if (number == 0) { return StatusTypes.ok; } else if (number == 1) { diff --git a/lib/logic/models/timezone_settings.dart b/lib/logic/models/timezone_settings.dart index 76d28aff..45348b64 100644 --- a/lib/logic/models/timezone_settings.dart +++ b/lib/logic/models/timezone_settings.dart @@ -1,18 +1,18 @@ +// ignore_for_file: always_specify_types + import 'package:timezone/timezone.dart'; class TimeZoneSettings { - final Location timezone; - TimeZoneSettings(this.timezone); - - Map toJson() { - return { - 'timezone': timezone.name, - }; - } - - factory TimeZoneSettings.fromString(String string) { - var location = timeZoneDatabase.locations[string]!; + factory TimeZoneSettings.fromString(final String string) { + final Location location = timeZoneDatabase.locations[string]!; return TimeZoneSettings(location); } + + TimeZoneSettings(this.timezone); + final Location timezone; + + Map toJson() => { + 'timezone': timezone.name, + }; } diff --git a/lib/main.dart b/lib/main.dart index 9593ee31..f7234d26 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,3 +1,5 @@ +// ignore_for_file: always_specify_types + import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -11,11 +13,11 @@ import 'package:selfprivacy/ui/pages/root_route.dart'; import 'package:wakelock/wakelock.dart'; import 'package:timezone/data/latest.dart' as tz; -import 'config/bloc_config.dart'; -import 'config/bloc_observer.dart'; -import 'config/get_it_config.dart'; -import 'config/localization.dart'; -import 'logic/cubit/app_settings/app_settings_cubit.dart'; +import 'package:selfprivacy/config/bloc_config.dart'; +import 'package:selfprivacy/config/bloc_observer.dart'; +import 'package:selfprivacy/config/get_it_config.dart'; +import 'package:selfprivacy/config/localization.dart'; +import 'package:selfprivacy/logic/cubit/app_settings/app_settings_cubit.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -34,11 +36,11 @@ void main() async { await EasyLocalization.ensureInitialized(); tz.initializeTimeZones(); - final lightThemeData = await AppThemeFactory.create( + final ThemeData lightThemeData = await AppThemeFactory.create( isDark: false, fallbackColor: BrandColors.primary, ); - final darkThemeData = await AppThemeFactory.create( + final ThemeData darkThemeData = await AppThemeFactory.create( isDark: true, fallbackColor: BrandColors.primary, ); @@ -48,30 +50,28 @@ void main() async { child: MyApp( lightThemeData: lightThemeData, darkThemeData: darkThemeData, - ))), + ),),), blocObserver: SimpleBlocObserver(), ); } class MyApp extends StatelessWidget { const MyApp({ - Key? key, required this.lightThemeData, required this.darkThemeData, - }) : super(key: key); + final super.key, + }); final ThemeData lightThemeData; final ThemeData darkThemeData; @override - Widget build(BuildContext context) { - return Localization( + Widget build(final BuildContext context) => Localization( child: AnnotatedRegion( value: SystemUiOverlayStyle.light, // Manually changing appbar color child: BlocAndProviderConfig( child: BlocBuilder( - builder: (context, appSettings) { - return MaterialApp( + builder: (final BuildContext context, final AppSettingsState appSettings) => MaterialApp( scaffoldMessengerKey: getIt.get().scaffoldMessengerKey, navigatorKey: getIt.get().navigatorKey, @@ -87,20 +87,18 @@ class MyApp extends StatelessWidget { home: appSettings.isOnboardingShowing ? const OnboardingPage(nextPage: InitializingPage()) : const RootPage(), - builder: (BuildContext context, Widget? widget) { + 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 = - (FlutterErrorDetails errorDetails) => error; + (final FlutterErrorDetails errorDetails) => error; return widget!; }, - ); - }, + ), ), ), ), ); - } } diff --git a/lib/theming/factory/app_theme_factory.dart b/lib/theming/factory/app_theme_factory.dart index 1d99c480..92979267 100644 --- a/lib/theming/factory/app_theme_factory.dart +++ b/lib/theming/factory/app_theme_factory.dart @@ -1,8 +1,11 @@ +// ignore_for_file: always_specify_types + import 'dart:io'; import 'package:dynamic_color/dynamic_color.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:material_color_utilities/palettes/core_palette.dart'; import 'package:system_theme/system_theme.dart'; import 'package:gtk_theme_fl/gtk_theme_fl.dart'; @@ -10,27 +13,25 @@ abstract class AppThemeFactory { AppThemeFactory._(); static Future create( - {required bool isDark, required Color fallbackColor}) { - return _createAppTheme( + {required final bool isDark, required final Color fallbackColor,}) => _createAppTheme( isDark: isDark, fallbackColor: fallbackColor, ); - } static Future _createAppTheme({ - bool isDark = false, - required Color fallbackColor, + required final Color fallbackColor, + final bool isDark = false, }) async { ColorScheme? gtkColorsScheme; - var brightness = isDark ? Brightness.dark : Brightness.light; + final Brightness brightness = isDark ? Brightness.dark : Brightness.light; - final dynamicColorsScheme = await _getDynamicColors(brightness); + final ColorScheme? dynamicColorsScheme = await _getDynamicColors(brightness); if (Platform.isLinux) { - GtkThemeData themeData = await GtkThemeData.initialize(); - final isGtkDark = + final GtkThemeData themeData = await GtkThemeData.initialize(); + final bool isGtkDark = Color(themeData.theme_bg_color).computeLuminance() < 0.5; - final isInverseNeeded = isGtkDark != isDark; + final bool isInverseNeeded = isGtkDark != isDark; gtkColorsScheme = ColorScheme.fromSeed( seedColor: Color(themeData.theme_selected_bg_color), brightness: brightness, @@ -39,7 +40,7 @@ abstract class AppThemeFactory { ); } - final accentColor = SystemAccentColor(fallbackColor); + final SystemAccentColor accentColor = SystemAccentColor(fallbackColor); try { await accentColor.load(); @@ -47,17 +48,17 @@ abstract class AppThemeFactory { print('_createAppTheme: ${e.message}'); } - final fallbackColorScheme = ColorScheme.fromSeed( + final ColorScheme fallbackColorScheme = ColorScheme.fromSeed( seedColor: accentColor.accent, brightness: brightness, ); - final colorScheme = + final ColorScheme colorScheme = dynamicColorsScheme ?? gtkColorsScheme ?? fallbackColorScheme; - final appTypography = Typography.material2021(); + final Typography appTypography = Typography.material2021(); - final materialThemeData = ThemeData( + final ThemeData materialThemeData = ThemeData( colorScheme: colorScheme, brightness: colorScheme.brightness, typography: appTypography, @@ -73,10 +74,10 @@ abstract class AppThemeFactory { return materialThemeData; } - static Future _getDynamicColors(Brightness brightness) { + static Future _getDynamicColors(final Brightness brightness) { try { return DynamicColorPlugin.getCorePalette().then( - (corePallet) => corePallet?.toColorScheme(brightness: brightness)); + (final CorePalette? corePallet) => corePallet?.toColorScheme(brightness: brightness),); } on PlatformException { return Future.value(null); } diff --git a/lib/ui/components/action_button/action_button.dart b/lib/ui/components/action_button/action_button.dart index e507fa0c..580a69fb 100644 --- a/lib/ui/components/action_button/action_button.dart +++ b/lib/ui/components/action_button/action_button.dart @@ -3,19 +3,19 @@ import 'package:selfprivacy/config/brand_colors.dart'; class ActionButton extends StatelessWidget { const ActionButton({ - Key? key, + final super.key, this.text, this.onPressed, this.isRed = false, - }) : super(key: key); + }); final VoidCallback? onPressed; final String? text; final bool isRed; @override - Widget build(BuildContext context) { - var navigator = Navigator.of(context); + Widget build(final BuildContext context) { + final NavigatorState navigator = Navigator.of(context); return TextButton( child: Text( diff --git a/lib/ui/components/brand_alert/brand_alert.dart b/lib/ui/components/brand_alert/brand_alert.dart index e4a8f04c..0d673ded 100644 --- a/lib/ui/components/brand_alert/brand_alert.dart +++ b/lib/ui/components/brand_alert/brand_alert.dart @@ -2,14 +2,12 @@ import 'package:flutter/material.dart'; class BrandAlert extends AlertDialog { BrandAlert({ - Key? key, - String? title, - String? contentText, - List? actions, + final super.key, + final String? title, + final String? contentText, + final super.actions, }) : super( - key: key, title: title != null ? Text(title) : null, content: title != null ? Text(contentText!) : null, - actions: actions, ); } diff --git a/lib/ui/components/brand_bottom_sheet/brand_bottom_sheet.dart b/lib/ui/components/brand_bottom_sheet/brand_bottom_sheet.dart index 22430d06..d5c6afbe 100644 --- a/lib/ui/components/brand_bottom_sheet/brand_bottom_sheet.dart +++ b/lib/ui/components/brand_bottom_sheet/brand_bottom_sheet.dart @@ -1,19 +1,21 @@ +// ignore_for_file: always_specify_types + import 'package:flutter/material.dart'; import 'package:selfprivacy/config/brand_colors.dart'; class BrandBottomSheet extends StatelessWidget { const BrandBottomSheet({ - Key? key, required this.child, + final super.key, this.isExpended = false, - }) : super(key: key); + }); final Widget child; final bool isExpended; @override - Widget build(BuildContext context) { - var mainHeight = MediaQuery.of(context).size.height - + Widget build(final BuildContext context) { + final double mainHeight = MediaQuery.of(context).size.height - MediaQuery.of(context).padding.top - 100; late Widget innerWidget; diff --git a/lib/ui/components/brand_button/brand_button.dart b/lib/ui/components/brand_button/brand_button.dart index 0b5b9c49..55d7fddc 100644 --- a/lib/ui/components/brand_button/brand_button.dart +++ b/lib/ui/components/brand_button/brand_button.dart @@ -1,3 +1,5 @@ +// ignore_for_file: always_specify_types + import 'package:flutter/material.dart'; import 'package:selfprivacy/ui/components/brand_button/filled_button.dart'; import 'package:selfprivacy/ui/components/brand_text/brand_text.dart'; @@ -5,11 +7,11 @@ import 'package:selfprivacy/ui/components/brand_text/brand_text.dart'; enum BrandButtonTypes { rised, text, iconText } class BrandButton { - static rised({ - Key? key, - required VoidCallback? onPressed, - String? text, - Widget? child, + static ConstrainedBox rised({ + required final VoidCallback? onPressed, + final Key? key, + final String? text, + final Widget? child, }) { assert(text == null || child == null, 'required title or child'); assert(text != null || child != null, 'required title or child'); @@ -27,10 +29,10 @@ class BrandButton { ); } - static text({ - Key? key, - required VoidCallback onPressed, - required String title, + static ConstrainedBox text({ + required final VoidCallback onPressed, + required final String title, + final Key? key, }) => ConstrainedBox( constraints: const BoxConstraints( @@ -40,11 +42,11 @@ class BrandButton { child: TextButton(onPressed: onPressed, child: Text(title)), ); - static emptyWithIconText({ - Key? key, - required VoidCallback onPressed, - required String title, - required Icon icon, + static _IconTextButton emptyWithIconText({ + required final VoidCallback onPressed, + required final String title, + required final Icon icon, + final Key? key, }) => _IconTextButton( key: key, @@ -55,16 +57,14 @@ class BrandButton { } class _IconTextButton extends StatelessWidget { - const _IconTextButton({Key? key, this.onPressed, this.title, this.icon}) - : super(key: key); + const _IconTextButton({final super.key, this.onPressed, this.title, this.icon}); final VoidCallback? onPressed; final String? title; final Icon? icon; @override - Widget build(BuildContext context) { - return Material( + Widget build(final BuildContext context) => Material( color: Colors.transparent, child: InkWell( onTap: onPressed, @@ -85,5 +85,4 @@ class _IconTextButton extends StatelessWidget { ), ), ); - } } diff --git a/lib/ui/components/brand_button/filled_button.dart b/lib/ui/components/brand_button/filled_button.dart index cc6aeb26..b3888f3c 100644 --- a/lib/ui/components/brand_button/filled_button.dart +++ b/lib/ui/components/brand_button/filled_button.dart @@ -2,12 +2,12 @@ import 'package:flutter/material.dart'; class FilledButton extends StatelessWidget { const FilledButton({ - Key? key, + final super.key, this.onPressed, this.title, this.child, this.disabled = false, - }) : super(key: key); + }); final VoidCallback? onPressed; final String? title; @@ -15,7 +15,7 @@ class FilledButton extends StatelessWidget { final bool disabled; @override - Widget build(BuildContext context) { + Widget build(final BuildContext context) { final ButtonStyle enabledStyle = ElevatedButton.styleFrom( onPrimary: Theme.of(context).colorScheme.onPrimary, primary: Theme.of(context).colorScheme.primary, diff --git a/lib/ui/components/brand_cards/brand_cards.dart b/lib/ui/components/brand_cards/brand_cards.dart index 7f19e47d..138a674a 100644 --- a/lib/ui/components/brand_cards/brand_cards.dart +++ b/lib/ui/components/brand_cards/brand_cards.dart @@ -1,7 +1,9 @@ +// ignore_for_file: always_specify_types + import 'package:flutter/material.dart'; class BrandCards { - static Widget big({required Widget child}) => _BrandCard( + static Widget big({required final Widget child}) => _BrandCard( padding: const EdgeInsets.symmetric( horizontal: 20, vertical: 15, @@ -10,7 +12,7 @@ class BrandCards { borderRadius: BorderRadius.circular(20), child: child, ); - static Widget small({required Widget child}) => _BrandCard( + static Widget small({required final Widget child}) => _BrandCard( padding: const EdgeInsets.symmetric( horizontal: 15, vertical: 10, @@ -19,10 +21,11 @@ class BrandCards { borderRadius: BorderRadius.circular(10), child: child, ); - static Widget outlined({required Widget child}) => _OutlinedCard( + static Widget outlined({required final Widget child}) => _OutlinedCard( child: child, ); - static Widget filled({required Widget child, bool tertiary = false}) => + static Widget filled( + {required final Widget child, final bool tertiary = false,}) => _FilledCard( tertiary: tertiary, child: child, @@ -31,12 +34,12 @@ class BrandCards { class _BrandCard extends StatelessWidget { const _BrandCard({ - Key? key, required this.child, required this.padding, required this.shadow, required this.borderRadius, - }) : super(key: key); + final super.key, + }); final Widget child; final EdgeInsets padding; @@ -44,65 +47,62 @@ class _BrandCard extends StatelessWidget { final BorderRadius borderRadius; @override - Widget build(BuildContext context) { - return Container( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - borderRadius: borderRadius, - boxShadow: shadow, - ), - padding: padding, - child: child, - ); - } + Widget build(final BuildContext context) => Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: borderRadius, + boxShadow: shadow, + ), + padding: padding, + child: child, + ); } class _OutlinedCard extends StatelessWidget { const _OutlinedCard({ - Key? key, + final super.key, required this.child, - }) : super(key: key); + }); final Widget child; @override - Widget build(BuildContext context) { - return Card( - elevation: 0.0, - shape: RoundedRectangleBorder( - borderRadius: const BorderRadius.all(Radius.circular(12)), - side: BorderSide( - color: Theme.of(context).colorScheme.outline, + Widget build(final BuildContext context) => Card( + elevation: 0.0, + shape: RoundedRectangleBorder( + borderRadius: const BorderRadius.all(Radius.circular(12)), + side: BorderSide( + color: Theme.of(context).colorScheme.outline, + ), ), - ), - clipBehavior: Clip.antiAlias, - child: child, - ); - } + clipBehavior: Clip.antiAlias, + child: child, + ); } class _FilledCard extends StatelessWidget { - const _FilledCard({Key? key, required this.child, required this.tertiary}) - : super(key: key); + const _FilledCard({ + required this.child, + required this.tertiary, + final super.key, + }); final Widget child; final bool tertiary; @override - Widget build(BuildContext context) { - return Card( - elevation: 0.0, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(12)), - ), - clipBehavior: Clip.antiAlias, - color: tertiary - ? Theme.of(context).colorScheme.tertiaryContainer - : Theme.of(context).colorScheme.surfaceVariant, - child: child, - ); - } + Widget build(final BuildContext context) => Card( + elevation: 0.0, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(12)), + ), + clipBehavior: Clip.antiAlias, + color: tertiary + ? Theme.of(context).colorScheme.tertiaryContainer + : Theme.of(context).colorScheme.surfaceVariant, + child: child, + ); } -final bigShadow = [ +final List bigShadow = [ BoxShadow( offset: const Offset(0, 4), blurRadius: 8, diff --git a/lib/ui/components/brand_divider/brand_divider.dart b/lib/ui/components/brand_divider/brand_divider.dart index bd3d9c92..c3a3b5a3 100644 --- a/lib/ui/components/brand_divider/brand_divider.dart +++ b/lib/ui/components/brand_divider/brand_divider.dart @@ -2,14 +2,12 @@ import 'package:flutter/material.dart'; import 'package:selfprivacy/config/brand_colors.dart'; class BrandDivider extends StatelessWidget { - const BrandDivider({Key? key}) : super(key: key); + const BrandDivider({super.key}); @override - Widget build(BuildContext context) { - return Container( + Widget build(final BuildContext context) => Container( width: double.infinity, height: 1, color: BrandColors.dividerColor, ); - } } diff --git a/lib/ui/components/brand_header/brand_header.dart b/lib/ui/components/brand_header/brand_header.dart index 82e73e47..7e0bd3e8 100644 --- a/lib/ui/components/brand_header/brand_header.dart +++ b/lib/ui/components/brand_header/brand_header.dart @@ -1,22 +1,23 @@ +// ignore_for_file: always_specify_types + import 'package:flutter/material.dart'; import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart'; import 'package:selfprivacy/ui/components/brand_text/brand_text.dart'; class BrandHeader extends StatelessWidget { const BrandHeader({ - Key? key, + final super.key, this.title = '', this.hasBackButton = false, this.onBackButtonPressed, - }) : super(key: key); + }); final String title; final bool hasBackButton; final VoidCallback? onBackButtonPressed; @override - Widget build(BuildContext context) { - return Container( + Widget build(final BuildContext context) => Container( height: 52, alignment: Alignment.centerLeft, padding: EdgeInsets.only( @@ -37,5 +38,4 @@ class BrandHeader extends StatelessWidget { ], ), ); - } } diff --git a/lib/ui/components/brand_hero_screen/brand_hero_screen.dart b/lib/ui/components/brand_hero_screen/brand_hero_screen.dart index 6d00963c..5061ec63 100644 --- a/lib/ui/components/brand_hero_screen/brand_hero_screen.dart +++ b/lib/ui/components/brand_hero_screen/brand_hero_screen.dart @@ -4,16 +4,16 @@ import 'package:selfprivacy/ui/components/pre_styled_buttons/flash_fab.dart'; class BrandHeroScreen extends StatelessWidget { const BrandHeroScreen({ - Key? key, + required this.children, + final super.key, this.headerTitle = '', this.hasBackButton = true, this.hasFlashButton = true, - required this.children, this.heroIcon, this.heroTitle, this.heroSubtitle, this.onBackButtonPressed, - }) : super(key: key); + }); final List children; final String headerTitle; @@ -25,8 +25,7 @@ class BrandHeroScreen extends StatelessWidget { final VoidCallback? onBackButtonPressed; @override - Widget build(BuildContext context) { - return SafeArea( + Widget build(BuildContext context) => SafeArea( child: Scaffold( appBar: PreferredSize( preferredSize: const Size.fromHeight(52.0), @@ -63,12 +62,11 @@ class BrandHeroScreen extends StatelessWidget { style: Theme.of(context).textTheme.bodyMedium!.copyWith( color: Theme.of(context).colorScheme.onBackground, ), - textAlign: TextAlign.start), + textAlign: TextAlign.start,), const SizedBox(height: 16.0), ...children, ], ), ), ); - } } diff --git a/lib/ui/components/brand_icons/brand_icons.dart b/lib/ui/components/brand_icons/brand_icons.dart index ea8e51a0..f66ed8ec 100644 --- a/lib/ui/components/brand_icons/brand_icons.dart +++ b/lib/ui/components/brand_icons/brand_icons.dart @@ -18,7 +18,7 @@ import 'package:flutter/widgets.dart'; class BrandIcons { BrandIcons._(); - static const _kFontFam = 'BrandIcons'; + static const String _kFontFam = 'BrandIcons'; static const String? _kFontPkg = null; static const IconData connection = diff --git a/lib/ui/components/brand_loader/brand_loader.dart b/lib/ui/components/brand_loader/brand_loader.dart index ea9f754b..5dc6a6ea 100644 --- a/lib/ui/components/brand_loader/brand_loader.dart +++ b/lib/ui/components/brand_loader/brand_loader.dart @@ -2,13 +2,12 @@ import 'package:flutter/material.dart'; import 'package:easy_localization/easy_localization.dart'; class BrandLoader { - static horizontal() => _HorizontalLoader(); + static _HorizontalLoader horizontal() => _HorizontalLoader(); } class _HorizontalLoader extends StatelessWidget { @override - Widget build(BuildContext context) { - return Column( + Widget build(final BuildContext context) => Column( mainAxisSize: MainAxisSize.min, children: [ Text('basis.wait'.tr()), @@ -16,5 +15,4 @@ class _HorizontalLoader extends StatelessWidget { const LinearProgressIndicator(minHeight: 3), ], ); - } } diff --git a/lib/ui/components/brand_md/brand_md.dart b/lib/ui/components/brand_md/brand_md.dart index 24c7c860..3de9f86a 100644 --- a/lib/ui/components/brand_md/brand_md.dart +++ b/lib/ui/components/brand_md/brand_md.dart @@ -8,9 +8,9 @@ import 'package:url_launcher/url_launcher_string.dart'; class BrandMarkdown extends StatefulWidget { const BrandMarkdown({ - Key? key, required this.fileName, - }) : super(key: key); + final super.key, + }); final String fileName; @@ -28,7 +28,7 @@ class _BrandMarkdownState extends State { } void _loadMdFile() async { - String mdFromFile = await rootBundle + final String mdFromFile = await rootBundle .loadString('assets/markdown/${widget.fileName}-${'locale'.tr()}.md'); setState(() { _mdContent = mdFromFile; @@ -36,9 +36,9 @@ class _BrandMarkdownState extends State { } @override - Widget build(BuildContext context) { - var isDark = Theme.of(context).brightness == Brightness.dark; - var markdown = MarkdownStyleSheet( + 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, ), @@ -58,9 +58,9 @@ class _BrandMarkdownState extends State { return Markdown( shrinkWrap: true, styleSheet: markdown, - onTapLink: (String text, String? href, String title) { + onTapLink: (final String text, final String? href, final String title) { if (href != null) { - canLaunchUrlString(href).then((canLaunchURL) { + canLaunchUrlString(href).then((final bool canLaunchURL) { if (canLaunchURL) { launchUrlString(href); } diff --git a/lib/ui/components/brand_radio/brand_radio.dart b/lib/ui/components/brand_radio/brand_radio.dart index 21cc2779..aca55505 100644 --- a/lib/ui/components/brand_radio/brand_radio.dart +++ b/lib/ui/components/brand_radio/brand_radio.dart @@ -3,15 +3,14 @@ import 'package:selfprivacy/config/brand_colors.dart'; class BrandRadio extends StatelessWidget { const BrandRadio({ - Key? key, + super.key, required this.isChecked, - }) : super(key: key); + }); final bool isChecked; @override - Widget build(BuildContext context) { - return Container( + Widget build(final BuildContext context) => Container( height: 20, width: 20, alignment: Alignment.center, @@ -31,12 +30,9 @@ class BrandRadio extends StatelessWidget { ) : null, ); - } - BoxBorder? _getBorder() { - return Border.all( + BoxBorder? _getBorder() => Border.all( color: isChecked ? BrandColors.primary : BrandColors.gray1, width: 2, ); - } } diff --git a/lib/ui/components/brand_radio_tile/brand_radio_tile.dart b/lib/ui/components/brand_radio_tile/brand_radio_tile.dart index b2784799..17b3ccff 100644 --- a/lib/ui/components/brand_radio_tile/brand_radio_tile.dart +++ b/lib/ui/components/brand_radio_tile/brand_radio_tile.dart @@ -4,11 +4,11 @@ import 'package:selfprivacy/ui/components/brand_text/brand_text.dart'; class BrandRadioTile extends StatelessWidget { const BrandRadioTile({ - Key? key, + super.key, required this.isChecked, required this.text, required this.onPress, - }) : super(key: key); + }); final bool isChecked; @@ -16,8 +16,7 @@ class BrandRadioTile extends StatelessWidget { final VoidCallback onPress; @override - Widget build(BuildContext context) { - return GestureDetector( + Widget build(BuildContext context) => GestureDetector( onTap: onPress, behavior: HitTestBehavior.translucent, child: Padding( @@ -33,5 +32,4 @@ class BrandRadioTile extends StatelessWidget { ), ), ); - } } diff --git a/lib/ui/components/brand_span_button/brand_span_button.dart b/lib/ui/components/brand_span_button/brand_span_button.dart index 6fdef622..da36ee02 100644 --- a/lib/ui/components/brand_span_button/brand_span_button.dart +++ b/lib/ui/components/brand_span_button/brand_span_button.dart @@ -5,19 +5,19 @@ import 'package:url_launcher/url_launcher.dart'; class BrandSpanButton extends TextSpan { BrandSpanButton({ - required String text, - required VoidCallback onTap, - TextStyle? style, + required final String text, + required final VoidCallback onTap, + final TextStyle? style, }) : super( recognizer: TapGestureRecognizer()..onTap = onTap, text: text, style: (style ?? const TextStyle()).copyWith(color: BrandColors.blue), ); - static link({ - required String text, - String? urlString, - TextStyle? style, + static BrandSpanButton link({ + required final String text, + final String? urlString, + final TextStyle? style, }) => BrandSpanButton( text: text, @@ -25,7 +25,7 @@ class BrandSpanButton extends TextSpan { onTap: () => _launchURL(urlString ?? text), ); - static _launchURL(String link) async { + static _launchURL(final String link) async { if (await canLaunchUrl(Uri.parse(link))) { await launchUrl(Uri.parse(link)); } else { diff --git a/lib/ui/components/brand_switch/brand_switch.dart b/lib/ui/components/brand_switch/brand_switch.dart index adf7e4e5..deb595a4 100644 --- a/lib/ui/components/brand_switch/brand_switch.dart +++ b/lib/ui/components/brand_switch/brand_switch.dart @@ -2,20 +2,18 @@ import 'package:flutter/material.dart'; class BrandSwitch extends StatelessWidget { const BrandSwitch({ - Key? key, required this.onChanged, required this.value, - }) : super(key: key); + final super.key, + }); final ValueChanged onChanged; final bool value; @override - Widget build(BuildContext context) { - return Switch( + Widget build(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 index 0c32fd5f..e1a03062 100644 --- a/lib/ui/components/brand_tab_bar/brand_tab_bar.dart +++ b/lib/ui/components/brand_tab_bar/brand_tab_bar.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart'; class BrandTabBar extends StatefulWidget { - const BrandTabBar({Key? key, this.controller}) : super(key: key); + const BrandTabBar({final super.key, this.controller}); final TabController? controller; @override @@ -34,26 +34,22 @@ class _BrandTabBarState extends State { } @override - Widget build(BuildContext context) { - return NavigationBar( + 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: (index) { + onDestinationSelected: (final index) { widget.controller!.animateTo(index); }, selectedIndex: currentIndex ?? 0, labelBehavior: NavigationDestinationLabelBehavior.onlyShowSelected, ); - } - _getIconButton(String label, IconData iconData, int index) { - return NavigationDestination( + 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 index 41436cc1..b64ed1c4 100644 --- a/lib/ui/components/brand_text/brand_text.dart +++ b/lib/ui/components/brand_text/brand_text.dart @@ -18,66 +18,10 @@ enum TextType { } class BrandText extends StatelessWidget { - const BrandText( - this.text, { - Key? key, - this.style, - required this.type, - this.overflow, - this.softWrap, - this.textAlign, - this.maxLines, - }) : super(key: key); - - final String? text; - final TextStyle? style; - final TextType type; - final TextOverflow? overflow; - final bool? softWrap; - final TextAlign? textAlign; - final int? maxLines; - - factory BrandText.h1( - String? text, { - TextStyle? style, - TextOverflow? overflow, - bool? softWrap, - }) => - BrandText( - text, - type: TextType.h1, - style: style, - ); - - factory BrandText.onboardingTitle(String text, {TextStyle? style}) => - BrandText( - text, - type: TextType.onboardingTitle, - style: style, - ); - factory BrandText.h2( - String? text, { - TextStyle? style, - TextAlign? textAlign, - }) => - BrandText( - text, - type: TextType.h2, - style: style, - textAlign: textAlign, - ); - factory BrandText.h3(String text, {TextStyle? style, TextAlign? textAlign}) => - BrandText( - text, - type: TextType.h3, - style: style, - textAlign: textAlign, - overflow: TextOverflow.ellipsis, - ); factory BrandText.h4( - String? text, { - TextStyle? style, - TextAlign? textAlign, + final String? text, { + final TextStyle? style, + final TextAlign? textAlign, }) => BrandText( text, @@ -89,10 +33,25 @@ class BrandText extends StatelessWidget { 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( - String? text, { - TextStyle? style, - TextAlign? textAlign, + final String? text, { + final TextStyle? style, + final TextAlign? textAlign, }) => BrandText( text, @@ -104,10 +63,54 @@ class BrandText extends StatelessWidget { 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( - String? text, { - TextStyle? style, - TextAlign? textAlign, + final String? text, { + final TextStyle? style, + final TextAlign? textAlign, }) => BrandText( text, @@ -115,39 +118,36 @@ class BrandText extends StatelessWidget { style: style, textAlign: textAlign, ); - factory BrandText.body1(String? text, {TextStyle? style}) => BrandText( - text, - type: TextType.body1, - style: style, - ); - factory BrandText.body2(String? text, {TextStyle? style}) => BrandText( - text, - type: TextType.body2, - style: style, - ); - factory BrandText.medium(String? text, - {TextStyle? style, TextAlign? textAlign}) => + factory BrandText.medium(final String? text, + {final TextStyle? style, final TextAlign? textAlign}) => BrandText( text, type: TextType.medium, style: style, textAlign: textAlign, ); - factory BrandText.small(String text, {TextStyle? style}) => BrandText( - text, - type: TextType.small, - style: style, - ); - factory BrandText.buttonTitleText(String? text, {TextStyle? style}) => - BrandText( - text, - type: TextType.buttonTitleText, - style: style, - ); + const BrandText( + this.text, { + super.key, + this.style, + required this.type, + 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(BuildContext context) { + Text build(final BuildContext context) { TextStyle style; - var isDark = Theme.of(context).brightness == Brightness.dark; + final bool isDark = Theme.of(context).brightness == Brightness.dark; switch (type) { case TextType.h1: style = isDark diff --git a/lib/ui/components/brand_timer/brand_timer.dart b/lib/ui/components/brand_timer/brand_timer.dart index 2aa75bce..b2ed1406 100644 --- a/lib/ui/components/brand_timer/brand_timer.dart +++ b/lib/ui/components/brand_timer/brand_timer.dart @@ -7,10 +7,10 @@ import 'package:selfprivacy/utils/named_font_weight.dart'; class BrandTimer extends StatefulWidget { const BrandTimer({ - Key? key, + super.key, required this.startDateTime, required this.duration, - }) : super(key: key); + }); final DateTime startDateTime; final Duration duration; @@ -31,8 +31,8 @@ class _BrandTimerState extends State { _timerStart() { _timeString = differenceFromStart; - timer = Timer.periodic(const Duration(seconds: 1), (Timer t) { - var timePassed = DateTime.now().difference(widget.startDateTime); + timer = Timer.periodic(const Duration(seconds: 1), (final Timer t) { + final Duration timePassed = DateTime.now().difference(widget.startDateTime); if (timePassed > widget.duration) { t.cancel(); } else { @@ -42,7 +42,7 @@ class _BrandTimerState extends State { } @override - void didUpdateWidget(BrandTimer oldWidget) { + void didUpdateWidget(final BrandTimer oldWidget) { if (timer.isActive) { timer.cancel(); } @@ -51,14 +51,12 @@ class _BrandTimerState extends State { } @override - Widget build(BuildContext context) { - return BrandText.medium( + Widget build(final BuildContext context) => BrandText.medium( _timeString, style: const TextStyle( fontWeight: NamedFontWeight.demiBold, ), ); - } void _getTime() { setState(() { @@ -69,10 +67,10 @@ class _BrandTimerState extends State { String get differenceFromStart => _durationToString(DateTime.now().difference(widget.startDateTime)); - String _durationToString(Duration duration) { - var timeLeft = widget.duration - duration; - String twoDigits(int n) => n.toString().padLeft(2, '0'); - String twoDigitSeconds = twoDigits(timeLeft.inSeconds); + String _durationToString(final Duration duration) { + final Duration timeLeft = widget.duration - duration; + String twoDigits(final int n) => n.toString().padLeft(2, '0'); + final String twoDigitSeconds = twoDigits(timeLeft.inSeconds); return 'timer.sec'.tr(args: [twoDigitSeconds]); } diff --git a/lib/ui/components/dots_indicator/dots_indicator.dart b/lib/ui/components/dots_indicator/dots_indicator.dart index e5c48bb4..fff647b7 100644 --- a/lib/ui/components/dots_indicator/dots_indicator.dart +++ b/lib/ui/components/dots_indicator/dots_indicator.dart @@ -3,19 +3,19 @@ import 'package:selfprivacy/config/brand_colors.dart'; class DotsIndicator extends StatelessWidget { const DotsIndicator({ - Key? key, required this.activeIndex, required this.count, - }) : super(key: key); + final super.key, + }); final int activeIndex; final int count; @override - Widget build(BuildContext context) { - var dots = List.generate( + Widget build(final BuildContext context) { + final List dots = List.generate( count, - (index) => Container( + (final index) => Container( margin: const EdgeInsets.symmetric(horizontal: 5, vertical: 10), height: 10, width: 10, diff --git a/lib/ui/components/error/error.dart b/lib/ui/components/error/error.dart index 64479cf7..9fe651cc 100644 --- a/lib/ui/components/error/error.dart +++ b/lib/ui/components/error/error.dart @@ -1,14 +1,13 @@ import 'package:flutter/material.dart'; class BrandError extends StatelessWidget { - const BrandError({Key? key, this.error, this.stackTrace}) : super(key: key); + const BrandError({final super.key, this.error, this.stackTrace}); final Object? error; final StackTrace? stackTrace; @override - Widget build(BuildContext context) { - return SafeArea( + Widget build(final BuildContext context) => SafeArea( child: Scaffold( body: Center( child: SingleChildScrollView( @@ -24,5 +23,4 @@ class BrandError extends StatelessWidget { ), ), ); - } } diff --git a/lib/ui/components/icon_status_mask/icon_status_mask.dart b/lib/ui/components/icon_status_mask/icon_status_mask.dart index 64f50668..b781c3d1 100644 --- a/lib/ui/components/icon_status_mask/icon_status_mask.dart +++ b/lib/ui/components/icon_status_mask/icon_status_mask.dart @@ -4,16 +4,16 @@ import 'package:selfprivacy/logic/models/state_types.dart'; class IconStatusMask extends StatelessWidget { const IconStatusMask({ - Key? key, + super.key, required this.child, required this.status, - }) : super(key: key); + }); final Icon child; final StateType status; @override - Widget build(BuildContext context) { + Widget build(final BuildContext context) { late List colors; switch (status) { case StateType.uninitialized: @@ -30,7 +30,7 @@ class IconStatusMask extends StatelessWidget { break; } return ShaderMask( - shaderCallback: (bounds) => LinearGradient( + shaderCallback: (final bounds) => LinearGradient( begin: const Alignment(-1, -0.8), end: const Alignment(0.9, 0.9), colors: colors, diff --git a/lib/ui/components/jobs_content/jobs_content.dart b/lib/ui/components/jobs_content/jobs_content.dart index cad2d7c4..e15fad3a 100644 --- a/lib/ui/components/jobs_content/jobs_content.dart +++ b/lib/ui/components/jobs_content/jobs_content.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:selfprivacy/config/brand_colors.dart'; import 'package:selfprivacy/config/brand_theme.dart'; import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/logic/cubit/jobs/jobs_cubit.dart'; @@ -14,14 +13,13 @@ import 'package:selfprivacy/ui/components/brand_loader/brand_loader.dart'; import 'package:selfprivacy/ui/components/brand_text/brand_text.dart'; class JobsContent extends StatelessWidget { - const JobsContent({Key? key}) : super(key: key); + const JobsContent({super.key}); @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { + Widget build(final BuildContext context) => BlocBuilder( + builder: (final context, final state) { late List widgets; - var installationState = context.read().state; + final ServerInstallationState installationState = context.read().state; if (state is JobsStateEmpty) { widgets = [ const SizedBox(height: 80), @@ -39,7 +37,7 @@ class JobsContent extends StatelessWidget { const SizedBox(height: 10), BrandButton.text( onPressed: () { - var nav = getIt(); + final NavigationService nav = getIt(); nav.showPopUpDialog(BrandAlert( title: 'jobs.rebootServer'.tr(), contentText: 'modals.3'.tr(), @@ -53,7 +51,7 @@ class JobsContent extends StatelessWidget { text: 'modals.9'.tr(), ) ], - )); + ),); }, title: 'jobs.rebootServer'.tr(), ), @@ -68,7 +66,7 @@ class JobsContent extends StatelessWidget { widgets = [ ...state.jobList .map( - (j) => Row( + (final j) => Row( children: [ Expanded( child: BrandCards.small( @@ -78,14 +76,18 @@ class JobsContent extends StatelessWidget { const SizedBox(width: 10), ElevatedButton( style: ElevatedButton.styleFrom( - primary: BrandColors.red1, + primary: Theme.of(context).colorScheme.errorContainer, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), ), onPressed: () => context.read().removeJob(j.id), - child: Text('basis.remove'.tr()), + child: Text('basis.remove'.tr(), + style: TextStyle( + color: Theme.of(context) + .colorScheme + .onErrorContainer,),), ), ], ), @@ -113,5 +115,4 @@ class JobsContent extends StatelessWidget { ); }, ); - } } 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 8fad8dd6..dd91f4ab 100644 --- a/lib/ui/components/not_ready_card/not_ready_card.dart +++ b/lib/ui/components/not_ready_card/not_ready_card.dart @@ -6,14 +6,13 @@ import 'package:selfprivacy/utils/route_transitions/basic.dart'; import 'package:easy_localization/easy_localization.dart'; class NotReadyCard extends StatelessWidget { - const NotReadyCard({Key? key}) : super(key: key); + const NotReadyCard({super.key}); @override - Widget build(BuildContext context) { - return Container( + Widget build(final BuildContext context) => Container( padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 10), decoration: BoxDecoration( - borderRadius: BorderRadius.circular(15), color: BrandColors.gray6), + borderRadius: BorderRadius.circular(15), color: BrandColors.gray6,), child: RichText( text: TextSpan( children: [ @@ -50,5 +49,4 @@ class NotReadyCard extends StatelessWidget { ), ), ); - } } diff --git a/lib/ui/components/one_page/one_page.dart b/lib/ui/components/one_page/one_page.dart index 30707766..66792716 100644 --- a/lib/ui/components/one_page/one_page.dart +++ b/lib/ui/components/one_page/one_page.dart @@ -6,17 +6,16 @@ import 'package:selfprivacy/ui/components/pre_styled_buttons/pre_styled_buttons. class OnePage extends StatelessWidget { const OnePage({ - Key? key, + super.key, required this.title, required this.child, - }) : super(key: key); + }); final String title; final Widget child; @override - Widget build(BuildContext context) { - return Scaffold( + Widget build(final BuildContext context) => Scaffold( appBar: PreferredSize( preferredSize: const Size.fromHeight(52), child: Column( @@ -40,10 +39,9 @@ class OnePage extends StatelessWidget { color: Theme.of(context).scaffoldBackgroundColor, alignment: Alignment.center, child: PreStyledButtons.close( - onPress: () => Navigator.of(context).pop()), + onPress: () => Navigator.of(context).pop(),), ), ), ), ); - } } diff --git a/lib/ui/components/pre_styled_buttons/close.dart b/lib/ui/components/pre_styled_buttons/close.dart index 5a9e6241..3beb2eb4 100644 --- a/lib/ui/components/pre_styled_buttons/close.dart +++ b/lib/ui/components/pre_styled_buttons/close.dart @@ -1,13 +1,12 @@ part of 'pre_styled_buttons.dart'; class _CloseButton extends StatelessWidget { - const _CloseButton({Key? key, required this.onPress}) : super(key: key); + const _CloseButton({super.key, required this.onPress}); final VoidCallback onPress; @override - Widget build(BuildContext context) { - return OutlinedButton( + Widget build(final BuildContext context) => OutlinedButton( onPressed: () => Navigator.of(context).pop(), child: Row( mainAxisSize: MainAxisSize.min, @@ -17,5 +16,4 @@ class _CloseButton extends StatelessWidget { ], ), ); - } } diff --git a/lib/ui/components/pre_styled_buttons/flash.dart b/lib/ui/components/pre_styled_buttons/flash.dart index 8a0f4b98..5d70013d 100644 --- a/lib/ui/components/pre_styled_buttons/flash.dart +++ b/lib/ui/components/pre_styled_buttons/flash.dart @@ -1,7 +1,7 @@ part of 'pre_styled_buttons.dart'; class _BrandFlashButton extends StatefulWidget { - const _BrandFlashButton({Key? key}) : super(key: key); + const _BrandFlashButton({super.key}); @override _BrandFlashButtonState createState() => _BrandFlashButtonState(); @@ -15,7 +15,7 @@ class _BrandFlashButtonState extends State<_BrandFlashButton> @override void initState() { _animationController = AnimationController( - vsync: this, duration: const Duration(milliseconds: 800)); + vsync: this, duration: const Duration(milliseconds: 800),); _colorTween = ColorTween( begin: BrandColors.black, end: BrandColors.primary, @@ -25,7 +25,7 @@ class _BrandFlashButtonState extends State<_BrandFlashButton> WidgetsBinding.instance.addPostFrameCallback(_afterLayout); } - void _afterLayout(_) { + void _afterLayout(final _) { if (Theme.of(context).brightness == Brightness.dark) { setState(() { _colorTween = ColorTween( @@ -45,9 +45,8 @@ class _BrandFlashButtonState extends State<_BrandFlashButton> bool wasPrevStateIsEmpty = true; @override - Widget build(BuildContext context) { - return BlocListener( - listener: (context, state) { + Widget build(final BuildContext context) => BlocListener( + listener: (final context, final state) { if (wasPrevStateIsEmpty && state is! JobsStateEmpty) { wasPrevStateIsEmpty = false; _animationController.forward(); @@ -61,7 +60,7 @@ class _BrandFlashButtonState extends State<_BrandFlashButton> onPressed: () { showBrandBottomSheet( context: context, - builder: (context) => const BrandBottomSheet( + builder: (final context) => const BrandBottomSheet( isExpended: true, child: JobsContent(), ), @@ -69,9 +68,9 @@ class _BrandFlashButtonState extends State<_BrandFlashButton> }, icon: AnimatedBuilder( animation: _colorTween, - builder: (context, child) { - var v = _animationController.value; - var icon = v > 0.5 ? Ionicons.flash : Ionicons.flash_outline; + builder: (final context, final 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( @@ -79,8 +78,7 @@ class _BrandFlashButtonState extends State<_BrandFlashButton> color: _colorTween.value, ), ); - }), + },), ), ); - } } diff --git a/lib/ui/components/pre_styled_buttons/flash_fab.dart b/lib/ui/components/pre_styled_buttons/flash_fab.dart index 733541c0..89f42e19 100644 --- a/lib/ui/components/pre_styled_buttons/flash_fab.dart +++ b/lib/ui/components/pre_styled_buttons/flash_fab.dart @@ -1,14 +1,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:ionicons/ionicons.dart'; -import 'package:selfprivacy/config/brand_colors.dart'; import 'package:selfprivacy/logic/cubit/jobs/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({Key? key}) : super(key: key); + const BrandFab({super.key}); @override State createState() => _BrandFabState(); @@ -22,25 +21,8 @@ class _BrandFabState extends State @override void initState() { _animationController = AnimationController( - vsync: this, duration: const Duration(milliseconds: 800)); - _colorTween = ColorTween( - begin: BrandColors.black, - end: BrandColors.primary, - ).animate(_animationController); - + vsync: this, duration: const Duration(milliseconds: 800),); super.initState(); - WidgetsBinding.instance.addPostFrameCallback(_afterLayout); - } - - void _afterLayout(_) { - if (Theme.of(context).brightness == Brightness.dark) { - setState(() { - _colorTween = ColorTween( - begin: BrandColors.white, - end: BrandColors.primary, - ).animate(_animationController); - }); - } } @override @@ -52,9 +34,14 @@ class _BrandFabState extends State bool wasPrevStateIsEmpty = true; @override - Widget build(BuildContext context) { + Widget build(final BuildContext context) { + _colorTween = ColorTween( + begin: Theme.of(context).colorScheme.onPrimaryContainer, + end: Theme.of(context).colorScheme.primary, + ).animate(_animationController); + return BlocListener( - listener: (context, state) { + listener: (final BuildContext context, final JobsState state) { if (wasPrevStateIsEmpty && state is! JobsStateEmpty) { wasPrevStateIsEmpty = false; _animationController.forward(); @@ -68,7 +55,7 @@ class _BrandFabState extends State onPressed: () { showBrandBottomSheet( context: context, - builder: (context) => const BrandBottomSheet( + builder: (final BuildContext context) => const BrandBottomSheet( isExpended: true, child: JobsContent(), ), @@ -76,9 +63,9 @@ class _BrandFabState extends State }, child: AnimatedBuilder( animation: _colorTween, - builder: (context, child) { - var v = _animationController.value; - var icon = v > 0.5 ? Ionicons.flash : Ionicons.flash_outline; + 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( @@ -86,7 +73,7 @@ class _BrandFabState extends State color: _colorTween.value, ), ); - }), + },), ), ); } diff --git a/lib/ui/components/pre_styled_buttons/pre_styled_buttons.dart b/lib/ui/components/pre_styled_buttons/pre_styled_buttons.dart index 860a83a3..cb50e2f9 100644 --- a/lib/ui/components/pre_styled_buttons/pre_styled_buttons.dart +++ b/lib/ui/components/pre_styled_buttons/pre_styled_buttons.dart @@ -14,7 +14,7 @@ part 'flash.dart'; class PreStyledButtons { static Widget close({ - required VoidCallback onPress, + required final VoidCallback onPress, }) => _CloseButton(onPress: onPress); diff --git a/lib/ui/components/progress_bar/progress_bar.dart b/lib/ui/components/progress_bar/progress_bar.dart index 4dfc10af..6e8d80c3 100644 --- a/lib/ui/components/progress_bar/progress_bar.dart +++ b/lib/ui/components/progress_bar/progress_bar.dart @@ -7,10 +7,10 @@ import 'package:selfprivacy/ui/components/brand_text/brand_text.dart'; class ProgressBar extends StatefulWidget { const ProgressBar({ - Key? key, + super.key, required this.steps, required this.activeIndex, - }) : super(key: key); + }); final int activeIndex; @@ -22,23 +22,23 @@ class ProgressBar extends StatefulWidget { class _ProgressBarState extends State { @override - Widget build(BuildContext context) { - double progress = 1 / widget.steps.length * (widget.activeIndex + 0.3); - var isDark = context.watch().state.isDarkModeOn; - var style = isDark ? progressTextStyleDark : progressTextStyleLight; + Widget build(final BuildContext context) { + final double progress = 1 / widget.steps.length * (widget.activeIndex + 0.3); + final bool isDark = context.watch().state.isDarkModeOn; + final TextStyle style = isDark ? progressTextStyleDark : progressTextStyleLight; - var allSteps = widget.steps.asMap().map( - (i, step) { - var value = _stepTitle(index: i, style: style, step: step); + final Iterable allSteps = widget.steps.asMap().map( + (final i, final step) { + final Container value = _stepTitle(index: i, style: style, step: step); return MapEntry(i, value); }, ).values; - List odd = []; - List even = []; + final List odd = []; + final List even = []; - var i = 0; - for (var step in allSteps) { + int i = 0; + for (final Container step in allSteps) { if (i.isEven) { even.add(step); } else { @@ -76,8 +76,7 @@ class _ProgressBarState extends State { borderRadius: BorderRadius.circular(5), ), child: LayoutBuilder( - builder: (_, constraints) { - return AnimatedContainer( + builder: (final _, final constraints) => AnimatedContainer( width: constraints.maxWidth * progress, height: 5, decoration: BoxDecoration( @@ -91,8 +90,7 @@ class _ProgressBarState extends State { duration: const Duration( milliseconds: 300, ), - ); - }, + ), ), ), const SizedBox(height: 5), @@ -105,12 +103,12 @@ class _ProgressBarState extends State { } Container _stepTitle({ - required int index, + required final int index, TextStyle? style, - String? step, + final String? step, }) { - var isActive = index == widget.activeIndex; - var checked = index < widget.activeIndex; + final bool isActive = index == widget.activeIndex; + final bool checked = index < widget.activeIndex; style = isActive ? style!.copyWith(fontWeight: FontWeight.w700) : style; return Container( @@ -122,13 +120,11 @@ class _ProgressBarState extends State { text: TextSpan( style: progressTextStyleLight, children: [ - checked - ? const WidgetSpan( + if (checked) const WidgetSpan( child: Padding( padding: EdgeInsets.only(bottom: 2, right: 2), child: Icon(BrandIcons.check, size: 11), - )) - : TextSpan(text: '${index + 1}.', style: style), + ),) else TextSpan(text: '${index + 1}.', style: style), TextSpan(text: step, style: style) ], ), diff --git a/lib/ui/components/switch_block/switch_bloc.dart b/lib/ui/components/switch_block/switch_bloc.dart index 3b5531cd..3aa89a33 100644 --- a/lib/ui/components/switch_block/switch_bloc.dart +++ b/lib/ui/components/switch_block/switch_bloc.dart @@ -3,24 +3,23 @@ import 'package:selfprivacy/config/brand_colors.dart'; class SwitcherBlock extends StatelessWidget { const SwitcherBlock({ - Key? key, + super.key, required this.child, required this.isActive, required this.onChange, - }) : super(key: key); + }); final Widget child; final bool isActive; final ValueChanged onChange; @override - Widget build(BuildContext context) { - return Container( + Widget build(final BuildContext context) => Container( padding: const EdgeInsets.only(top: 20, bottom: 5), decoration: const BoxDecoration( border: Border( bottom: BorderSide(width: 1, color: BrandColors.dividerColor), - )), + ),), child: Row( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -36,5 +35,4 @@ class SwitcherBlock extends StatelessWidget { ], ), ); - } } diff --git a/lib/ui/helpers/modals.dart b/lib/ui/helpers/modals.dart index 540b11ec..8867885f 100644 --- a/lib/ui/helpers/modals.dart +++ b/lib/ui/helpers/modals.dart @@ -2,8 +2,8 @@ import 'package:flutter/material.dart'; import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; Future showBrandBottomSheet({ - required BuildContext context, - required WidgetBuilder builder, + required final BuildContext context, + required final WidgetBuilder builder, }) => showCupertinoModalBottomSheet( builder: builder, diff --git a/lib/ui/pages/backup_details/backup_details.dart b/lib/ui/pages/backup_details/backup_details.dart index d36238da..f7784a18 100644 --- a/lib/ui/pages/backup_details/backup_details.dart +++ b/lib/ui/pages/backup_details/backup_details.dart @@ -13,12 +13,12 @@ import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.da import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart'; import 'package:selfprivacy/ui/components/brand_text/brand_text.dart'; -import '../../components/brand_cards/brand_cards.dart'; +import 'package:selfprivacy/ui/components/brand_cards/brand_cards.dart'; -var navigatorKey = GlobalKey(); +GlobalKey navigatorKey = GlobalKey(); class BackupDetails extends StatefulWidget { - const BackupDetails({Key? key}) : super(key: key); + const BackupDetails({super.key}); @override State createState() => _BackupDetailsState(); @@ -27,21 +27,21 @@ class BackupDetails extends StatefulWidget { class _BackupDetailsState extends State with SingleTickerProviderStateMixin { @override - Widget build(BuildContext context) { - var isReady = context.watch().state + Widget build(final BuildContext context) { + final bool isReady = context.watch().state is ServerInstallationFinished; - var isBackupInitialized = context.watch().state.isInitialized; - var backupStatus = context.watch().state.status; - var providerState = isReady && isBackupInitialized + final bool isBackupInitialized = context.watch().state.isInitialized; + final BackupStatusEnum backupStatus = context.watch().state.status; + final StateType providerState = isReady && isBackupInitialized ? (backupStatus == BackupStatusEnum.error ? StateType.warning : StateType.stable) : StateType.uninitialized; - var preventActions = context.watch().state.preventActions; - var backupProgress = context.watch().state.progress; - var backupError = context.watch().state.error; - var backups = context.watch().state.backups; - var refreshing = context.watch().state.refreshing; + final bool preventActions = context.watch().state.preventActions; + final double backupProgress = context.watch().state.progress; + final String backupError = context.watch().state.error; + final List backups = context.watch().state.backups; + final bool refreshing = context.watch().state.refreshing; return BrandHeroScreen( heroIcon: BrandIcons.save, @@ -84,7 +84,7 @@ class _BackupDetailsState extends State ListTile( title: Text( 'providers.backup.creating'.tr( - args: [(backupProgress * 100).round().toString()]), + args: [(backupProgress * 100).round().toString()],), style: Theme.of(context).textTheme.headline6, ), subtitle: LinearProgressIndicator( @@ -96,7 +96,7 @@ class _BackupDetailsState extends State ListTile( title: Text( 'providers.backup.restoring'.tr( - args: [(backupProgress * 100).round().toString()]), + args: [(backupProgress * 100).round().toString()],), style: Theme.of(context).textTheme.headline6, ), subtitle: LinearProgressIndicator( @@ -148,12 +148,11 @@ class _BackupDetailsState extends State ), if (backups.isNotEmpty) Column( - children: backups.map((backup) { - return ListTile( + children: backups.map((final Backup backup) => ListTile( onTap: preventActions ? null : () { - var nav = getIt(); + final NavigationService nav = getIt(); nav.showPopUpDialog(BrandAlert( title: 'providers.backup.restoring'.tr(), contentText: 'providers.backup.restore_alert' @@ -171,13 +170,12 @@ class _BackupDetailsState extends State text: 'modals.yes'.tr(), ) ], - )); + ),); }, title: Text( '${MaterialLocalizations.of(context).formatShortDate(backup.time)} ${TimeOfDay.fromDateTime(backup.time).format(context)}', ), - ); - }).toList(), + ),).toList(), ), ], ), diff --git a/lib/ui/pages/devices/devices.dart b/lib/ui/pages/devices/devices.dart index eb5b0531..e14f5263 100644 --- a/lib/ui/pages/devices/devices.dart +++ b/lib/ui/pages/devices/devices.dart @@ -9,7 +9,7 @@ import 'package:selfprivacy/ui/pages/devices/new_device.dart'; import 'package:selfprivacy/utils/route_transitions/basic.dart'; class DevicesScreen extends StatefulWidget { - const DevicesScreen({Key? key}) : super(key: key); + const DevicesScreen({super.key}); @override State createState() => _DevicesScreenState(); @@ -17,8 +17,8 @@ class DevicesScreen extends StatefulWidget { class _DevicesScreenState extends State { @override - Widget build(BuildContext context) { - final devicesStatus = context.watch().state; + Widget build(final BuildContext context) { + final ApiDevicesState devicesStatus = context.watch().state; return RefreshIndicator( onRefresh: () async { @@ -46,7 +46,7 @@ class _DevicesScreenState extends State { ), ), ...devicesStatus.otherDevices - .map((device) => _DeviceTile(device: device)) + .map((final device) => _DeviceTile(device: device)) .toList(), const SizedBox(height: 16), OutlinedButton( @@ -79,27 +79,24 @@ class _DevicesScreenState extends State { } class _DeviceTile extends StatelessWidget { - const _DeviceTile({Key? key, required this.device}) : super(key: key); + const _DeviceTile({super.key, required this.device}); final ApiToken device; @override - Widget build(BuildContext context) { - return ListTile( + Widget build(final BuildContext context) => ListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 0, vertical: 4), title: Text(device.name), subtitle: Text('devices.main_screen.access_granted_on' - .tr(args: [DateFormat.yMMMMd().format(device.date)])), + .tr(args: [DateFormat.yMMMMd().format(device.date)]),), onTap: device.isCaller ? null : () => _showConfirmationDialog(context, device), ); - } - _showConfirmationDialog(BuildContext context, ApiToken device) => showDialog( + Future _showConfirmationDialog(final BuildContext context, final ApiToken device) => showDialog( context: context, - builder: (context) { - return AlertDialog( + builder: (final context) => AlertDialog( title: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ @@ -119,7 +116,7 @@ class _DeviceTile extends StatelessWidget { Text( 'devices.revoke_device_alert.description' .tr(args: [device.name]), - style: Theme.of(context).textTheme.bodyMedium), + style: Theme.of(context).textTheme.bodyMedium,), ], ), actions: [ @@ -137,7 +134,6 @@ class _DeviceTile extends StatelessWidget { }, ), ], - ); - }, + ), ); } diff --git a/lib/ui/pages/devices/new_device.dart b/lib/ui/pages/devices/new_device.dart index 7929b73e..4a152380 100644 --- a/lib/ui/pages/devices/new_device.dart +++ b/lib/ui/pages/devices/new_device.dart @@ -7,11 +7,10 @@ import 'package:selfprivacy/ui/components/brand_button/filled_button.dart'; import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; class NewDeviceScreen extends StatelessWidget { - const NewDeviceScreen({Key? key}) : super(key: key); + const NewDeviceScreen({super.key}); @override - Widget build(BuildContext context) { - return BrandHeroScreen( + Widget build(final BuildContext context) => BrandHeroScreen( heroTitle: 'devices.add_new_device_screen.header'.tr(), heroSubtitle: 'devices.add_new_device_screen.description'.tr(), hasBackButton: true, @@ -19,7 +18,7 @@ class NewDeviceScreen extends StatelessWidget { children: [ FutureBuilder( future: context.read().getNewDeviceKey(), - builder: (context, snapshot) { + builder: (final BuildContext context, final AsyncSnapshot snapshot) { if (snapshot.hasData) { return _KeyDisplay( newDeviceKey: snapshot.data.toString(), @@ -31,16 +30,14 @@ class NewDeviceScreen extends StatelessWidget { ), ], ); - } } class _KeyDisplay extends StatelessWidget { - const _KeyDisplay({Key? key, required this.newDeviceKey}) : super(key: key); + const _KeyDisplay({super.key, required this.newDeviceKey}); final String newDeviceKey; @override - Widget build(BuildContext context) { - return Column( + Widget build(final BuildContext context) => Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Divider(), @@ -80,5 +77,4 @@ class _KeyDisplay extends StatelessWidget { const SizedBox(height: 24), ], ); - } } diff --git a/lib/ui/pages/dns_details/dns_details.dart b/lib/ui/pages/dns_details/dns_details.dart index b891c4d1..3e3cc67a 100644 --- a/lib/ui/pages/dns_details/dns_details.dart +++ b/lib/ui/pages/dns_details/dns_details.dart @@ -8,17 +8,17 @@ import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.da import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart'; class DnsDetailsPage extends StatefulWidget { - const DnsDetailsPage({Key? key}) : super(key: key); + const DnsDetailsPage({super.key}); @override State createState() => _DnsDetailsPageState(); } class _DnsDetailsPageState extends State { - Widget _getStateCard(DnsRecordsStatus dnsState, Function fixCallback) { - var description = ''; - var subtitle = ''; - var icon = const Icon( + Widget _getStateCard(final DnsRecordsStatus dnsState, final Function fixCallback) { + String description = ''; + String subtitle = ''; + Icon icon = const Icon( Icons.check, color: Colors.green, ); @@ -63,11 +63,11 @@ class _DnsDetailsPageState extends State { } @override - Widget build(BuildContext context) { - var isReady = context.watch().state + Widget build(final BuildContext context) { + final bool isReady = context.watch().state is ServerInstallationFinished; - final domain = getIt().serverDomain?.domainName ?? ''; - var dnsCubit = context.watch().state; + final String domain = getIt().serverDomain?.domainName ?? ''; + final DnsRecordsState dnsCubit = context.watch().state; print(dnsCubit.dnsState); @@ -124,11 +124,11 @@ class _DnsDetailsPageState extends State { ), ...dnsCubit.dnsRecords .where( - (dnsRecord) => + (final dnsRecord) => dnsRecord.category == DnsRecordsCategory.services, ) .map( - (dnsRecord) => Column( + (final dnsRecord) => Column( children: [ const Divider( height: 1.0, @@ -180,11 +180,11 @@ class _DnsDetailsPageState extends State { ), ...dnsCubit.dnsRecords .where( - (dnsRecord) => + (final dnsRecord) => dnsRecord.category == DnsRecordsCategory.email, ) .map( - (dnsRecord) => Column( + (final dnsRecord) => Column( children: [ const Divider( height: 1.0, diff --git a/lib/ui/pages/more/about/about.dart b/lib/ui/pages/more/about/about.dart index 53faa191..bc1a3819 100644 --- a/lib/ui/pages/more/about/about.dart +++ b/lib/ui/pages/more/about/about.dart @@ -4,21 +4,19 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:selfprivacy/ui/components/brand_md/brand_md.dart'; class AboutPage extends StatelessWidget { - const AboutPage({Key? key}) : super(key: key); + const AboutPage({super.key}); @override - Widget build(BuildContext context) { - return SafeArea( + Widget build(final BuildContext context) => SafeArea( child: Scaffold( appBar: PreferredSize( preferredSize: const Size.fromHeight(52), child: BrandHeader( - title: 'more.about_project'.tr(), hasBackButton: true), + title: 'more.about_project'.tr(), hasBackButton: true,), ), body: const BrandMarkdown( fileName: 'about', ), ), ); - } } diff --git a/lib/ui/pages/more/app_settings/app_setting.dart b/lib/ui/pages/more/app_settings/app_setting.dart index ac4cbc49..1dc50416 100644 --- a/lib/ui/pages/more/app_settings/app_setting.dart +++ b/lib/ui/pages/more/app_settings/app_setting.dart @@ -13,7 +13,7 @@ import 'package:selfprivacy/utils/named_font_weight.dart'; import 'package:easy_localization/easy_localization.dart'; class AppSettingsPage extends StatefulWidget { - const AppSettingsPage({Key? key}) : super(key: key); + const AppSettingsPage({super.key}); @override State createState() => _AppSettingsPageState(); @@ -21,16 +21,15 @@ class AppSettingsPage extends StatefulWidget { class _AppSettingsPageState extends State { @override - Widget build(BuildContext context) { - var isDarkModeOn = context.watch().state.isDarkModeOn; + Widget build(final BuildContext context) { + final bool isDarkModeOn = context.watch().state.isDarkModeOn; return SafeArea( - child: Builder(builder: (context) { - return Scaffold( + child: Builder(builder: (final context) => Scaffold( appBar: PreferredSize( preferredSize: const Size.fromHeight(52), child: BrandHeader( - title: 'more.settings.title'.tr(), hasBackButton: true), + title: 'more.settings.title'.tr(), hasBackButton: true,), ), body: ListView( padding: paddingH15V0, @@ -41,7 +40,7 @@ class _AppSettingsPageState extends State { decoration: const BoxDecoration( border: Border( bottom: BorderSide(width: 1, color: BrandColors.dividerColor), - )), + ),), child: Row( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -56,7 +55,7 @@ class _AppSettingsPageState extends State { const SizedBox(width: 5), BrandSwitch( value: Theme.of(context).brightness == Brightness.dark, - onChanged: (value) => context + onChanged: (final value) => context .read() .updateDarkMode(isDarkModeOn: !isDarkModeOn), ), @@ -68,7 +67,7 @@ class _AppSettingsPageState extends State { decoration: const BoxDecoration( border: Border( bottom: BorderSide(width: 1, color: BrandColors.dividerColor), - )), + ),), child: Row( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -95,8 +94,7 @@ class _AppSettingsPageState extends State { onPressed: () { showDialog( context: context, - builder: (_) { - return BrandAlert( + builder: (final _) => BrandAlert( title: 'modals.3'.tr(), contentText: 'modals.4'.tr(), actions: [ @@ -108,13 +106,12 @@ class _AppSettingsPageState extends State { .read() .clearAppConfig(); Navigator.of(context).pop(); - }), + },), ActionButton( text: 'basis.cancel'.tr(), ), ], - ); - }, + ), ); }, ), @@ -124,20 +121,19 @@ class _AppSettingsPageState extends State { deleteServer(context) ], ), - ); - }), + ),), ); } - Widget deleteServer(BuildContext context) { - var isDisabled = + Widget deleteServer(final BuildContext context) { + final bool isDisabled = context.watch().state.serverDetails == null; return Container( padding: const EdgeInsets.only(top: 20, bottom: 5), decoration: const BoxDecoration( border: Border( bottom: BorderSide(width: 1, color: BrandColors.dividerColor), - )), + ),), child: Row( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -159,8 +155,7 @@ class _AppSettingsPageState extends State { : () { showDialog( context: context, - builder: (_) { - return BrandAlert( + builder: (final _) => BrandAlert( title: 'modals.3'.tr(), contentText: 'modals.6'.tr(), actions: [ @@ -170,25 +165,22 @@ class _AppSettingsPageState extends State { onPressed: () async { showDialog( context: context, - builder: (context) { - return Container( + 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( @@ -207,18 +199,17 @@ class _AppSettingsPageState extends State { class _TextColumn extends StatelessWidget { const _TextColumn({ - Key? key, + super.key, required this.title, required this.value, this.hasWarning = false, - }) : super(key: key); + }); final String title; final String value; final bool hasWarning; @override - Widget build(BuildContext context) { - return Column( + Widget build(final BuildContext context) => Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ BrandText.body1( @@ -231,8 +222,7 @@ class _TextColumn extends StatelessWidget { fontSize: 13, height: 1.53, color: BrandColors.gray1, - ).merge(TextStyle(color: hasWarning ? BrandColors.warning : null))), + ).merge(TextStyle(color: hasWarning ? BrandColors.warning : null)),), ], ); - } } diff --git a/lib/ui/pages/more/console/console.dart b/lib/ui/pages/more/console/console.dart index 1c77e6bf..a3d046b6 100644 --- a/lib/ui/pages/more/console/console.dart +++ b/lib/ui/pages/more/console/console.dart @@ -8,7 +8,7 @@ import 'package:selfprivacy/ui/components/brand_divider/brand_divider.dart'; import 'package:selfprivacy/ui/components/brand_header/brand_header.dart'; class Console extends StatefulWidget { - const Console({Key? key}) : super(key: key); + const Console({super.key}); @override State createState() => _ConsoleState(); @@ -31,8 +31,7 @@ class _ConsoleState extends State { void update() => setState(() => {}); @override - Widget build(BuildContext context) { - return SafeArea( + Widget build(final BuildContext context) => SafeArea( child: Scaffold( appBar: PreferredSize( preferredSize: const Size.fromHeight(53), @@ -45,9 +44,9 @@ class _ConsoleState extends State { ), body: FutureBuilder( future: getIt.allReady(), - builder: (BuildContext context, AsyncSnapshot snapshot) { + builder: (final BuildContext context, final AsyncSnapshot snapshot) { if (snapshot.hasData) { - var messages = getIt.get().messages; + final List messages = getIt.get().messages; return ListView( reverse: true, @@ -55,8 +54,8 @@ class _ConsoleState extends State { children: [ const SizedBox(height: 20), ...UnmodifiableListView(messages - .map((message) { - var isError = message.type == MessageType.warning; + .map((final message) { + final bool isError = message.type == MessageType.warning; return Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: RichText( @@ -69,7 +68,7 @@ class _ConsoleState extends State { style: TextStyle( fontWeight: FontWeight.bold, color: - isError ? BrandColors.red1 : null)), + isError ? BrandColors.red1 : null,),), TextSpan(text: message.text), ], ), @@ -77,7 +76,7 @@ class _ConsoleState extends State { ); }) .toList() - .reversed), + .reversed,), ], ); } else { @@ -97,5 +96,4 @@ class _ConsoleState extends State { ), ), ); - } } diff --git a/lib/ui/pages/more/info/info.dart b/lib/ui/pages/more/info/info.dart index 639fab4e..baa82021 100644 --- a/lib/ui/pages/more/info/info.dart +++ b/lib/ui/pages/more/info/info.dart @@ -7,11 +7,10 @@ import 'package:package_info/package_info.dart'; import 'package:easy_localization/easy_localization.dart'; class InfoPage extends StatelessWidget { - const InfoPage({Key? key}) : super(key: key); + const InfoPage({super.key}); @override - Widget build(BuildContext context) { - return SafeArea( + Widget build(final BuildContext context) => SafeArea( child: Scaffold( appBar: PreferredSize( preferredSize: const Size.fromHeight(52), @@ -24,18 +23,15 @@ class InfoPage extends StatelessWidget { const SizedBox(height: 10), FutureBuilder( future: _version(), - builder: (context, snapshot) { - return BrandText.body1('more.about_app_page.text' - .tr(args: [snapshot.data.toString()])); - }), + builder: (final context, final snapshot) => BrandText.body1('more.about_app_page.text' + .tr(args: [snapshot.data.toString()]),),), ], ), ), ); - } Future _version() async { - var packageInfo = await PackageInfo.fromPlatform(); + final PackageInfo packageInfo = await PackageInfo.fromPlatform(); return packageInfo.version; } } diff --git a/lib/ui/pages/more/more.dart b/lib/ui/pages/more/more.dart index 334d0707..eee3398e 100644 --- a/lib/ui/pages/more/more.dart +++ b/lib/ui/pages/more/more.dart @@ -14,18 +14,18 @@ import 'package:selfprivacy/ui/pages/root_route.dart'; import 'package:selfprivacy/ui/pages/ssh_keys/ssh_keys.dart'; import 'package:selfprivacy/utils/route_transitions/basic.dart'; -import '../../../logic/cubit/users/users_cubit.dart'; -import 'about/about.dart'; -import 'app_settings/app_setting.dart'; -import 'console/console.dart'; -import 'info/info.dart'; +import 'package:selfprivacy/logic/cubit/users/users_cubit.dart'; +import 'package:selfprivacy/ui/pages/more/about/about.dart'; +import 'package:selfprivacy/ui/pages/more/app_settings/app_setting.dart'; +import 'package:selfprivacy/ui/pages/more/console/console.dart'; +import 'package:selfprivacy/ui/pages/more/info/info.dart'; class MorePage extends StatelessWidget { - const MorePage({Key? key}) : super(key: key); + const MorePage({super.key}); @override - Widget build(BuildContext context) { - var isReady = context.watch().state + Widget build(final BuildContext context) { + final bool isReady = context.watch().state is ServerInstallationFinished; return Scaffold( @@ -55,7 +55,7 @@ class MorePage extends StatelessWidget { iconData: Ionicons.key_outline, goTo: SshKeysPage( user: context.read().state.rootUser, - )), + ),), if (isReady) _MoreMenuItem( iconData: Icons.password_outlined, @@ -105,13 +105,13 @@ class MorePage extends StatelessWidget { class _MoreMenuItem extends StatelessWidget { const _MoreMenuItem({ - Key? key, + super.key, required this.iconData, required this.title, this.subtitle, this.goTo, this.accent = false, - }) : super(key: key); + }); final IconData iconData; final String title; @@ -120,8 +120,8 @@ class _MoreMenuItem extends StatelessWidget { final bool accent; @override - Widget build(BuildContext context) { - final color = accent + Widget build(final BuildContext context) { + final Color color = accent ? Theme.of(context).colorScheme.onTertiaryContainer : Theme.of(context).colorScheme.onSurface; return BrandCards.filled( diff --git a/lib/ui/pages/onboarding/onboarding.dart b/lib/ui/pages/onboarding/onboarding.dart index 4530c746..364f5380 100644 --- a/lib/ui/pages/onboarding/onboarding.dart +++ b/lib/ui/pages/onboarding/onboarding.dart @@ -6,7 +6,7 @@ import 'package:selfprivacy/utils/route_transitions/basic.dart'; import 'package:easy_localization/easy_localization.dart'; class OnboardingPage extends StatefulWidget { - const OnboardingPage({Key? key, required this.nextPage}) : super(key: key); + const OnboardingPage({super.key, required this.nextPage}); final Widget nextPage; @override @@ -22,8 +22,7 @@ class _OnboardingPageState extends State { } @override - Widget build(BuildContext context) { - return SafeArea( + Widget build(final BuildContext context) => SafeArea( child: Scaffold( body: PageView( controller: pageController, @@ -34,19 +33,15 @@ class _OnboardingPageState extends State { ), ), ); - } - Widget _withPadding(Widget child) { - return Padding( + Widget _withPadding(final Widget child) => Padding( padding: const EdgeInsets.symmetric( horizontal: 15, ), child: child, ); - } - Widget firstPage() { - return ConstrainedBox( + Widget firstPage() => ConstrainedBox( constraints: BoxConstraints( maxHeight: MediaQuery.of(context).size.height, ), @@ -85,10 +80,8 @@ class _OnboardingPageState extends State { ], ), ); - } - Widget secondPage() { - return ConstrainedBox( + Widget secondPage() => ConstrainedBox( constraints: BoxConstraints( maxHeight: MediaQuery.of(context).size.height, ), @@ -126,7 +119,7 @@ class _OnboardingPageState extends State { context.read().turnOffOnboarding(); Navigator.of(context).pushAndRemoveUntil( materialRoute(widget.nextPage), - (route) => false, + (final route) => false, ); }, text: 'basis.got_it'.tr(), @@ -135,16 +128,15 @@ class _OnboardingPageState extends State { ], ), ); - } } String _fileName({ - required BuildContext context, - required String path, - required String fileName, - required String fileExtention, + required final BuildContext context, + required final String path, + required final String fileName, + required final String fileExtention, }) { - var theme = Theme.of(context); - var isDark = theme.brightness == Brightness.dark; + final ThemeData theme = Theme.of(context); + final bool isDark = theme.brightness == Brightness.dark; return '$path/$fileName${isDark ? '-dark' : '-light'}.$fileExtention'; } diff --git a/lib/ui/pages/providers/providers.dart b/lib/ui/pages/providers/providers.dart index 2abed128..7168689a 100644 --- a/lib/ui/pages/providers/providers.dart +++ b/lib/ui/pages/providers/providers.dart @@ -18,10 +18,10 @@ 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'; -var navigatorKey = GlobalKey(); +GlobalKey navigatorKey = GlobalKey(); class ProvidersPage extends StatefulWidget { - const ProvidersPage({Key? key}) : super(key: key); + const ProvidersPage({super.key}); @override State createState() => _ProvidersPageState(); @@ -29,11 +29,11 @@ class ProvidersPage extends StatefulWidget { class _ProvidersPageState extends State { @override - Widget build(BuildContext context) { - var isReady = context.watch().state + Widget build(final BuildContext context) { + final bool isReady = context.watch().state is ServerInstallationFinished; - var isBackupInitialized = context.watch().state.isInitialized; - var dnsStatus = context.watch().state.dnsState; + final bool isBackupInitialized = context.watch().state.isInitialized; + final DnsRecordsStatus dnsStatus = context.watch().state.dnsState; StateType getDnsStatus() { if (dnsStatus == DnsRecordsStatus.uninitialized || @@ -46,9 +46,9 @@ class _ProvidersPageState extends State { return StateType.stable; } - final cards = ProviderType.values + final List cards = ProviderType.values .map( - (type) => Padding( + (final ProviderType type) => Padding( padding: const EdgeInsets.only(bottom: 30), child: _Card( provider: ProviderModel( @@ -87,21 +87,21 @@ class _ProvidersPageState extends State { } class _Card extends StatelessWidget { - const _Card({Key? key, required this.provider}) : super(key: key); + const _Card({super.key, required this.provider}); final ProviderModel provider; @override - Widget build(BuildContext context) { + Widget build(final BuildContext context) { late String title; String? message; late String stableText; late VoidCallback onTap; - var isReady = context.watch().state + final bool isReady = context.watch().state is ServerInstallationFinished; - ServerInstallationState appConfig = + final ServerInstallationState appConfig = context.watch().state; - var domainName = + final String domainName = appConfig.isDomainFilled ? appConfig.serverDomain!.domainName : ''; switch (provider.type) { @@ -110,7 +110,7 @@ class _Card extends StatelessWidget { stableText = 'providers.server.status'.tr(); onTap = () => showBrandBottomSheet( context: context, - builder: (context) => const BrandBottomSheet( + builder: (final BuildContext context) => const BrandBottomSheet( isExpended: true, child: ServerDetailsScreen(), ), @@ -124,7 +124,7 @@ class _Card extends StatelessWidget { onTap = () => Navigator.of(context).push(materialRoute( const DnsDetailsPage(), - )); + ),); break; case ProviderType.backup: title = 'providers.backup.card_title'.tr(); @@ -132,7 +132,7 @@ class _Card extends StatelessWidget { onTap = () => Navigator.of(context).push(materialRoute( const BackupDetails(), - )); + ),); break; } return GestureDetector( diff --git a/lib/ui/pages/recovery_key/recovery_key.dart b/lib/ui/pages/recovery_key/recovery_key.dart index 810f3080..9d329a20 100644 --- a/lib/ui/pages/recovery_key/recovery_key.dart +++ b/lib/ui/pages/recovery_key/recovery_key.dart @@ -10,12 +10,11 @@ import 'package:selfprivacy/ui/components/brand_button/brand_button.dart'; import 'package:selfprivacy/ui/components/brand_button/filled_button.dart'; import 'package:selfprivacy/ui/components/brand_cards/brand_cards.dart'; import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; -import 'package:selfprivacy/ui/components/brand_text/brand_text.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({Key? key}) : super(key: key); + const RecoveryKey({super.key}); @override State createState() => _RecoveryKeyState(); @@ -23,8 +22,8 @@ class RecoveryKey extends StatefulWidget { class _RecoveryKeyState extends State { @override - Widget build(BuildContext context) { - final keyStatus = context.watch().state; + Widget build(final BuildContext context) { + final RecoveryKeyState keyStatus = context.watch().state; final List widgets; String? subtitle = @@ -62,7 +61,7 @@ class _RecoveryKeyState extends State { } class RecoveryKeyContent extends StatefulWidget { - const RecoveryKeyContent({Key? key}) : super(key: key); + const RecoveryKeyContent({super.key}); @override State createState() => _RecoveryKeyContentState(); @@ -72,8 +71,8 @@ class _RecoveryKeyContentState extends State { bool _isConfigurationVisible = false; @override - Widget build(BuildContext context) { - final keyStatus = context.watch().state; + Widget build(final BuildContext context) { + final RecoveryKeyState keyStatus = context.watch().state; return Column( children: [ @@ -108,14 +107,12 @@ class _RecoveryKeyContentState extends State { } class RecoveryKeyStatusCard extends StatelessWidget { - const RecoveryKeyStatusCard({required this.isValid, Key? key}) - : super(key: key); + const RecoveryKeyStatusCard({required this.isValid, super.key}); final bool isValid; @override - Widget build(BuildContext context) { - return BrandCards.filled( + Widget build(final BuildContext context) => BrandCards.filled( child: ListTile( title: isValid ? Text( @@ -144,18 +141,16 @@ class RecoveryKeyStatusCard extends StatelessWidget { : Theme.of(context).colorScheme.errorContainer, ), ); - } } class RecoveryKeyInformation extends StatelessWidget { - const RecoveryKeyInformation({required this.state, Key? key}) - : super(key: key); + const RecoveryKeyInformation({required this.state, super.key}); final RecoveryKeyState state; @override - Widget build(BuildContext context) { - const padding = EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0); + Widget build(final BuildContext context) { + const EdgeInsets padding = EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0); return SizedBox( width: double.infinity, child: Column( @@ -196,7 +191,7 @@ class RecoveryKeyInformation extends StatelessWidget { } class RecoveryKeyConfiguration extends StatefulWidget { - const RecoveryKeyConfiguration({Key? key}) : super(key: key); + const RecoveryKeyConfiguration({super.key}); @override State createState() => _RecoveryKeyConfigurationState(); @@ -222,7 +217,7 @@ class _RecoveryKeyConfigurationState extends State { _isLoading = true; }); try { - final token = await context.read().generateRecoveryKey( + final String token = await context.read().generateRecoveryKey( numberOfUses: _isAmountToggled ? int.tryParse(_amountController.text) : null, expirationDate: _isExpirationToggled ? _selectedDate : null, @@ -248,8 +243,8 @@ class _RecoveryKeyConfigurationState extends State { } void _updateErrorStatuses() { - final amount = _amountController.text; - final expiration = _expirationController.text; + final String amount = _amountController.text; + final String expiration = _expirationController.text; print('amount: $amount'); print('_isAmountToggled: $_isAmountToggled'); @@ -261,7 +256,7 @@ class _RecoveryKeyConfigurationState extends State { } else if (amount.isEmpty) { _isAmountError = true; } else { - final amountInt = int.tryParse(amount); + final int? amountInt = int.tryParse(amount); _isAmountError = amountInt == null || amountInt <= 0; } @@ -279,7 +274,7 @@ class _RecoveryKeyConfigurationState extends State { } @override - Widget build(BuildContext context) { + Widget build(final BuildContext context) { if (_isDateSelected) { _expirationController.text = DateFormat.yMMMMd().format(_selectedDate); } @@ -294,7 +289,7 @@ class _RecoveryKeyConfigurationState extends State { value: _isAmountToggled, title: Text('recovery_key.key_amount_toggle'.tr()), activeColor: Theme.of(context).colorScheme.primary, - onChanged: (bool toggled) { + onChanged: (final bool toggled) { setState( () { _isAmountToggled = toggled; @@ -317,7 +312,7 @@ class _RecoveryKeyConfigurationState extends State { decoration: InputDecoration( border: const OutlineInputBorder(), errorText: _isAmountError ? ' ' : null, - labelText: 'recovery_key.key_amount_field_title'.tr()), + labelText: 'recovery_key.key_amount_field_title'.tr(),), keyboardType: TextInputType.number, inputFormatters: [ FilteringTextInputFormatter.digitsOnly, @@ -332,7 +327,7 @@ class _RecoveryKeyConfigurationState extends State { value: _isExpirationToggled, title: Text('recovery_key.key_duedate_toggle'.tr()), activeColor: Theme.of(context).colorScheme.primary, - onChanged: (bool toggled) { + onChanged: (final bool toggled) { setState( () { _isExpirationToggled = toggled; @@ -359,7 +354,7 @@ class _RecoveryKeyConfigurationState extends State { decoration: InputDecoration( border: const OutlineInputBorder(), errorText: _isExpirationError ? ' ' : null, - labelText: 'recovery_key.key_duedate_field_title'.tr()), + labelText: 'recovery_key.key_duedate_field_title'.tr(),), keyboardType: TextInputType.number, inputFormatters: [ FilteringTextInputFormatter.digitsOnly, @@ -381,12 +376,12 @@ class _RecoveryKeyConfigurationState extends State { ); } - Future _selectDate(BuildContext context) async { - final selected = await showDatePicker( + Future _selectDate(final BuildContext context) async { + final DateTime? selected = await showDatePicker( context: context, initialDate: _selectedDate, firstDate: DateTime.now(), - lastDate: DateTime(DateTime.now().year + 50)); + lastDate: DateTime(DateTime.now().year + 50),); if (selected != null && selected != _selectedDate) { setState( diff --git a/lib/ui/pages/recovery_key/recovery_key_receiving.dart b/lib/ui/pages/recovery_key/recovery_key_receiving.dart index 7356864d..f695dd47 100644 --- a/lib/ui/pages/recovery_key/recovery_key_receiving.dart +++ b/lib/ui/pages/recovery_key/recovery_key_receiving.dart @@ -4,14 +4,12 @@ import 'package:selfprivacy/ui/components/brand_button/filled_button.dart'; import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; class RecoveryKeyReceiving extends StatelessWidget { - const RecoveryKeyReceiving({required this.recoveryKey, Key? key}) - : super(key: key); + const RecoveryKeyReceiving({required this.recoveryKey, super.key}); final String recoveryKey; @override - Widget build(BuildContext context) { - return BrandHeroScreen( + Widget build(final BuildContext context) => BrandHeroScreen( heroTitle: 'recovery_key.key_main_header'.tr(), heroSubtitle: 'recovery_key.key_receiving_description'.tr(), hasBackButton: true, @@ -42,10 +40,9 @@ class RecoveryKeyReceiving extends StatelessWidget { FilledButton( title: 'recovery_key.key_receiving_done'.tr(), onPressed: () { - Navigator.of(context).popUntil((route) => route.isFirst); + Navigator.of(context).popUntil((final route) => route.isFirst); }, ), ], ); - } } diff --git a/lib/ui/pages/root_route.dart b/lib/ui/pages/root_route.dart index 9e0fcf67..fb4efeed 100644 --- a/lib/ui/pages/root_route.dart +++ b/lib/ui/pages/root_route.dart @@ -6,10 +6,10 @@ 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 '../components/pre_styled_buttons/flash_fab.dart'; +import 'package:selfprivacy/ui/components/pre_styled_buttons/flash_fab.dart'; class RootPage extends StatefulWidget { - const RootPage({Key? key}) : super(key: key); + const RootPage({super.key}); @override State createState() => _RootPageState(); @@ -48,13 +48,13 @@ class _RootPageState extends State with TickerProviderStateMixin { } @override - Widget build(BuildContext context) { - var isReady = context.watch().state + Widget build(final BuildContext context) { + final bool isReady = context.watch().state is ServerInstallationFinished; return SafeArea( child: Provider( - create: (_) => ChangeTab(tabController.animateTo), + create: (final _) => ChangeTab(tabController.animateTo), child: Scaffold( body: TabBarView( controller: tabController, @@ -92,7 +92,7 @@ class _RootPageState extends State with TickerProviderStateMixin { } class ChangeTab { - final ValueChanged onPress; ChangeTab(this.onPress); + final ValueChanged onPress; } diff --git a/lib/ui/pages/server_details/chart.dart b/lib/ui/pages/server_details/chart.dart index bc94bac4..0091b6b1 100644 --- a/lib/ui/pages/server_details/chart.dart +++ b/lib/ui/pages/server_details/chart.dart @@ -1,13 +1,13 @@ part of 'server_details_screen.dart'; class _Chart extends StatelessWidget { - const _Chart({Key? key}) : super(key: key); + const _Chart({final super.key}); @override - Widget build(BuildContext context) { - var cubit = context.watch(); - var period = cubit.state.period; - var state = cubit.state; + Widget build(final BuildContext context) { + final HetznerMetricsCubit cubit = context.watch(); + final Period period = cubit.state.period; + final HetznerMetricsState state = cubit.state; List charts; if (state is HetznerMetricsLoading) { charts = [ @@ -85,8 +85,8 @@ class _Chart extends StatelessWidget { ); } - Widget getCpuChart(HetznerMetricsLoaded state) { - var data = state.cpu; + Widget getCpuChart(final HetznerMetricsLoaded state) { + final data = state.cpu; return SizedBox( height: 200, @@ -98,9 +98,9 @@ class _Chart extends StatelessWidget { ); } - Widget getPpsChart(HetznerMetricsLoaded state) { - var ppsIn = state.ppsIn; - var ppsOut = state.ppsOut; + Widget getPpsChart(final HetznerMetricsLoaded state) { + final ppsIn = state.ppsIn; + final ppsOut = state.ppsOut; return SizedBox( height: 200, @@ -112,9 +112,9 @@ class _Chart extends StatelessWidget { ); } - Widget getBandwidthChart(HetznerMetricsLoaded state) { - var ppsIn = state.bandwidthIn; - var ppsOut = state.bandwidthOut; + Widget getBandwidthChart(final HetznerMetricsLoaded state) { + final ppsIn = state.bandwidthIn; + final ppsOut = state.bandwidthOut; return SizedBox( height: 200, @@ -129,7 +129,7 @@ class _Chart extends StatelessWidget { class Legend extends StatelessWidget { const Legend({ - Key? key, + final Key? key, required this.color, required this.text, }) : super(key: key); @@ -137,7 +137,7 @@ class Legend extends StatelessWidget { final String text; final Color color; @override - Widget build(BuildContext context) { + Widget build(final BuildContext context) { return Row( mainAxisAlignment: MainAxisAlignment.end, children: [ @@ -151,14 +151,14 @@ class Legend extends StatelessWidget { class _ColoredBox extends StatelessWidget { const _ColoredBox({ - Key? key, + final Key? key, required this.color, }) : super(key: key); final Color color; @override - Widget build(BuildContext context) { + Widget build(final BuildContext context) { return Container( width: 10, height: 10, diff --git a/lib/ui/pages/setup/initializing.dart b/lib/ui/pages/setup/initializing.dart index 98853c8f..e3fd0e77 100644 --- a/lib/ui/pages/setup/initializing.dart +++ b/lib/ui/pages/setup/initializing.dart @@ -326,7 +326,7 @@ class InitializingPage extends StatelessWidget { ), ), SizedBox( - width: 50, + width: 56, child: BrandButton.rised( onPressed: () => context.read().load(), child: Row( @@ -422,7 +422,7 @@ class InitializingPage extends StatelessWidget { .setValue(!isVisible), ), suffixIconConstraints: const BoxConstraints(minWidth: 60), - prefixIconConstraints: const BoxConstraints(maxWidth: 85), + prefixIconConstraints: const BoxConstraints(maxWidth: 60), prefixIcon: Container(), ), ); 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 7496ef37..1c02f34d 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,3 +1,5 @@ +// ignore_for_file: always_specify_types + 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'; @@ -36,24 +38,24 @@ class RecoverByNewDeviceKeyInput extends StatelessWidget { @override Widget build(BuildContext context) { - var appConfig = context.watch(); + ServerInstallationCubit appConfig = context.watch(); return BlocProvider( - create: (context) => RecoveryDeviceFormCubit( + create: (BuildContext context) => RecoveryDeviceFormCubit( appConfig, FieldCubitFactory(context), ServerRecoveryMethods.newDeviceKey, ), child: BlocListener( - listener: (context, state) { + listener: (BuildContext context, ServerInstallationState state) { if (state is ServerInstallationRecovery && state.currentStep != RecoveryStep.newDeviceKey) { Navigator.of(context).pop(); } }, child: Builder( - builder: (context) { - var formCubitState = context.watch().state; + builder: (BuildContext context) { + FormCubitState formCubitState = context.watch().state; return BrandHeroScreen( heroTitle: 'recovering.recovery_main_header'.tr(), 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 46c1b3b4..fa96c04e 100644 --- a/lib/ui/pages/setup/recovering/recover_by_old_token.dart +++ b/lib/ui/pages/setup/recovering/recover_by_old_token.dart @@ -1,3 +1,5 @@ +// ignore_for_file: always_specify_types + 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'; @@ -11,13 +13,11 @@ import 'package:selfprivacy/logic/cubit/forms/factories/field_cubit_factory.dart class RecoverByOldTokenInstruction extends StatelessWidget { @override const RecoverByOldTokenInstruction( - {Key? key, required this.instructionFilename}) - : super(key: key); + {super.key, required this.instructionFilename,}); @override - Widget build(BuildContext context) { - return BlocListener( - listener: (context, state) { + Widget build(final BuildContext context) => BlocListener( + listener: (final context, final state) { if (state is ServerInstallationRecovery && state.currentStep != RecoveryStep.selecting) { Navigator.of(context).pop(); @@ -43,27 +43,26 @@ class RecoverByOldTokenInstruction extends StatelessWidget { ], ), ); - } final String instructionFilename; } class RecoverByOldToken extends StatelessWidget { - const RecoverByOldToken({Key? key}) : super(key: key); + const RecoverByOldToken({super.key}); @override - Widget build(BuildContext context) { - var appConfig = context.watch(); + Widget build(final BuildContext context) { + final ServerInstallationCubit appConfig = context.watch(); return BlocProvider( - create: (context) => RecoveryDeviceFormCubit( + create: (final context) => RecoveryDeviceFormCubit( appConfig, FieldCubitFactory(context), ServerRecoveryMethods.oldToken, ), child: Builder( - builder: (context) { - var formCubitState = context.watch().state; + builder: (final context) { + final FormCubitState formCubitState = context.watch().state; return BrandHeroScreen( heroTitle: 'recovering.recovery_main_header'.tr(), 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 c9bd2439..51a930a8 100644 --- a/lib/ui/pages/setup/recovering/recover_by_recovery_key.dart +++ b/lib/ui/pages/setup/recovering/recover_by_recovery_key.dart @@ -8,21 +8,21 @@ import 'package:selfprivacy/ui/components/brand_button/filled_button.dart'; import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; class RecoverByRecoveryKey extends StatelessWidget { - const RecoverByRecoveryKey({Key? key}) : super(key: key); + const RecoverByRecoveryKey({final Key? key}) : super(key: key); @override - Widget build(BuildContext context) { - var appConfig = context.watch(); + Widget build(final BuildContext context) { + ServerInstallationCubit appConfig = context.watch(); return BlocProvider( - create: (context) => RecoveryDeviceFormCubit( + create: (final context) => RecoveryDeviceFormCubit( appConfig, FieldCubitFactory(context), ServerRecoveryMethods.recoveryKey, ), child: Builder( - builder: (context) { - var formCubitState = context.watch().state; + builder: (final context) { + FormCubitState formCubitState = context.watch().state; return BrandHeroScreen( heroTitle: 'recovering.recovery_main_header'.tr(), diff --git a/lib/ui/pages/setup/recovering/recovery_confirm_backblaze.dart b/lib/ui/pages/setup/recovering/recovery_confirm_backblaze.dart index 2a9fd8a9..d14955d7 100644 --- a/lib/ui/pages/setup/recovering/recovery_confirm_backblaze.dart +++ b/lib/ui/pages/setup/recovering/recovery_confirm_backblaze.dart @@ -10,16 +10,16 @@ import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.da import 'package:selfprivacy/ui/components/brand_md/brand_md.dart'; class RecoveryConfirmBackblaze extends StatelessWidget { - const RecoveryConfirmBackblaze({Key? key}) : super(key: key); + const RecoveryConfirmBackblaze({final Key? key}) : super(key: key); @override - Widget build(BuildContext context) { - var appConfig = context.watch(); + Widget build(final BuildContext context) { + ServerInstallationCubit appConfig = context.watch(); return BlocProvider( - create: (context) => BackblazeFormCubit(appConfig), - child: Builder(builder: (context) { - var formCubitState = context.watch().state; + create: (final BuildContext context) => BackblazeFormCubit(appConfig), + child: Builder(builder: (final BuildContext context) { + FormCubitState formCubitState = context.watch().state; return BrandHeroScreen( heroTitle: 'recovering.confirm_backblaze'.tr(), @@ -55,8 +55,7 @@ class RecoveryConfirmBackblaze extends StatelessWidget { context: context, isScrollControlled: true, backgroundColor: Colors.transparent, - builder: (BuildContext context) { - return const BrandBottomSheet( + builder: (final BuildContext context) => const BrandBottomSheet( isExpended: true, child: Padding( padding: paddingH15V0, @@ -64,14 +63,13 @@ class RecoveryConfirmBackblaze extends StatelessWidget { fileName: 'how_backblaze', ), ), - ); - }, + ), ), 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 28f1a8fc..1de939b1 100644 --- a/lib/ui/pages/setup/recovering/recovery_confirm_cloudflare.dart +++ b/lib/ui/pages/setup/recovering/recovery_confirm_cloudflare.dart @@ -1,3 +1,5 @@ +// ignore_for_file: always_specify_types + import 'package:cubit_form/cubit_form.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; @@ -10,22 +12,22 @@ import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.da import 'package:selfprivacy/ui/components/brand_md/brand_md.dart'; class RecoveryConfirmCloudflare extends StatelessWidget { - const RecoveryConfirmCloudflare({Key? key}) : super(key: key); + const RecoveryConfirmCloudflare({super.key}); @override - Widget build(BuildContext context) { - var appConfig = context.watch(); + Widget build(final BuildContext context) { + final ServerInstallationCubit appConfig = context.watch(); return BlocProvider( - create: (context) => CloudFlareFormCubit(appConfig), - child: Builder(builder: (context) { - var formCubitState = context.watch().state; + create: (final BuildContext context) => CloudFlareFormCubit(appConfig), + child: Builder(builder: (final BuildContext context) { + final FormCubitState formCubitState = context.watch().state; return BrandHeroScreen( heroTitle: 'recovering.confirm_cloudflare'.tr(), heroSubtitle: 'recovering.confirm_cloudflare_description'.tr(args: [ appConfig.state.serverDomain?.domainName ?? 'your domain' - ]), + ],), hasBackButton: true, hasFlashButton: false, children: [ @@ -49,8 +51,7 @@ class RecoveryConfirmCloudflare extends StatelessWidget { context: context, isScrollControlled: true, backgroundColor: Colors.transparent, - builder: (BuildContext context) { - return const BrandBottomSheet( + builder: (final BuildContext context) => const BrandBottomSheet( isExpended: true, child: Padding( padding: paddingH15V0, @@ -58,14 +59,13 @@ class RecoveryConfirmCloudflare extends StatelessWidget { fileName: 'how_cloudflare', ), ), - ); - }, + ), ), 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 8242e521..fd7658ab 100644 --- a/lib/ui/pages/setup/recovering/recovery_confirm_server.dart +++ b/lib/ui/pages/setup/recovering/recovery_confirm_server.dart @@ -8,7 +8,7 @@ import 'package:selfprivacy/ui/components/brand_cards/brand_cards.dart'; import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; class RecoveryConfirmServer extends StatefulWidget { - const RecoveryConfirmServer({Key? key}) : super(key: key); + const RecoveryConfirmServer({super.key}); @override State createState() => _RecoveryConfirmServerState(); @@ -259,11 +259,11 @@ class _RecoveryConfirmServerState extends State { class IsValidStringDisplay extends StatelessWidget { const IsValidStringDisplay({ - Key? key, + super.key, required this.isValid, required this.textIfValid, required this.textIfInvalid, - }) : super(key: key); + }); final bool isValid; final String textIfValid; diff --git a/lib/ui/pages/setup/recovering/recovery_hentzner_connected.dart b/lib/ui/pages/setup/recovering/recovery_hentzner_connected.dart index 04093aed..6973ae2d 100644 --- a/lib/ui/pages/setup/recovering/recovery_hentzner_connected.dart +++ b/lib/ui/pages/setup/recovering/recovery_hentzner_connected.dart @@ -11,17 +11,17 @@ import 'package:selfprivacy/logic/cubit/server_installation/server_installation_ import 'package:selfprivacy/ui/components/brand_md/brand_md.dart'; class RecoveryHetznerConnected extends StatelessWidget { - const RecoveryHetznerConnected({Key? key}) : super(key: key); + const RecoveryHetznerConnected({final Key? key}) : super(key: key); @override - Widget build(BuildContext context) { - var appConfig = context.watch(); + Widget build(final BuildContext context) { + ServerInstallationCubit appConfig = context.watch(); return BlocProvider( - create: (context) => HetznerFormCubit(appConfig), + create: (final BuildContext context) => HetznerFormCubit(appConfig), child: Builder( - builder: (context) { - var formCubitState = context.watch().state; + builder: (final BuildContext context) { + FormCubitState formCubitState = context.watch().state; return BrandHeroScreen( heroTitle: 'recovering.hetzner_connected'.tr(), @@ -52,7 +52,7 @@ class RecoveryHetznerConnected extends StatelessWidget { context: context, isScrollControlled: true, backgroundColor: Colors.transparent, - builder: (BuildContext context) { + builder: (final BuildContext context) { return const BrandBottomSheet( isExpended: true, child: Padding( diff --git a/lib/utils/extensions/duration.dart b/lib/utils/extensions/duration.dart index 49fa96b8..aac00eb2 100644 --- a/lib/utils/extensions/duration.dart +++ b/lib/utils/extensions/duration.dart @@ -6,9 +6,7 @@ extension DurationFormatter on Duration { this.inHours.remainder(24), this.inMinutes.remainder(60), this.inSeconds.remainder(60) - ].map((seg) { - return seg.toString().padLeft(2, '0'); - }).join(':'); + ].map((seg) => seg.toString().padLeft(2, '0')).join(':'); } String toDayHourMinuteFormat() { @@ -17,9 +15,7 @@ extension DurationFormatter on Duration { var segments = [ this.inHours.remainder(24).abs(), this.inMinutes.remainder(60).abs(), - ].map((seg) { - return seg.toString().padLeft(2, '0'); - }); + ].map((seg) => seg.toString().padLeft(2, '0')); return '$designator${segments.first}:${segments.last}'; } @@ -33,9 +29,7 @@ extension DurationFormatter on Duration { var segments = [ this.inHours.remainder(24), this.inMinutes.remainder(60), - ].map((seg) { - return seg.toString().padLeft(2, '0'); - }); + ].map((seg) => seg.toString().padLeft(2, '0')); return '${segments.first} h ${segments.last} min'; } } diff --git a/lib/utils/extensions/elevation_extension.dart b/lib/utils/extensions/elevation_extension.dart index 78de82f1..4b1cbf3f 100644 --- a/lib/utils/extensions/elevation_extension.dart +++ b/lib/utils/extensions/elevation_extension.dart @@ -1,3 +1,5 @@ +// ignore_for_file: always_specify_types + library elevation_extension; import 'package:flutter/material.dart'; @@ -5,16 +7,15 @@ import 'package:flutter/cupertino.dart'; extension ElevationExtension on BoxDecoration { BoxDecoration copyWith({ - Color? color, - DecorationImage? image, - BoxBorder? border, - BorderRadiusGeometry? borderRadius, - List? boxShadow, - Gradient? gradient, - BlendMode? backgroundBlendMode, - BoxShape? shape, - }) { - return BoxDecoration( + final Color? color, + final DecorationImage? image, + final BoxBorder? border, + final BorderRadiusGeometry? borderRadius, + final List? boxShadow, + final Gradient? gradient, + final BlendMode? backgroundBlendMode, + final BoxShape? shape, + }) => BoxDecoration( color: color ?? this.color, image: image ?? this.image, border: border ?? this.border, @@ -26,5 +27,4 @@ extension ElevationExtension on BoxDecoration { backgroundBlendMode: backgroundBlendMode ?? this.backgroundBlendMode, shape: shape ?? this.shape, ); - } } diff --git a/lib/utils/extensions/text_extensions.dart b/lib/utils/extensions/text_extensions.dart index bf810f51..6afaee08 100644 --- a/lib/utils/extensions/text_extensions.dart +++ b/lib/utils/extensions/text_extensions.dart @@ -1,7 +1,7 @@ import 'package:flutter/cupertino.dart'; extension TextExtension on Text { - Text withColor(Color color) => Text( + Text withColor(final Color color) => Text( data!, key: key, strutStyle: strutStyle, @@ -20,20 +20,19 @@ extension TextExtension on Text { ); Text copyWith({ - Key? key, - StrutStyle? strutStyle, - TextAlign? textAlign, - TextDirection? textDirection, - Locale? locale, - bool? softWrap, - TextOverflow? overflow, - double? textScaleFactor, - int? maxLines, - String? semanticsLabel, - TextWidthBasis? textWidthBasis, - TextStyle? style, - }) { - return Text(data!, + final Key? key, + final StrutStyle? strutStyle, + final TextAlign? textAlign, + final TextDirection? textDirection, + final Locale? locale, + final bool? softWrap, + final TextOverflow? overflow, + final double? textScaleFactor, + final int? maxLines, + final String? semanticsLabel, + final TextWidthBasis? textWidthBasis, + final TextStyle? style, + }) => Text(data!, key: key ?? this.key, strutStyle: strutStyle ?? this.strutStyle, textAlign: textAlign ?? this.textAlign, @@ -45,6 +44,5 @@ extension TextExtension on Text { maxLines: maxLines ?? this.maxLines, semanticsLabel: semanticsLabel ?? this.semanticsLabel, textWidthBasis: textWidthBasis ?? this.textWidthBasis, - style: style != null ? this.style?.merge(style) ?? style : this.style); - } + style: style != null ? this.style?.merge(style) ?? style : this.style,); } diff --git a/lib/utils/password_generator.dart b/lib/utils/password_generator.dart index 35bdaecb..70fd83a9 100644 --- a/lib/utils/password_generator.dart +++ b/lib/utils/password_generator.dart @@ -1,3 +1,5 @@ +// ignore_for_file: always_specify_types + import 'dart:math'; Random _rnd = Random(); @@ -5,19 +7,19 @@ Random _rnd = Random(); typedef StringGeneratorFunction = String Function(); class StringGenerators { - static const letters = 'abcdefghijklmnopqrstuvwxyz'; - static const numbers = '1234567890'; - static const symbols = '_'; + static const String letters = 'abcdefghijklmnopqrstuvwxyz'; + static const String numbers = '1234567890'; + static const String symbols = '_'; static String getRandomString( - int length, { - hasLowercaseLetters = false, - hasUppercaseLetters = false, - hasNumbers = false, - hasSymbols = false, - isStrict = false, + final int length, { + final hasLowercaseLetters = false, + final hasUppercaseLetters = false, + final hasNumbers = false, + final hasSymbols = false, + final isStrict = false, }) { - var chars = ''; + String chars = ''; if (hasLowercaseLetters) chars += letters; if (hasUppercaseLetters) chars += letters.toUpperCase(); if (hasNumbers) chars += numbers; @@ -29,8 +31,8 @@ class StringGenerators { return genString(length, chars); } - var res = ''; - var loose = length; + String res = ''; + int loose = length; if (hasLowercaseLetters) { loose -= 1; res += genString(1, letters); @@ -49,20 +51,18 @@ class StringGenerators { } res += genString(loose, chars); - var shuffledlist = res.split('')..shuffle(); + final List shuffledlist = res.split('')..shuffle(); return shuffledlist.join(); } - static String genString(int length, String chars) { - return String.fromCharCodes( + static String genString(final int length, final String chars) => String.fromCharCodes( Iterable.generate( length, - (_) => chars.codeUnitAt( + (final _) => chars.codeUnitAt( _rnd.nextInt(chars.length), ), ), ); - } static StringGeneratorFunction userPassword = () => getRandomString( 8, diff --git a/lib/utils/route_transitions/basic.dart b/lib/utils/route_transitions/basic.dart index 2cea3eb9..5cd69cfd 100644 --- a/lib/utils/route_transitions/basic.dart +++ b/lib/utils/route_transitions/basic.dart @@ -1,9 +1,11 @@ +// ignore_for_file: always_specify_types + import 'package:flutter/material.dart'; -Route materialRoute(Widget widget) => MaterialPageRoute( - builder: (context) => widget, +Route materialRoute(final Widget widget) => MaterialPageRoute( + builder: (final BuildContext context) => widget, ); -Route noAnimationRoute(Widget widget) => PageRouteBuilder( - pageBuilder: (context, animation1, animation2) => widget, +Route noAnimationRoute(final Widget widget) => PageRouteBuilder( + pageBuilder: (final BuildContext context, final Animation animation1, final Animation animation2) => widget, ); diff --git a/lib/utils/route_transitions/slide_bottom.dart b/lib/utils/route_transitions/slide_bottom.dart index 28363e2d..374a5e90 100644 --- a/lib/utils/route_transitions/slide_bottom.dart +++ b/lib/utils/route_transitions/slide_bottom.dart @@ -1,19 +1,18 @@ import 'package:flutter/material.dart'; -Function pageBuilder = (Widget widget) => ( - BuildContext context, - Animation animation, - Animation secondaryAnimation, +Function pageBuilder = (final Widget widget) => ( + final BuildContext context, + final Animation animation, + final Animation secondaryAnimation, ) => widget; Function transitionsBuilder = ( - BuildContext context, - Animation animation, - Animation secondaryAnimation, - Widget child, -) { - return SlideTransition( + final BuildContext context, + final Animation animation, + final Animation secondaryAnimation, + final Widget child, +) => SlideTransition( position: Tween( begin: const Offset(0, 1), end: Offset.zero, @@ -31,7 +30,6 @@ Function transitionsBuilder = ( child: child, ), ); -}; class SlideBottomRoute extends PageRouteBuilder { SlideBottomRoute(this.widget) @@ -39,7 +37,7 @@ class SlideBottomRoute extends PageRouteBuilder { transitionDuration: const Duration(milliseconds: 150), pageBuilder: pageBuilder(widget), transitionsBuilder: transitionsBuilder as Widget Function( - BuildContext, Animation, Animation, Widget), + BuildContext, Animation, Animation, Widget,), ); final Widget widget; diff --git a/lib/utils/route_transitions/slide_right.dart b/lib/utils/route_transitions/slide_right.dart index 635bb021..774dcaff 100644 --- a/lib/utils/route_transitions/slide_right.dart +++ b/lib/utils/route_transitions/slide_right.dart @@ -1,19 +1,18 @@ import 'package:flutter/material.dart'; -Function pageBuilder = (Widget widget) => ( - BuildContext context, - Animation animation, - Animation secondaryAnimation, +Function pageBuilder = (final Widget widget) => ( + final BuildContext context, + final Animation animation, + final Animation secondaryAnimation, ) => widget; Function transitionsBuilder = ( - BuildContext context, - Animation animation, - Animation secondaryAnimation, - Widget child, -) { - return SlideTransition( + final BuildContext context, + final Animation animation, + final Animation secondaryAnimation, + final Widget child, +) => SlideTransition( position: Tween( begin: const Offset(-1, 0), end: Offset.zero, @@ -31,14 +30,13 @@ Function transitionsBuilder = ( child: child, ), ); -}; class SlideRightRoute extends PageRouteBuilder { SlideRightRoute(this.widget) : super( pageBuilder: pageBuilder(widget), transitionsBuilder: transitionsBuilder as Widget Function( - BuildContext, Animation, Animation, Widget), + BuildContext, Animation, Animation, Widget,), ); final Widget widget; diff --git a/lib/utils/ui_helpers.dart b/lib/utils/ui_helpers.dart index 1958c90f..c34721e1 100644 --- a/lib/utils/ui_helpers.dart +++ b/lib/utils/ui_helpers.dart @@ -3,6 +3,6 @@ import 'package:selfprivacy/logic/cubit/server_installation/server_installation_ /// it's ui helpers use only for ui components, don't use for logic components. class UiHelpers { - static String getDomainName(ServerInstallationState config) => + static String getDomainName(final ServerInstallationState config) => config.isDomainFilled ? config.serverDomain!.domainName : 'example.com'; } diff --git a/test/widget_test.dart b/test/widget_test.dart index 48cbdccf..72a8e56e 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -8,12 +8,12 @@ void main() { test('assert chart empty', () { expect(() { StringGenerators.getRandomString(8); - }, throwsAssertionError); + }, throwsAssertionError,); }); test('only lowercase string', () { - var length = 8; - var generatedString = + const int length = 8; + final String generatedString = StringGenerators.getRandomString(length, hasLowercaseLetters: true); expect(generatedString, isNot(matches(regExpNewLines))); @@ -26,9 +26,9 @@ void main() { }); test('only uppercase string', () { - var length = 8; - var generatedString = StringGenerators.getRandomString(length, - hasLowercaseLetters: false, hasUppercaseLetters: true); + const int length = 8; + final String generatedString = StringGenerators.getRandomString(length, + hasLowercaseLetters: false, hasUppercaseLetters: true,); expect(generatedString, isNot(matches(regExpNewLines))); expect(generatedString, isNot(matches(regExpWhiteSpaces))); @@ -40,11 +40,11 @@ void main() { }); test('only numbers string', () { - var length = 8; - var generatedString = StringGenerators.getRandomString(length, + const int length = 8; + final String generatedString = StringGenerators.getRandomString(length, hasLowercaseLetters: false, hasUppercaseLetters: false, - hasNumbers: true); + hasNumbers: true,); expect(generatedString, isNot(matches(regExpNewLines))); expect(generatedString, isNot(matches(regExpWhiteSpaces))); @@ -56,8 +56,8 @@ void main() { }); test('only symbols string', () { - var length = 8; - var generatedString = StringGenerators.getRandomString( + const int length = 8; + final String generatedString = StringGenerators.getRandomString( length, hasLowercaseLetters: false, hasUppercaseLetters: false, @@ -77,13 +77,13 @@ void main() { group('Strict mode', () { test('All', () { - var length = 5; - var generatedString = StringGenerators.getRandomString(length, + const int length = 5; + final String generatedString = StringGenerators.getRandomString(length, hasLowercaseLetters: true, hasUppercaseLetters: true, hasNumbers: true, hasSymbols: true, - isStrict: true); + isStrict: true,); expect(generatedString, isNot(matches(regExpNewLines))); expect(generatedString, isNot(matches(regExpWhiteSpaces))); @@ -94,13 +94,13 @@ void main() { expect(generatedString.length, equals(length)); }); test('Lowercase letters and numbers', () { - var length = 3; - var generatedString = StringGenerators.getRandomString(length, + const int length = 3; + final String generatedString = StringGenerators.getRandomString(length, hasLowercaseLetters: true, hasUppercaseLetters: false, hasNumbers: true, hasSymbols: false, - isStrict: true); + isStrict: true,); expect(generatedString, isNot(matches(regExpNewLines))); expect(generatedString, isNot(matches(regExpWhiteSpaces))); @@ -114,9 +114,9 @@ void main() { }); } -var regExpNewLines = RegExp(r'[\n\r]+'); -var regExpWhiteSpaces = RegExp(r'[\s]+'); -var regExpUppercaseLetters = RegExp(r'[A-Z]'); -var regExpLowercaseLetters = RegExp(r'[a-z]'); -var regExpNumbers = RegExp(r'[0-9]'); -var regExpSymbols = RegExp(r'(?:_|[^\w\s])+'); +RegExp regExpNewLines = RegExp(r'[\n\r]+'); +RegExp regExpWhiteSpaces = RegExp(r'[\s]+'); +RegExp regExpUppercaseLetters = RegExp(r'[A-Z]'); +RegExp regExpLowercaseLetters = RegExp(r'[a-z]'); +RegExp regExpNumbers = RegExp(r'[0-9]'); +RegExp regExpSymbols = RegExp(r'(?:_|[^\w\s])+'); From 2ac8e4366b93e2c887f4c8d63971185d6d628af1 Mon Sep 17 00:00:00 2001 From: NaiJi Date: Mon, 6 Jun 2022 01:40:34 +0300 Subject: [PATCH 43/52] Linting! Co-authored-by: Inex Code --- analysis_options.yaml | 1 - lib/config/bloc_config.dart | 50 +- lib/config/bloc_observer.dart | 8 +- lib/config/brand_colors.dart | 2 - lib/config/localization.dart | 16 +- lib/logic/api_maps/api_map.dart | 32 +- lib/logic/api_maps/backblaze.dart | 26 +- lib/logic/api_maps/cloudflare.dart | 49 +- lib/logic/api_maps/hetzner.dart | 21 +- lib/logic/api_maps/server.dart | 166 ++-- .../authentication_dependend_cubit.dart | 2 - .../app_settings/app_settings_cubit.dart | 12 +- .../app_settings/app_settings_state.dart | 7 +- lib/logic/cubit/backups/backups_cubit.dart | 122 +-- lib/logic/cubit/backups/backups_state.dart | 2 - lib/logic/cubit/devices/devices_cubit.dart | 12 +- lib/logic/cubit/devices/devices_state.dart | 25 +- .../cubit/dns_records/dns_records_cubit.dart | 70 +- .../cubit/dns_records/dns_records_state.dart | 28 +- .../forms/factories/field_cubit_factory.dart | 30 +- .../initializing/backblaze_form_cubit.dart | 2 - .../initializing/cloudflare_form_cubit.dart | 6 +- .../initializing/hetzner_form_cubit.dart | 6 +- .../initializing/root_user_form_cubit.dart | 6 +- .../recovery_device_form_cubit.dart | 9 +- .../recovery_domain_form_cubit.dart | 13 +- .../cubit/forms/user/ssh_form_cubit.dart | 21 +- .../cubit/forms/user/user_form_cubit.dart | 5 +- .../cubit/forms/validations/validations.dart | 17 +- .../hetzner_metrics_repository.dart | 10 +- .../hetzner_metrics_state.dart | 2 - lib/logic/cubit/jobs/jobs_cubit.dart | 14 +- lib/logic/cubit/jobs/jobs_state.dart | 5 +- .../cubit/providers/providers_state.dart | 17 +- .../recovery_key/recovery_key_state.dart | 17 +- .../server_detailed_info_state.dart | 3 - .../server_installation_repository.dart | 112 ++- .../server_installation_state.dart | 28 +- lib/logic/cubit/services/services_state.dart | 2 - lib/logic/cubit/users/users_cubit.dart | 235 ++++-- lib/logic/cubit/users/users_state.dart | 20 +- lib/logic/get_it/api_config.dart | 2 - lib/logic/get_it/console.dart | 2 - lib/logic/models/hive/user.dart | 5 +- lib/logic/models/hive/user.g.dart | 2 - lib/logic/models/job.dart | 2 - .../models/json/auto_upgrade_settings.dart | 2 - .../models/json/hetzner_server_info.dart | 10 +- .../models/json/recovery_token_status.dart | 3 - .../models/json/server_configurations.dart | 3 - lib/logic/models/message.dart | 8 +- lib/logic/models/provider.dart | 2 - lib/logic/models/server_basic_info.dart | 4 - lib/logic/models/timezone_settings.dart | 7 +- lib/main.dart | 31 +- lib/theming/factory/app_theme_factory.dart | 22 +- .../action_button/action_button.dart | 2 +- .../brand_bottom_sheet.dart | 2 - .../components/brand_button/brand_button.dart | 51 +- .../components/brand_cards/brand_cards.dart | 11 +- .../brand_divider/brand_divider.dart | 10 +- .../components/brand_header/brand_header.dart | 40 +- .../brand_hero_screen/brand_hero_screen.dart | 78 +- .../components/brand_loader/brand_loader.dart | 20 +- .../components/brand_radio/brand_radio.dart | 46 +- .../brand_radio_tile/brand_radio_tile.dart | 32 +- .../brand_span_button/brand_span_button.dart | 16 +- .../components/brand_switch/brand_switch.dart | 10 +- .../brand_tab_bar/brand_tab_bar.dart | 39 +- lib/ui/components/brand_text/brand_text.dart | 36 +- .../components/brand_timer/brand_timer.dart | 17 +- .../icon_status_mask/icon_status_mask.dart | 2 +- .../components/jobs_content/jobs_content.dart | 191 ++--- .../not_ready_card/not_ready_card.dart | 70 +- lib/ui/components/one_page/one_page.dart | 55 +- .../components/pre_styled_buttons/close.dart | 20 +- .../components/pre_styled_buttons/flash.dart | 57 +- .../pre_styled_buttons/flash_fab.dart | 32 +- .../pre_styled_buttons.dart | 2 +- .../components/progress_bar/progress_bar.dart | 46 +- .../components/switch_block/switch_bloc.dart | 39 +- .../pages/backup_details/backup_details.dart | 83 +- lib/ui/pages/devices/devices.dart | 102 +-- lib/ui/pages/devices/new_device.dart | 121 +-- lib/ui/pages/dns_details/dns_details.dart | 10 +- lib/ui/pages/more/about/about.dart | 24 +- .../pages/more/app_settings/app_setting.dart | 151 ++-- lib/ui/pages/more/console/console.dart | 129 +-- lib/ui/pages/more/info/info.dart | 38 +- lib/ui/pages/more/more.dart | 14 +- lib/ui/pages/onboarding/onboarding.dart | 176 ++-- lib/ui/pages/providers/providers.dart | 26 +- lib/ui/pages/recovery_key/recovery_key.dart | 104 +-- .../recovery_key/recovery_key_receiving.dart | 72 +- lib/ui/pages/root_route.dart | 3 +- lib/ui/pages/server_details/chart.dart | 42 +- lib/ui/pages/server_details/cpu_chart.dart | 135 ++-- lib/ui/pages/server_details/header.dart | 87 +- .../pages/server_details/network_charts.dart | 179 ++--- .../server_details/server_details_screen.dart | 25 +- .../pages/server_details/server_settings.dart | 76 +- lib/ui/pages/server_details/text_details.dart | 62 +- .../server_details/time_zone/time_zone.dart | 135 ++-- lib/ui/pages/services/services.dart | 370 ++++----- lib/ui/pages/setup/initializing.dart | 757 +++++++++--------- .../recovering/recover_by_new_device_key.dart | 53 +- .../recovering/recover_by_old_token.dart | 71 +- .../recovering/recover_by_recovery_key.dart | 8 +- .../recovery_confirm_backblaze.dart | 88 +- .../recovery_confirm_cloudflare.dart | 81 +- .../recovering/recovery_confirm_server.dart | 468 +++++------ .../recovery_hentzner_connected.dart | 33 +- .../recovering/recovery_method_select.dart | 190 ++--- .../setup/recovering/recovery_routing.dart | 30 +- lib/ui/pages/ssh_keys/new_ssh_key.dart | 126 +-- lib/ui/pages/ssh_keys/ssh_keys.dart | 215 ++--- lib/ui/pages/users/add_user_fab.dart | 35 +- lib/ui/pages/users/empty.dart | 48 +- lib/ui/pages/users/new_user.dart | 152 ++-- lib/ui/pages/users/user.dart | 80 +- lib/ui/pages/users/user_details.dart | 140 ++-- lib/ui/pages/users/users.dart | 61 +- lib/utils/color_utils.dart | 11 +- lib/utils/extensions/duration.dart | 29 +- lib/utils/extensions/elevation_extension.dart | 30 +- lib/utils/password_generator.dart | 37 +- lib/utils/route_transitions/basic.dart | 9 +- pubspec.lock | 16 +- 128 files changed, 3773 insertions(+), 3419 deletions(-) diff --git a/analysis_options.yaml b/analysis_options.yaml index 4aab61a8..3ab00ee3 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -36,7 +36,6 @@ linter: always_declare_return_types: true always_put_required_named_parameters_first: true always_put_control_body_on_new_line: true - always_specify_types: true avoid_escaping_inner_quotes: true avoid_setters_without_getters: true eol_at_end_of_file: true diff --git a/lib/config/bloc_config.dart b/lib/config/bloc_config.dart index f800a4e2..3946d3b9 100644 --- a/lib/config/bloc_config.dart +++ b/lib/config/bloc_config.dart @@ -1,5 +1,3 @@ -// ignore_for_file: always_specify_types - import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:selfprivacy/logic/cubit/devices/devices_cubit.dart'; @@ -20,14 +18,14 @@ class BlocAndProviderConfig extends StatelessWidget { @override Widget build(final BuildContext context) { - const bool isDark = false; - final ServerInstallationCubit serverInstallationCubit = ServerInstallationCubit()..load(); - final UsersCubit usersCubit = UsersCubit(serverInstallationCubit); - final ServicesCubit servicesCubit = ServicesCubit(serverInstallationCubit); - final BackupsCubit backupsCubit = BackupsCubit(serverInstallationCubit); - final DnsRecordsCubit dnsRecordsCubit = DnsRecordsCubit(serverInstallationCubit); - final RecoveryKeyCubit recoveryKeyCubit = RecoveryKeyCubit(serverInstallationCubit); - final ApiDevicesCubit apiDevicesCubit = ApiDevicesCubit(serverInstallationCubit); + const isDark = false; + final serverInstallationCubit = ServerInstallationCubit()..load(); + final usersCubit = UsersCubit(serverInstallationCubit); + final servicesCubit = ServicesCubit(serverInstallationCubit); + final backupsCubit = BackupsCubit(serverInstallationCubit); + final dnsRecordsCubit = DnsRecordsCubit(serverInstallationCubit); + final recoveryKeyCubit = RecoveryKeyCubit(serverInstallationCubit); + final apiDevicesCubit = ApiDevicesCubit(serverInstallationCubit); return MultiProvider( providers: [ BlocProvider( @@ -36,14 +34,32 @@ class BlocAndProviderConfig extends StatelessWidget { isOnboardingShowing: true, )..load(), ), - BlocProvider(create: (final _) => serverInstallationCubit, lazy: false), + BlocProvider( + create: (final _) => serverInstallationCubit, + lazy: false, + ), BlocProvider(create: (final _) => ProvidersCubit()), - BlocProvider(create: (final _) => usersCubit..load(), lazy: false), - BlocProvider(create: (final _) => servicesCubit..load(), lazy: false), - BlocProvider(create: (final _) => backupsCubit..load(), lazy: false), - BlocProvider(create: (final _) => dnsRecordsCubit..load()), - BlocProvider(create: (final _) => recoveryKeyCubit..load()), - BlocProvider(create: (final _) => apiDevicesCubit..load()), + BlocProvider( + create: (final _) => usersCubit..load(), + lazy: false, + ), + BlocProvider( + create: (final _) => servicesCubit..load(), + lazy: false, + ), + BlocProvider( + create: (final _) => backupsCubit..load(), + lazy: false, + ), + BlocProvider( + create: (final _) => dnsRecordsCubit..load(), + ), + BlocProvider( + create: (final _) => recoveryKeyCubit..load(), + ), + BlocProvider( + create: (final _) => apiDevicesCubit..load(), + ), BlocProvider( create: (final _) => JobsCubit(usersCubit: usersCubit, servicesCubit: servicesCubit), diff --git a/lib/config/bloc_observer.dart b/lib/config/bloc_observer.dart index 56fa5a32..e68923c9 100644 --- a/lib/config/bloc_observer.dart +++ b/lib/config/bloc_observer.dart @@ -1,5 +1,3 @@ -// ignore_for_file: always_specify_types - import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:selfprivacy/ui/components/error/error.dart'; @@ -11,7 +9,11 @@ class SimpleBlocObserver extends BlocObserver { SimpleBlocObserver(); @override - void onError(final BlocBase bloc, final Object error, final StackTrace stackTrace) { + void onError( + final BlocBase bloc, + final Object error, + final StackTrace stackTrace, + ) { final NavigatorState navigator = getIt.get().navigator!; navigator.push( diff --git a/lib/config/brand_colors.dart b/lib/config/brand_colors.dart index f6866c2d..15d1433a 100644 --- a/lib/config/brand_colors.dart +++ b/lib/config/brand_colors.dart @@ -1,5 +1,3 @@ -// ignore_for_file: always_specify_types - import 'package:flutter/material.dart'; class BrandColors { diff --git a/lib/config/localization.dart b/lib/config/localization.dart index e3f6d8d2..b8356950 100644 --- a/lib/config/localization.dart +++ b/lib/config/localization.dart @@ -1,5 +1,3 @@ -// ignore_for_file: always_specify_types - import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; @@ -12,11 +10,11 @@ class Localization extends StatelessWidget { final Widget? child; @override Widget build(final BuildContext context) => EasyLocalization( - supportedLocales: const [Locale('ru'), Locale('en')], - path: 'assets/translations', - fallbackLocale: const Locale('en'), - saveLocale: false, - useOnlyLangCode: true, - child: child!, - ); + supportedLocales: const [Locale('ru'), Locale('en')], + path: 'assets/translations', + fallbackLocale: const Locale('en'), + saveLocale: false, + useOnlyLangCode: true, + child: child!, + ); } diff --git a/lib/logic/api_maps/api_map.dart b/lib/logic/api_maps/api_map.dart index ce1a54b2..007bfd98 100644 --- a/lib/logic/api_maps/api_map.dart +++ b/lib/logic/api_maps/api_map.dart @@ -1,5 +1,3 @@ -// ignore_for_file: always_specify_types - import 'dart:async'; import 'dart:developer'; import 'dart:io'; @@ -20,19 +18,24 @@ abstract class ApiMap { (dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = (final HttpClient client) { client.badCertificateCallback = - (final X509Certificate cert, final String host, final int port) => true; + (final X509Certificate cert, final String host, final int port) => + true; return client; }; - dio.interceptors.add(InterceptorsWrapper(onError: (final DioError e, final ErrorInterceptorHandler handler) { - print(e.requestOptions.path); - print(e.requestOptions.data); + dio.interceptors.add( + InterceptorsWrapper( + onError: (final DioError e, final ErrorInterceptorHandler handler) { + print(e.requestOptions.path); + print(e.requestOptions.data); - print(e.message); - print(e.response); + print(e.message); + print(e.response); - return handler.next(e); - },),); + return handler.next(e); + }, + ), + ); return dio; } @@ -56,7 +59,7 @@ class ConsoleInterceptor extends InterceptorsWrapper { } @override - Future onRequest( + Future onRequest( final RequestOptions options, final RequestInterceptorHandler handler, ) async { @@ -70,7 +73,7 @@ class ConsoleInterceptor extends InterceptorsWrapper { } @override - Future onResponse( + Future onResponse( final Response response, final ResponseInterceptorHandler handler, ) async { @@ -87,7 +90,10 @@ class ConsoleInterceptor extends InterceptorsWrapper { } @override - Future onError(final DioError err, final ErrorInterceptorHandler handler) async { + Future onError( + final DioError err, + final ErrorInterceptorHandler handler, + ) async { final Response? response = err.response; log(err.toString()); addMessage( diff --git a/lib/logic/api_maps/backblaze.dart b/lib/logic/api_maps/backblaze.dart index 0957df2d..f6626a26 100644 --- a/lib/logic/api_maps/backblaze.dart +++ b/lib/logic/api_maps/backblaze.dart @@ -1,5 +1,3 @@ -// ignore_for_file: always_specify_types - import 'dart:io'; import 'package:dio/dio.dart'; @@ -15,8 +13,10 @@ class BackblazeApiAuth { } class BackblazeApplicationKey { - BackblazeApplicationKey( - {required this.applicationKeyId, required this.applicationKey,}); + BackblazeApplicationKey({ + required this.applicationKeyId, + required this.applicationKey, + }); final String applicationKeyId; final String applicationKey; @@ -29,7 +29,8 @@ class BackblazeApi extends ApiMap { BaseOptions get options { final BaseOptions options = BaseOptions(baseUrl: rootAddress); if (isWithToken) { - final BackblazeCredential? backblazeCredential = getIt().backblazeCredential; + final BackblazeCredential? backblazeCredential = + getIt().backblazeCredential; final String token = backblazeCredential!.applicationKey; options.headers = {'Authorization': 'Basic $token'}; } @@ -48,12 +49,15 @@ class BackblazeApi extends ApiMap { Future getAuthorizationToken() async { final Dio client = await getClient(); - final BackblazeCredential? backblazeCredential = getIt().backblazeCredential; + final BackblazeCredential? backblazeCredential = + getIt().backblazeCredential; if (backblazeCredential == null) { throw Exception('Backblaze credential is null'); } final String encodedApiKey = encodedBackblazeKey( - backblazeCredential.keyId, backblazeCredential.applicationKey,); + backblazeCredential.keyId, + backblazeCredential.applicationKey, + ); final Response response = await client.get( 'b2_authorize_account', options: Options(headers: {'Authorization': 'Basic $encodedApiKey'}), @@ -89,7 +93,8 @@ class BackblazeApi extends ApiMap { // Create bucket Future createBucket(final String bucketName) async { final BackblazeApiAuth auth = await getAuthorizationToken(); - final BackblazeCredential? backblazeCredential = getIt().backblazeCredential; + final BackblazeCredential? backblazeCredential = + getIt().backblazeCredential; final Dio client = await getClient(); client.options.baseUrl = auth.apiUrl; final Response response = await client.post( @@ -138,8 +143,9 @@ class BackblazeApi extends ApiMap { close(client); if (response.statusCode == HttpStatus.ok) { return BackblazeApplicationKey( - applicationKeyId: response.data['applicationKeyId'], - applicationKey: response.data['applicationKey'],); + applicationKeyId: response.data['applicationKeyId'], + applicationKey: response.data['applicationKey'], + ); } else { throw Exception('code: ${response.statusCode}'); } diff --git a/lib/logic/api_maps/cloudflare.dart b/lib/logic/api_maps/cloudflare.dart index 29fb2c46..9141d5fe 100644 --- a/lib/logic/api_maps/cloudflare.dart +++ b/lib/logic/api_maps/cloudflare.dart @@ -1,5 +1,3 @@ -// ignore_for_file: always_specify_types - import 'dart:io'; import 'package:dio/dio.dart'; @@ -14,7 +12,6 @@ class DomainNotFoundException implements Exception { } class CloudflareApi extends ApiMap { - CloudflareApi({ this.hasLogger = false, this.isWithToken = true, @@ -50,11 +47,14 @@ class CloudflareApi extends ApiMap { String rootAddress = 'https://api.cloudflare.com/client/v4'; Future isValid(final String token) async { - validateStatus = (final status) => status == HttpStatus.ok || status == HttpStatus.unauthorized; + validateStatus = (final status) => + status == HttpStatus.ok || status == HttpStatus.unauthorized; final Dio client = await getClient(); - final Response response = await client.get('/user/tokens/verify', - options: Options(headers: {'Authorization': 'Bearer $token'}),); + final Response response = await client.get( + '/user/tokens/verify', + options: Options(headers: {'Authorization': 'Bearer $token'}), + ); close(client); @@ -68,7 +68,8 @@ class CloudflareApi extends ApiMap { } Future getZoneId(final String domain) async { - validateStatus = (final status) => status == HttpStatus.ok || status == HttpStatus.forbidden; + validateStatus = (final status) => + status == HttpStatus.ok || status == HttpStatus.forbidden; final Dio client = await getClient(); final Response response = await client.get( '/zones', @@ -127,13 +128,15 @@ class CloudflareApi extends ApiMap { for (final record in records) { if (record['zone_name'] == domainName) { - allRecords.add(DnsRecord( - name: record['name'], - type: record['type'], - content: record['content'], - ttl: record['ttl'], - proxied: record['proxied'], - ),); + allRecords.add( + DnsRecord( + name: record['name'], + type: record['type'], + content: record['content'], + ttl: record['ttl'], + proxied: record['proxied'], + ), + ); } } @@ -169,16 +172,22 @@ class CloudflareApi extends ApiMap { } } - List projectDnsRecords(final String? domainName, final String? ip4) { - final DnsRecord domainA = DnsRecord(type: 'A', name: domainName, content: ip4); + List projectDnsRecords( + final String? domainName, + final String? ip4, + ) { + final DnsRecord domainA = + DnsRecord(type: 'A', name: domainName, content: ip4); final DnsRecord mx = DnsRecord(type: 'MX', name: '@', content: domainName); final DnsRecord apiA = DnsRecord(type: 'A', name: 'api', content: ip4); final DnsRecord cloudA = DnsRecord(type: 'A', name: 'cloud', content: ip4); final DnsRecord gitA = DnsRecord(type: 'A', name: 'git', content: ip4); final DnsRecord meetA = DnsRecord(type: 'A', name: 'meet', content: ip4); - final DnsRecord passwordA = DnsRecord(type: 'A', name: 'password', content: ip4); - final DnsRecord socialA = DnsRecord(type: 'A', name: 'social', content: ip4); + final DnsRecord passwordA = + DnsRecord(type: 'A', name: 'password', content: ip4); + final DnsRecord socialA = + DnsRecord(type: 'A', name: 'social', content: ip4); final DnsRecord vpn = DnsRecord(type: 'A', name: 'vpn', content: ip4); final DnsRecord txt1 = DnsRecord( @@ -211,7 +220,9 @@ class CloudflareApi extends ApiMap { } Future setDkim( - final String dkimRecordString, final ServerDomain cloudFlareDomain,) async { + final String dkimRecordString, + final ServerDomain cloudFlareDomain, + ) async { final String domainZoneId = cloudFlareDomain.zoneId; final String url = '$rootAddress/zones/$domainZoneId/dns_records'; diff --git a/lib/logic/api_maps/hetzner.dart b/lib/logic/api_maps/hetzner.dart index 3901cb40..54d60c21 100644 --- a/lib/logic/api_maps/hetzner.dart +++ b/lib/logic/api_maps/hetzner.dart @@ -1,5 +1,3 @@ -// ignore_for_file: always_specify_types - import 'dart:convert'; import 'dart:io'; @@ -12,7 +10,6 @@ import 'package:selfprivacy/logic/models/hive/user.dart'; import 'package:selfprivacy/utils/password_generator.dart'; class HetznerApi extends ApiMap { - HetznerApi({this.hasLogger = false, this.isWithToken = true}); @override bool hasLogger; @@ -39,7 +36,8 @@ class HetznerApi extends ApiMap { String rootAddress = 'https://api.hetzner.cloud/v1'; Future isValid(final String token) async { - validateStatus = (final int? status) => status == HttpStatus.ok || status == HttpStatus.unauthorized; + validateStatus = (final int? status) => + status == HttpStatus.ok || status == HttpStatus.unauthorized; final Dio client = await getClient(); final Response response = await client.get( '/servers', @@ -201,8 +199,12 @@ class HetznerApi extends ApiMap { } Future> getMetrics( - final DateTime start, final DateTime end, final String type,) async { - final ServerHostingDetails? hetznerServer = getIt().serverDetails; + final DateTime start, + final DateTime end, + final String type, + ) async { + final ServerHostingDetails? hetznerServer = + getIt().serverDetails; final Dio client = await getClient(); final Map queryParameters = { @@ -219,7 +221,8 @@ class HetznerApi extends ApiMap { } Future getInfo() async { - final ServerHostingDetails? hetznerServer = getIt().serverDetails; + final ServerHostingDetails? hetznerServer = + getIt().serverDetails; final Dio client = await getClient(); final Response response = await client.get('/servers/${hetznerServer!.id}'); close(client); @@ -233,6 +236,7 @@ class HetznerApi extends ApiMap { close(client); return (response.data!['servers'] as List) + // ignore: unnecessary_lambdas .map((final e) => HetznerServerInfo.fromJson(e)) .toList(); } @@ -241,7 +245,8 @@ class HetznerApi extends ApiMap { required final String ip4, required final String domainName, }) async { - final ServerHostingDetails? hetznerServer = getIt().serverDetails; + final ServerHostingDetails? hetznerServer = + getIt().serverDetails; final Dio client = await getClient(); await client.post( '/servers/${hetznerServer!.id}/actions/change_dns_ptr', diff --git a/lib/logic/api_maps/server.dart b/lib/logic/api_maps/server.dart index 522ee528..bdd544f7 100644 --- a/lib/logic/api_maps/server.dart +++ b/lib/logic/api_maps/server.dart @@ -1,5 +1,3 @@ -// ignore_for_file: always_specify_types - import 'dart:async'; import 'dart:convert'; import 'dart:io'; @@ -20,7 +18,6 @@ import 'package:selfprivacy/logic/models/timezone_settings.dart'; import 'package:selfprivacy/logic/api_maps/api_map.dart'; class ApiResponse { - ApiResponse({ required this.statusCode, required this.data, @@ -34,12 +31,12 @@ class ApiResponse { } class ServerApi extends ApiMap { - - ServerApi( - {this.hasLogger = false, - this.isWithToken = true, - this.overrideDomain, - this.customToken,}); + ServerApi({ + this.hasLogger = false, + this.isWithToken = true, + this.overrideDomain, + this.customToken, + }); @override bool hasLogger; @override @@ -52,13 +49,17 @@ class ServerApi extends ApiMap { BaseOptions options = BaseOptions(); if (isWithToken) { - final ServerDomain? cloudFlareDomain = getIt().serverDomain; + final ServerDomain? cloudFlareDomain = + getIt().serverDomain; final String domainName = cloudFlareDomain!.domainName; final String? apiToken = getIt().serverDetails?.apiToken; - options = BaseOptions(baseUrl: 'https://api.$domainName', headers: { - 'Authorization': 'Bearer $apiToken', - },); + options = BaseOptions( + baseUrl: 'https://api.$domainName', + headers: { + 'Authorization': 'Bearer $apiToken', + }, + ); } if (overrideDomain != null) { @@ -157,14 +158,18 @@ class ServerApi extends ApiMap { ); } - Future>> getUsersList({final withMainUser = false}) async { + Future>> getUsersList({ + final withMainUser = false, + }) async { final List res = []; Response response; final Dio client = await getClient(); try { - response = await client.get('/users', - queryParameters: withMainUser ? {'withMainUser': 'true'} : null,); + response = await client.get( + '/users', + queryParameters: withMainUser ? {'withMainUser': 'true'} : null, + ); for (final user in response.data) { res.add(user.toString()); } @@ -194,7 +199,10 @@ class ServerApi extends ApiMap { ); } - Future> addUserSshKey(final User user, final String sshKey) async { + Future> addUserSshKey( + final User user, + final String sshKey, + ) async { late Response response; final Dio client = await getClient(); @@ -259,7 +267,9 @@ class ServerApi extends ApiMap { final Dio client = await getClient(); try { response = await client.get('/services/ssh/keys/${user.login}'); - res = (response.data as List).map((final e) => e as String).toList(); + res = (response.data as List) + .map((final e) => e as String) + .toList(); } on DioError catch (e) { print(e.message); return ApiResponse>( @@ -290,7 +300,10 @@ class ServerApi extends ApiMap { ); } - Future> deleteUserSshKey(final User user, final String sshKey) async { + Future> deleteUserSshKey( + final User user, + final String sshKey, + ) async { Response response; final Dio client = await getClient(); @@ -360,7 +373,10 @@ class ServerApi extends ApiMap { return res; } - Future switchService(final ServiceTypes type, final bool needToTurnOn) async { + Future switchService( + final ServiceTypes type, + final bool needToTurnOn, + ) async { final Dio client = await getClient(); try { client.post( @@ -431,7 +447,7 @@ class ServerApi extends ApiMap { final Dio client = await getClient(); try { response = await client.get('/services/restic/backup/list'); - backups = response.data.map((final e) => Backup.fromJson(e)).toList(); + backups = response.data.map(Backup.fromJson).toList(); } on DioError catch (e) { print(e.message); } catch (e) { @@ -562,7 +578,9 @@ class ServerApi extends ApiMap { return settings; } - Future updateAutoUpgradeSettings(final AutoUpgradeSettings settings) async { + Future updateAutoUpgradeSettings( + final AutoUpgradeSettings settings, + ) async { final Dio client = await getClient(); try { await client.put( @@ -579,7 +597,8 @@ class ServerApi extends ApiMap { Future getServerTimezone() async { // I am not sure how to initialize TimeZoneSettings with default value... final Dio client = await getClient(); - final Response response = await client.get('/system/configuration/timezone'); + final Response response = + await client.get('/system/configuration/timezone'); close(client); return TimeZoneSettings.fromString(response.data); @@ -642,9 +661,10 @@ class ServerApi extends ApiMap { } on DioError catch (e) { print(e.message); return ApiResponse( - errorMessage: e.message, - statusCode: e.response?.statusCode ?? HttpStatus.internalServerError, - data: const RecoveryKeyStatus(exists: false, valid: false),); + errorMessage: e.message, + statusCode: e.response?.statusCode ?? HttpStatus.internalServerError, + data: const RecoveryKeyStatus(exists: false, valid: false), + ); } finally { close(client); } @@ -652,10 +672,11 @@ class ServerApi extends ApiMap { final int code = response.statusCode ?? HttpStatus.internalServerError; return ApiResponse( - statusCode: code, - data: response.data != null - ? RecoveryKeyStatus.fromJson(response.data) - : null,); + statusCode: code, + data: response.data != null + ? RecoveryKeyStatus.fromJson(response.data) + : null, + ); } Future> generateRecoveryToken( @@ -681,9 +702,10 @@ class ServerApi extends ApiMap { } on DioError catch (e) { print(e.message); return ApiResponse( - errorMessage: e.message, - statusCode: e.response?.statusCode ?? HttpStatus.internalServerError, - data: '',); + errorMessage: e.message, + statusCode: e.response?.statusCode ?? HttpStatus.internalServerError, + data: '', + ); } finally { close(client); } @@ -691,8 +713,9 @@ class ServerApi extends ApiMap { final int code = response.statusCode ?? HttpStatus.internalServerError; return ApiResponse( - statusCode: code, - data: response.data != null ? response.data['token'] : '',); + statusCode: code, + data: response.data != null ? response.data['token'] : '', + ); } Future> useRecoveryToken(final DeviceToken token) async { @@ -710,9 +733,10 @@ class ServerApi extends ApiMap { } on DioError catch (e) { print(e.message); return ApiResponse( - errorMessage: e.message, - statusCode: e.response?.statusCode ?? HttpStatus.internalServerError, - data: '',); + errorMessage: e.message, + statusCode: e.response?.statusCode ?? HttpStatus.internalServerError, + data: '', + ); } finally { client.close(); } @@ -720,8 +744,9 @@ class ServerApi extends ApiMap { final int code = response.statusCode ?? HttpStatus.internalServerError; return ApiResponse( - statusCode: code, - data: response.data != null ? response.data['token'] : '',); + statusCode: code, + data: response.data != null ? response.data['token'] : '', + ); } Future> authorizeDevice(final DeviceToken token) async { @@ -739,9 +764,10 @@ class ServerApi extends ApiMap { } on DioError catch (e) { print(e.message); return ApiResponse( - errorMessage: e.message, - statusCode: e.response?.statusCode ?? HttpStatus.internalServerError, - data: '',); + errorMessage: e.message, + statusCode: e.response?.statusCode ?? HttpStatus.internalServerError, + data: '', + ); } finally { client.close(); } @@ -760,9 +786,10 @@ class ServerApi extends ApiMap { } on DioError catch (e) { print(e.message); return ApiResponse( - errorMessage: e.message, - statusCode: e.response?.statusCode ?? HttpStatus.internalServerError, - data: '',); + errorMessage: e.message, + statusCode: e.response?.statusCode ?? HttpStatus.internalServerError, + data: '', + ); } finally { client.close(); } @@ -770,8 +797,9 @@ class ServerApi extends ApiMap { final int code = response.statusCode ?? HttpStatus.internalServerError; return ApiResponse( - statusCode: code, - data: response.data != null ? response.data['token'] : '',); + statusCode: code, + data: response.data != null ? response.data['token'] : '', + ); } Future> deleteDeviceToken() async { @@ -783,9 +811,10 @@ class ServerApi extends ApiMap { } on DioError catch (e) { print(e.message); return ApiResponse( - errorMessage: e.message, - statusCode: e.response?.statusCode ?? HttpStatus.internalServerError, - data: '',); + errorMessage: e.message, + statusCode: e.response?.statusCode ?? HttpStatus.internalServerError, + data: '', + ); } finally { client.close(); } @@ -804,9 +833,10 @@ class ServerApi extends ApiMap { } on DioError catch (e) { print(e.message); return ApiResponse( - errorMessage: e.message, - statusCode: e.response?.statusCode ?? HttpStatus.internalServerError, - data: [],); + errorMessage: e.message, + statusCode: e.response?.statusCode ?? HttpStatus.internalServerError, + data: [], + ); } finally { client.close(); } @@ -814,10 +844,11 @@ class ServerApi extends ApiMap { final int code = response.statusCode ?? HttpStatus.internalServerError; return ApiResponse( - statusCode: code, - data: (response.data != null) - ? response.data.map((final e) => ApiToken.fromJson(e)).toList() - : [],); + statusCode: code, + data: (response.data != null) + ? response.data.map(ApiToken.fromJson).toList() + : [], + ); } Future> refreshCurrentApiToken() async { @@ -829,9 +860,10 @@ class ServerApi extends ApiMap { } on DioError catch (e) { print(e.message); return ApiResponse( - errorMessage: e.message, - statusCode: e.response?.statusCode ?? HttpStatus.internalServerError, - data: '',); + errorMessage: e.message, + statusCode: e.response?.statusCode ?? HttpStatus.internalServerError, + data: '', + ); } finally { client.close(); } @@ -839,8 +871,9 @@ class ServerApi extends ApiMap { final int code = response.statusCode ?? HttpStatus.internalServerError; return ApiResponse( - statusCode: code, - data: response.data != null ? response.data['token'] : '',); + statusCode: code, + data: response.data != null ? response.data['token'] : '', + ); } Future> deleteApiToken(final String device) async { @@ -856,9 +889,10 @@ class ServerApi extends ApiMap { } on DioError catch (e) { print(e.message); return ApiResponse( - errorMessage: e.message, - statusCode: e.response?.statusCode ?? HttpStatus.internalServerError, - data: null,); + errorMessage: e.message, + statusCode: e.response?.statusCode ?? HttpStatus.internalServerError, + data: null, + ); } finally { client.close(); } diff --git a/lib/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart b/lib/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart index 68705b5b..096a4d4f 100644 --- a/lib/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart +++ b/lib/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart @@ -1,5 +1,3 @@ -// ignore_for_file: always_specify_types - import 'dart:async'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:equatable/equatable.dart'; diff --git a/lib/logic/cubit/app_settings/app_settings_cubit.dart b/lib/logic/cubit/app_settings/app_settings_cubit.dart index 7c2d55e3..06b46730 100644 --- a/lib/logic/cubit/app_settings/app_settings_cubit.dart +++ b/lib/logic/cubit/app_settings/app_settings_cubit.dart @@ -1,5 +1,3 @@ -// ignore_for_file: always_specify_types - import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:hive/hive.dart'; @@ -25,10 +23,12 @@ class AppSettingsCubit extends Cubit { void load() { final bool? isDarkModeOn = box.get(BNames.isDarkModeOn); final bool? isOnboardingShowing = box.get(BNames.isOnboardingShowing); - emit(state.copyWith( - isDarkModeOn: isDarkModeOn, - isOnboardingShowing: isOnboardingShowing, - ),); + emit( + state.copyWith( + isDarkModeOn: isDarkModeOn, + isOnboardingShowing: isOnboardingShowing, + ), + ); } void updateDarkMode({required final bool isDarkModeOn}) { diff --git a/lib/logic/cubit/app_settings/app_settings_state.dart b/lib/logic/cubit/app_settings/app_settings_state.dart index 6000fc55..92da9667 100644 --- a/lib/logic/cubit/app_settings/app_settings_state.dart +++ b/lib/logic/cubit/app_settings/app_settings_state.dart @@ -1,5 +1,3 @@ -// ignore_for_file: always_specify_types - part of 'app_settings_cubit.dart'; class AppSettingsState extends Equatable { @@ -11,7 +9,10 @@ class AppSettingsState extends Equatable { final bool isDarkModeOn; final bool isOnboardingShowing; - AppSettingsState copyWith({final isDarkModeOn, final isOnboardingShowing}) => + AppSettingsState copyWith({ + final bool? isDarkModeOn, + final bool? isOnboardingShowing, + }) => AppSettingsState( isDarkModeOn: isDarkModeOn ?? this.isDarkModeOn, isOnboardingShowing: isOnboardingShowing ?? this.isOnboardingShowing, diff --git a/lib/logic/cubit/backups/backups_cubit.dart b/lib/logic/cubit/backups/backups_cubit.dart index e77156db..63cdfb3e 100644 --- a/lib/logic/cubit/backups/backups_cubit.dart +++ b/lib/logic/cubit/backups/backups_cubit.dart @@ -1,5 +1,3 @@ -// ignore_for_file: always_specify_types - import 'dart:async'; import 'package:easy_localization/easy_localization.dart'; @@ -15,7 +13,9 @@ part 'backups_state.dart'; class BackupsCubit extends ServerInstallationDependendCubit { BackupsCubit(final ServerInstallationCubit serverInstallationCubit) : super( - serverInstallationCubit, const BackupsState(preventActions: true),); + serverInstallationCubit, + const BackupsState(preventActions: true), + ); final ServerApi api = ServerApi(); final BackblazeApi backblaze = BackblazeApi(); @@ -25,59 +25,72 @@ class BackupsCubit extends ServerInstallationDependendCubit { if (serverInstallationCubit.state is ServerInstallationFinished) { final BackblazeBucket? bucket = getIt().backblazeBucket; if (bucket == null) { - emit(const BackupsState( - isInitialized: false, preventActions: false, refreshing: false,),); + emit( + const BackupsState( + isInitialized: false, + preventActions: false, + refreshing: false, + ), + ); } else { final BackupStatus status = await api.getBackupStatus(); switch (status.status) { case BackupStatusEnum.noKey: case BackupStatusEnum.notInitialized: - emit(BackupsState( - backups: const [], - isInitialized: true, - preventActions: false, - progress: 0, - status: status.status, - refreshing: false, - ),); + emit( + BackupsState( + backups: const [], + isInitialized: true, + preventActions: false, + progress: 0, + status: status.status, + refreshing: false, + ), + ); break; case BackupStatusEnum.initializing: - emit(BackupsState( - backups: const [], - isInitialized: true, - preventActions: false, - progress: 0, - status: status.status, - refreshTimer: const Duration(seconds: 10), - refreshing: false, - ),); + emit( + BackupsState( + backups: const [], + isInitialized: true, + preventActions: false, + progress: 0, + status: status.status, + refreshTimer: const Duration(seconds: 10), + refreshing: false, + ), + ); break; case BackupStatusEnum.initialized: case BackupStatusEnum.error: final List backups = await api.getBackups(); - emit(BackupsState( - backups: backups, - isInitialized: true, - preventActions: false, - progress: status.progress, - status: status.status, - error: status.errorMessage ?? '', - refreshing: false, - ),); + emit( + BackupsState( + backups: backups, + isInitialized: true, + preventActions: false, + progress: status.progress, + status: status.status, + error: status.errorMessage ?? '', + refreshing: false, + ), + ); break; case BackupStatusEnum.backingUp: case BackupStatusEnum.restoring: final List backups = await api.getBackups(); - emit(BackupsState( - backups: backups, - isInitialized: true, - preventActions: true, - progress: status.progress, - status: status.status, - error: status.errorMessage ?? '', - refreshTimer: const Duration(seconds: 5), - refreshing: false, - ),); + emit( + BackupsState( + backups: backups, + isInitialized: true, + preventActions: true, + progress: status.progress, + status: status.status, + error: status.errorMessage ?? '', + refreshTimer: const Duration(seconds: 5), + refreshing: false, + ), + ); break; default: emit(const BackupsState()); @@ -101,10 +114,11 @@ class BackupsCubit extends ServerInstallationDependendCubit { final BackblazeApplicationKey key = await backblaze.createKey(bucketId); final BackblazeBucket bucket = BackblazeBucket( - bucketId: bucketId, - bucketName: bucketName, - applicationKey: key.applicationKey, - applicationKeyId: key.applicationKeyId,); + bucketId: bucketId, + bucketName: bucketName, + applicationKey: key.applicationKey, + applicationKeyId: key.applicationKeyId, + ); await getIt().storeBackblazeBucket(bucket); await api.uploadBackblazeConfig(bucket); @@ -141,14 +155,16 @@ class BackupsCubit extends ServerInstallationDependendCubit { emit(state.copyWith(refreshing: true)); final List backups = await api.getBackups(); final BackupStatus status = await api.getBackupStatus(); - emit(state.copyWith( - backups: backups, - progress: status.progress, - status: status.status, - error: status.errorMessage, - refreshTimer: refreshTimeFromState(status.status), - refreshing: false, - ),); + emit( + state.copyWith( + backups: backups, + progress: status.progress, + status: status.status, + error: status.errorMessage, + refreshTimer: refreshTimeFromState(status.status), + refreshing: false, + ), + ); if (useTimer) { Timer(state.refreshTimer, () => updateBackups(useTimer: true)); } diff --git a/lib/logic/cubit/backups/backups_state.dart b/lib/logic/cubit/backups/backups_state.dart index 3600c7a2..33ec52c8 100644 --- a/lib/logic/cubit/backups/backups_state.dart +++ b/lib/logic/cubit/backups/backups_state.dart @@ -1,5 +1,3 @@ -// ignore_for_file: always_specify_types - part of 'backups_cubit.dart'; class BackupsState extends ServerInstallationDependendState { diff --git a/lib/logic/cubit/devices/devices_cubit.dart b/lib/logic/cubit/devices/devices_cubit.dart index ec302477..01f17a7d 100644 --- a/lib/logic/cubit/devices/devices_cubit.dart +++ b/lib/logic/cubit/devices/devices_cubit.dart @@ -1,5 +1,3 @@ -// ignore_for_file: always_specify_types - import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/logic/api_maps/server.dart'; import 'package:selfprivacy/logic/common_enum/common_enum.dart'; @@ -50,9 +48,12 @@ class ApiDevicesCubit Future deleteDevice(final ApiToken device) async { final ApiResponse response = await api.deleteApiToken(device.name); if (response.isSuccess) { - emit(ApiDevicesState( + emit( + ApiDevicesState( state.devices.where((final d) => d.name != device.name).toList(), - LoadingStatus.success,),); + LoadingStatus.success, + ), + ); } else { getIt() .showSnackBar(response.errorMessage ?? 'Error deleting device'); @@ -65,7 +66,8 @@ class ApiDevicesCubit return response.data; } else { getIt().showSnackBar( - response.errorMessage ?? 'Error getting new device key',); + response.errorMessage ?? 'Error getting new device key', + ); return null; } } diff --git a/lib/logic/cubit/devices/devices_state.dart b/lib/logic/cubit/devices/devices_state.dart index ba7d7e90..86fd53c2 100644 --- a/lib/logic/cubit/devices/devices_state.dart +++ b/lib/logic/cubit/devices/devices_state.dart @@ -1,5 +1,3 @@ -// ignore_for_file: always_specify_types - part of 'devices_cubit.dart'; class ApiDevicesState extends ServerInstallationDependendState { @@ -10,12 +8,14 @@ class ApiDevicesState extends ServerInstallationDependendState { final LoadingStatus status; List get devices => _devices; - ApiToken get thisDevice => _devices.firstWhere((final device) => device.isCaller, - orElse: () => ApiToken( - name: 'Error fetching device', - isCaller: true, - date: DateTime.now(), - ),); + ApiToken get thisDevice => _devices.firstWhere( + (final device) => device.isCaller, + orElse: () => ApiToken( + name: 'Error fetching device', + isCaller: true, + date: DateTime.now(), + ), + ); List get otherDevices => _devices.where((final device) => !device.isCaller).toList(); @@ -23,10 +23,11 @@ class ApiDevicesState extends ServerInstallationDependendState { ApiDevicesState copyWith({ final List? devices, final LoadingStatus? status, - }) => ApiDevicesState( - devices ?? _devices, - status ?? this.status, - ); + }) => + ApiDevicesState( + devices ?? _devices, + status ?? this.status, + ); @override List get props => [_devices]; diff --git a/lib/logic/cubit/dns_records/dns_records_cubit.dart b/lib/logic/cubit/dns_records/dns_records_cubit.dart index d6faba8e..0590b065 100644 --- a/lib/logic/cubit/dns_records/dns_records_cubit.dart +++ b/lib/logic/cubit/dns_records/dns_records_cubit.dart @@ -1,5 +1,3 @@ -// ignore_for_file: always_specify_types - import 'package:cubit_form/cubit_form.dart'; import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart'; import 'package:selfprivacy/logic/models/hive/server_domain.dart'; @@ -13,18 +11,26 @@ part 'dns_records_state.dart'; class DnsRecordsCubit extends ServerInstallationDependendCubit { DnsRecordsCubit(final ServerInstallationCubit serverInstallationCubit) - : super(serverInstallationCubit, - const DnsRecordsState(dnsState: DnsRecordsStatus.refreshing),); + : super( + serverInstallationCubit, + const DnsRecordsState(dnsState: DnsRecordsStatus.refreshing), + ); final ServerApi api = ServerApi(); final CloudflareApi cloudflare = CloudflareApi(); @override Future load() async { - emit(DnsRecordsState( + emit( + DnsRecordsState( dnsState: DnsRecordsStatus.refreshing, dnsRecords: _getDesiredDnsRecords( - serverInstallationCubit.state.serverDomain?.domainName, '', '',),),); + serverInstallationCubit.state.serverDomain?.domainName, + '', + '', + ), + ), + ); print('Loading DNS status'); if (serverInstallationCubit.state is ServerInstallationFinished) { final ServerDomain? domain = serverInstallationCubit.state.serverDomain; @@ -41,41 +47,48 @@ class DnsRecordsCubit if (record.description == 'providers.domain.record_description.dkim') { final DnsRecord foundRecord = records.firstWhere( - (final r) => r.name == record.name && r.type == record.type, - orElse: () => DnsRecord( - name: record.name, - type: record.type, - content: '', - ttl: 800, - proxied: false,),); + (final r) => r.name == record.name && r.type == record.type, + orElse: () => DnsRecord( + name: record.name, + type: record.type, + content: '', + ttl: 800, + proxied: false, + ), + ); // remove all spaces and tabulators from // the foundRecord.content and the record.content // to compare them final String? foundContent = foundRecord.content?.replaceAll(RegExp(r'\s+'), ''); - final String content = record.content.replaceAll(RegExp(r'\s+'), ''); + final String content = + record.content.replaceAll(RegExp(r'\s+'), ''); if (foundContent == content) { foundRecords.add(record.copyWith(isSatisfied: true)); } else { foundRecords.add(record.copyWith(isSatisfied: false)); } } else { - if (records.any((final r) => - r.name == record.name && - r.type == record.type && - r.content == record.content,)) { + if (records.any( + (final r) => + r.name == record.name && + r.type == record.type && + r.content == record.content, + )) { foundRecords.add(record.copyWith(isSatisfied: true)); } else { foundRecords.add(record.copyWith(isSatisfied: false)); } } } - emit(DnsRecordsState( - dnsRecords: foundRecords, - dnsState: foundRecords.any((final r) => r.isSatisfied == false) - ? DnsRecordsStatus.error - : DnsRecordsStatus.good, - ),); + emit( + DnsRecordsState( + dnsRecords: foundRecords, + dnsState: foundRecords.any((final r) => r.isSatisfied == false) + ? DnsRecordsStatus.error + : DnsRecordsStatus.good, + ), + ); } else { emit(const DnsRecordsState()); } @@ -105,13 +118,18 @@ class DnsRecordsCubit final String? dkimPublicKey = await api.getDkim(); await cloudflare.removeSimilarRecords(cloudFlareDomain: domain!); await cloudflare.createMultipleDnsRecords( - cloudFlareDomain: domain, ip4: ipAddress,); + cloudFlareDomain: domain, + ip4: ipAddress, + ); await cloudflare.setDkim(dkimPublicKey ?? '', domain); await load(); } List _getDesiredDnsRecords( - final String? domainName, final String? ipAddress, final String? dkimPublicKey,) { + final String? domainName, + final String? ipAddress, + final String? dkimPublicKey, + ) { if (domainName == null || ipAddress == null || dkimPublicKey == null) { return []; } diff --git a/lib/logic/cubit/dns_records/dns_records_state.dart b/lib/logic/cubit/dns_records/dns_records_state.dart index d6d0c67b..4b39d014 100644 --- a/lib/logic/cubit/dns_records/dns_records_state.dart +++ b/lib/logic/cubit/dns_records/dns_records_state.dart @@ -1,5 +1,3 @@ -// ignore_for_file: always_specify_types - part of 'dns_records_cubit.dart'; enum DnsRecordsStatus { @@ -33,10 +31,11 @@ class DnsRecordsState extends ServerInstallationDependendState { DnsRecordsState copyWith({ final DnsRecordsStatus? dnsState, final List? dnsRecords, - }) => DnsRecordsState( - dnsState: dnsState ?? this.dnsState, - dnsRecords: dnsRecords ?? this.dnsRecords, - ); + }) => + DnsRecordsState( + dnsState: dnsState ?? this.dnsState, + dnsRecords: dnsRecords ?? this.dnsRecords, + ); } class DesiredDnsRecord { @@ -63,12 +62,13 @@ class DesiredDnsRecord { final String? description, final DnsRecordsCategory? category, final bool? isSatisfied, - }) => DesiredDnsRecord( - name: name ?? this.name, - type: type ?? this.type, - content: content ?? this.content, - description: description ?? this.description, - category: category ?? this.category, - isSatisfied: isSatisfied ?? this.isSatisfied, - ); + }) => + DesiredDnsRecord( + name: name ?? this.name, + type: type ?? this.type, + content: content ?? this.content, + description: description ?? this.description, + category: category ?? this.category, + isSatisfied: isSatisfied ?? this.isSatisfied, + ); } diff --git a/lib/logic/cubit/forms/factories/field_cubit_factory.dart b/lib/logic/cubit/forms/factories/field_cubit_factory.dart index d71f3a7b..62067cea 100644 --- a/lib/logic/cubit/forms/factories/field_cubit_factory.dart +++ b/lib/logic/cubit/forms/factories/field_cubit_factory.dart @@ -1,5 +1,3 @@ -// ignore_for_file: always_specify_types - import 'package:flutter/material.dart'; import 'package:selfprivacy/logic/cubit/forms/validations/validations.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -24,15 +22,20 @@ class FieldCubitFactory { initalValue: '', validations: [ ValidationModel( - (final String s) => s.toLowerCase() == 'root', 'validations.root_name'.tr(),), + (final String s) => s.toLowerCase() == 'root', + 'validations.root_name'.tr(), + ), ValidationModel( - (final String login) => context.read().state.isLoginRegistered(login), + (final String login) => + context.read().state.isLoginRegistered(login), 'validations.user_already_exist'.tr(), ), RequiredStringValidation('validations.required'.tr()), LengthStringLongerValidation(userMaxLength), - ValidationModel((final String s) => !userAllowedRegExp.hasMatch(s), - 'validations.invalid_format'.tr(),), + ValidationModel( + (final String s) => !userAllowedRegExp.hasMatch(s), + 'validations.invalid_format'.tr(), + ), ], ); } @@ -48,18 +51,19 @@ class FieldCubitFactory { validations: [ RequiredStringValidation('validations.required'.tr()), ValidationModel( - passwordForbiddenRegExp.hasMatch, - 'validations.invalid_format'.tr(),), + passwordForbiddenRegExp.hasMatch, + 'validations.invalid_format'.tr(), + ), ], ); } FieldCubit createRequiredStringField() => FieldCubit( - initalValue: '', - validations: [ - RequiredStringValidation('validations.required'.tr()), - ], - ); + initalValue: '', + validations: [ + RequiredStringValidation('validations.required'.tr()), + ], + ); final BuildContext context; } diff --git a/lib/logic/cubit/forms/setup/initializing/backblaze_form_cubit.dart b/lib/logic/cubit/forms/setup/initializing/backblaze_form_cubit.dart index 0ac87e30..4769286d 100644 --- a/lib/logic/cubit/forms/setup/initializing/backblaze_form_cubit.dart +++ b/lib/logic/cubit/forms/setup/initializing/backblaze_form_cubit.dart @@ -1,5 +1,3 @@ -// ignore_for_file: always_specify_types - import 'dart:async'; import 'package:cubit_form/cubit_form.dart'; import 'package:selfprivacy/logic/api_maps/backblaze.dart'; diff --git a/lib/logic/cubit/forms/setup/initializing/cloudflare_form_cubit.dart b/lib/logic/cubit/forms/setup/initializing/cloudflare_form_cubit.dart index c8dc14d1..01d26835 100644 --- a/lib/logic/cubit/forms/setup/initializing/cloudflare_form_cubit.dart +++ b/lib/logic/cubit/forms/setup/initializing/cloudflare_form_cubit.dart @@ -1,5 +1,3 @@ -// ignore_for_file: always_specify_types - import 'dart:async'; import 'package:cubit_form/cubit_form.dart'; @@ -16,7 +14,9 @@ class CloudFlareFormCubit extends FormCubit { validations: [ RequiredStringValidation('validations.required'.tr()), ValidationModel( - regExp.hasMatch, 'validations.key_format'.tr(),), + regExp.hasMatch, + 'validations.key_format'.tr(), + ), LengthStringNotEqualValidation(40) ], ); diff --git a/lib/logic/cubit/forms/setup/initializing/hetzner_form_cubit.dart b/lib/logic/cubit/forms/setup/initializing/hetzner_form_cubit.dart index a7e2bb1d..b8f47e10 100644 --- a/lib/logic/cubit/forms/setup/initializing/hetzner_form_cubit.dart +++ b/lib/logic/cubit/forms/setup/initializing/hetzner_form_cubit.dart @@ -1,5 +1,3 @@ -// ignore_for_file: always_specify_types - import 'dart:async'; import 'package:cubit_form/cubit_form.dart'; @@ -16,7 +14,9 @@ class HetznerFormCubit extends FormCubit { validations: [ RequiredStringValidation('validations.required'.tr()), ValidationModel( - regExp.hasMatch, 'validations.key_format'.tr(),), + regExp.hasMatch, + 'validations.key_format'.tr(), + ), LengthStringNotEqualValidation(64) ], ); diff --git a/lib/logic/cubit/forms/setup/initializing/root_user_form_cubit.dart b/lib/logic/cubit/forms/setup/initializing/root_user_form_cubit.dart index 0914d86b..b3cf606f 100644 --- a/lib/logic/cubit/forms/setup/initializing/root_user_form_cubit.dart +++ b/lib/logic/cubit/forms/setup/initializing/root_user_form_cubit.dart @@ -1,5 +1,3 @@ -// ignore_for_file: always_specify_types - import 'dart:async'; import 'package:cubit_form/cubit_form.dart'; @@ -9,7 +7,9 @@ import 'package:selfprivacy/logic/models/hive/user.dart'; class RootUserFormCubit extends FormCubit { RootUserFormCubit( - this.serverInstallationCubit, final FieldCubitFactory fieldFactory,) { + this.serverInstallationCubit, + final FieldCubitFactory fieldFactory, + ) { userName = fieldFactory.createUserLoginField(); password = fieldFactory.createUserPasswordField(); diff --git a/lib/logic/cubit/forms/setup/recovering/recovery_device_form_cubit.dart b/lib/logic/cubit/forms/setup/recovering/recovery_device_form_cubit.dart index 00f5b685..ad93871c 100644 --- a/lib/logic/cubit/forms/setup/recovering/recovery_device_form_cubit.dart +++ b/lib/logic/cubit/forms/setup/recovering/recovery_device_form_cubit.dart @@ -1,5 +1,3 @@ -// ignore_for_file: always_specify_types - import 'dart:async'; import 'package:cubit_form/cubit_form.dart'; @@ -7,8 +5,11 @@ import 'package:selfprivacy/logic/cubit/server_installation/server_installation_ import 'package:selfprivacy/logic/cubit/forms/factories/field_cubit_factory.dart'; class RecoveryDeviceFormCubit extends FormCubit { - RecoveryDeviceFormCubit(this.installationCubit, - final FieldCubitFactory fieldFactory, this.recoveryMethod,) { + RecoveryDeviceFormCubit( + this.installationCubit, + final FieldCubitFactory fieldFactory, + this.recoveryMethod, + ) { tokenField = fieldFactory.createRequiredStringField(); super.addFields([tokenField]); diff --git a/lib/logic/cubit/forms/setup/recovering/recovery_domain_form_cubit.dart b/lib/logic/cubit/forms/setup/recovering/recovery_domain_form_cubit.dart index d2bea806..664b87b8 100644 --- a/lib/logic/cubit/forms/setup/recovering/recovery_domain_form_cubit.dart +++ b/lib/logic/cubit/forms/setup/recovering/recovery_domain_form_cubit.dart @@ -1,5 +1,3 @@ -// ignore_for_file: always_specify_types - import 'dart:async'; import 'package:cubit_form/cubit_form.dart'; @@ -10,7 +8,9 @@ import 'package:selfprivacy/logic/cubit/forms/factories/field_cubit_factory.dart class RecoveryDomainFormCubit extends FormCubit { RecoveryDomainFormCubit( - this.initializingCubit, final FieldCubitFactory fieldFactory,) { + this.initializingCubit, + final FieldCubitFactory fieldFactory, + ) { serverDomainField = fieldFactory.createRequiredStringField(); super.addFields([serverDomainField]); @@ -25,9 +25,10 @@ class RecoveryDomainFormCubit extends FormCubit { @override FutureOr asyncValidation() async { final ServerApi api = ServerApi( - hasLogger: false, - isWithToken: false, - overrideDomain: serverDomainField.state.value,); + hasLogger: false, + isWithToken: false, + overrideDomain: serverDomainField.state.value, + ); // API version doesn't require access token, // so if the entered domain is indeed valid diff --git a/lib/logic/cubit/forms/user/ssh_form_cubit.dart b/lib/logic/cubit/forms/user/ssh_form_cubit.dart index 4262939e..ba38a642 100644 --- a/lib/logic/cubit/forms/user/ssh_form_cubit.dart +++ b/lib/logic/cubit/forms/user/ssh_form_cubit.dart @@ -1,5 +1,3 @@ -// ignore_for_file: always_specify_types - import 'dart:async'; import 'package:cubit_form/cubit_form.dart'; @@ -14,21 +12,26 @@ class SshFormCubit extends FormCubit { required this.user, }) { final RegExp keyRegExp = RegExp( - r'^(ssh-rsa AAAAB3NzaC1yc2|ssh-ed25519 AAAAC3NzaC1lZDI1NTE5)[0-9A-Za-z+/]+[=]{0,3}( .*)?$',); + r'^(ssh-rsa AAAAB3NzaC1yc2|ssh-ed25519 AAAAC3NzaC1lZDI1NTE5)[0-9A-Za-z+/]+[=]{0,3}( .*)?$', + ); key = FieldCubit( initalValue: '', validations: [ ValidationModel( - (final String newKey) => user.sshKeys.any((final String key) => key == newKey), + (final String newKey) => + user.sshKeys.any((final String key) => key == newKey), 'validations.key_already_exists'.tr(), ), RequiredStringValidation('validations.required'.tr()), - ValidationModel((final String s) { - print(s); - print(keyRegExp.hasMatch(s)); - return !keyRegExp.hasMatch(s); - }, 'validations.invalid_format'.tr(),), + ValidationModel( + (final String s) { + print(s); + print(keyRegExp.hasMatch(s)); + return !keyRegExp.hasMatch(s); + }, + 'validations.invalid_format'.tr(), + ), ], ); diff --git a/lib/logic/cubit/forms/user/user_form_cubit.dart b/lib/logic/cubit/forms/user/user_form_cubit.dart index 30d83e26..a385befb 100644 --- a/lib/logic/cubit/forms/user/user_form_cubit.dart +++ b/lib/logic/cubit/forms/user/user_form_cubit.dart @@ -1,5 +1,3 @@ -// ignore_for_file: always_specify_types - import 'dart:async'; import 'package:cubit_form/cubit_form.dart'; @@ -21,7 +19,8 @@ class UserFormCubit extends FormCubit { login.setValue(isEdit ? user.login : ''); password = fieldFactory.createUserPasswordField(); password.setValue( - isEdit ? (user.password ?? '') : StringGenerators.userPassword(),); + isEdit ? (user.password ?? '') : StringGenerators.userPassword(), + ); super.addFields([login, password]); } diff --git a/lib/logic/cubit/forms/validations/validations.dart b/lib/logic/cubit/forms/validations/validations.dart index dd2e653b..f233bded 100644 --- a/lib/logic/cubit/forms/validations/validations.dart +++ b/lib/logic/cubit/forms/validations/validations.dart @@ -1,5 +1,3 @@ -// ignore_for_file: always_specify_types - import 'package:cubit_form/cubit_form.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -9,7 +7,8 @@ abstract class LengthStringValidation extends ValidationModel { @override String? check(final String val) { final int length = val.length; - final String errorMessage = errorMassage.replaceAll('[]', length.toString()); + final String errorMessage = + errorMassage.replaceAll('[]', length.toString()); return test(val) ? errorMessage : null; } } @@ -17,13 +16,17 @@ abstract class LengthStringValidation extends ValidationModel { class LengthStringNotEqualValidation extends LengthStringValidation { /// String must be equal to [length] LengthStringNotEqualValidation(final int length) - : super((final n) => n.length != length, - 'validations.length_not_equal'.tr(args: [length.toString()]),); + : super( + (final n) => n.length != length, + 'validations.length_not_equal'.tr(args: [length.toString()]), + ); } class LengthStringLongerValidation extends LengthStringValidation { /// String must be shorter than or equal to [length] LengthStringLongerValidation(final int length) - : super((final n) => n.length > length, - 'validations.length_longer'.tr(args: [length.toString()]),); + : super( + (final n) => n.length > length, + 'validations.length_longer'.tr(args: [length.toString()]), + ); } diff --git a/lib/logic/cubit/hetzner_metrics/hetzner_metrics_repository.dart b/lib/logic/cubit/hetzner_metrics/hetzner_metrics_repository.dart index f9bcf15f..de7f3d43 100644 --- a/lib/logic/cubit/hetzner_metrics/hetzner_metrics_repository.dart +++ b/lib/logic/cubit/hetzner_metrics/hetzner_metrics_repository.dart @@ -1,5 +1,3 @@ -// ignore_for_file: always_specify_types - import 'package:selfprivacy/logic/api_maps/hetzner.dart'; import 'package:selfprivacy/logic/common_enum/common_enum.dart'; import 'package:selfprivacy/logic/models/hetzner_metrics.dart'; @@ -52,7 +50,11 @@ class HetznerMetricsRepository { } List timeSeriesSerializer( - final Map json, final String type,) { + final Map json, + final String type, +) { final List list = json['time_series'][type]['values']; - return list.map((final el) => TimeSeriesData(el[0], double.parse(el[1]))).toList(); + return list + .map((final el) => TimeSeriesData(el[0], double.parse(el[1]))) + .toList(); } diff --git a/lib/logic/cubit/hetzner_metrics/hetzner_metrics_state.dart b/lib/logic/cubit/hetzner_metrics/hetzner_metrics_state.dart index 793bfdfc..b6204db9 100644 --- a/lib/logic/cubit/hetzner_metrics/hetzner_metrics_state.dart +++ b/lib/logic/cubit/hetzner_metrics/hetzner_metrics_state.dart @@ -1,5 +1,3 @@ -// ignore_for_file: always_specify_types - part of 'hetzner_metrics_cubit.dart'; abstract class HetznerMetricsState extends Equatable { diff --git a/lib/logic/cubit/jobs/jobs_cubit.dart b/lib/logic/cubit/jobs/jobs_cubit.dart index 7f116f1d..6de64677 100644 --- a/lib/logic/cubit/jobs/jobs_cubit.dart +++ b/lib/logic/cubit/jobs/jobs_cubit.dart @@ -1,5 +1,3 @@ -// ignore_for_file: always_specify_types - import 'package:easy_localization/easy_localization.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -43,11 +41,12 @@ class JobsCubit extends Cubit { if (state is JobsStateWithJobs) { newJobsList.addAll((state as JobsStateWithJobs).jobList); } - final bool needToRemoveJob = - newJobsList.any((final el) => el is ServiceToggleJob && el.type == job.type); + final bool needToRemoveJob = newJobsList + .any((final el) => el is ServiceToggleJob && el.type == job.type); if (needToRemoveJob) { - final Job removingJob = newJobsList - .firstWhere((final el) => el is ServiceToggleJob && el.type == job.type); + final Job removingJob = newJobsList.firstWhere( + (final el) => el is ServiceToggleJob && el.type == job.type, + ); removeJob(removingJob.id); } else { newJobsList.add(job); @@ -61,7 +60,8 @@ class JobsCubit extends Cubit { if (state is JobsStateWithJobs) { newJobsList.addAll((state as JobsStateWithJobs).jobList); } - final bool isExistInJobList = newJobsList.any((final el) => el is CreateSSHKeyJob); + final bool isExistInJobList = + newJobsList.any((final el) => el is CreateSSHKeyJob); if (!isExistInJobList) { newJobsList.add(job); getIt().showSnackBar('jobs.jobAdded'.tr()); diff --git a/lib/logic/cubit/jobs/jobs_state.dart b/lib/logic/cubit/jobs/jobs_state.dart index 0817dfbf..dbcf968e 100644 --- a/lib/logic/cubit/jobs/jobs_state.dart +++ b/lib/logic/cubit/jobs/jobs_state.dart @@ -1,5 +1,3 @@ -// ignore_for_file: always_specify_types - part of 'jobs_cubit.dart'; abstract class JobsState extends Equatable { @@ -16,7 +14,8 @@ class JobsStateWithJobs extends JobsState { final List jobList; JobsState removeById(final String id) { - final List newJobsList = jobList.where((final element) => element.id != id).toList(); + final List newJobsList = + jobList.where((final element) => element.id != id).toList(); if (newJobsList.isEmpty) { return JobsStateEmpty(); diff --git a/lib/logic/cubit/providers/providers_state.dart b/lib/logic/cubit/providers/providers_state.dart index 89951b60..04146b5d 100644 --- a/lib/logic/cubit/providers/providers_state.dart +++ b/lib/logic/cubit/providers/providers_state.dart @@ -1,5 +1,3 @@ -// ignore_for_file: always_specify_types - part of 'providers_cubit.dart'; class ProvidersState extends Equatable { @@ -7,18 +5,23 @@ class ProvidersState extends Equatable { final List all; - ProvidersState updateElement(final ProviderModel provider, final StateType newState) { + ProvidersState updateElement( + final ProviderModel provider, + final StateType newState, + ) { final List newList = [...all]; final int index = newList.indexOf(provider); newList[index] = provider.updateState(newState); return ProvidersState(newList); } - List get connected => - all.where((final service) => service.state != StateType.uninitialized).toList(); + List get connected => all + .where((final service) => service.state != StateType.uninitialized) + .toList(); - List get uninitialized => - all.where((final service) => service.state == StateType.uninitialized).toList(); + List get uninitialized => all + .where((final service) => service.state == StateType.uninitialized) + .toList(); bool get isFullyInitialized => uninitialized.isEmpty; diff --git a/lib/logic/cubit/recovery_key/recovery_key_state.dart b/lib/logic/cubit/recovery_key/recovery_key_state.dart index bd6664ef..1b764298 100644 --- a/lib/logic/cubit/recovery_key/recovery_key_state.dart +++ b/lib/logic/cubit/recovery_key/recovery_key_state.dart @@ -1,13 +1,13 @@ -// ignore_for_file: always_specify_types - part of 'recovery_key_cubit.dart'; class RecoveryKeyState extends ServerInstallationDependendState { const RecoveryKeyState(this._status, this.loadingStatus); const RecoveryKeyState.initial() - : this(const RecoveryKeyStatus(exists: false, valid: false), - LoadingStatus.refreshing,); + : this( + const RecoveryKeyStatus(exists: false, valid: false), + LoadingStatus.refreshing, + ); final RecoveryKeyStatus _status; final LoadingStatus loadingStatus; @@ -23,8 +23,9 @@ class RecoveryKeyState extends ServerInstallationDependendState { RecoveryKeyState copyWith({ final RecoveryKeyStatus? status, final LoadingStatus? loadingStatus, - }) => RecoveryKeyState( - status ?? _status, - loadingStatus ?? this.loadingStatus, - ); + }) => + RecoveryKeyState( + status ?? _status, + loadingStatus ?? this.loadingStatus, + ); } diff --git a/lib/logic/cubit/server_detailed_info/server_detailed_info_state.dart b/lib/logic/cubit/server_detailed_info/server_detailed_info_state.dart index 6a984328..ef226c1e 100644 --- a/lib/logic/cubit/server_detailed_info/server_detailed_info_state.dart +++ b/lib/logic/cubit/server_detailed_info/server_detailed_info_state.dart @@ -1,5 +1,3 @@ -// ignore_for_file: always_specify_types - part of 'server_detailed_info_cubit.dart'; abstract class ServerDetailsState extends Equatable { @@ -18,7 +16,6 @@ class ServerDetailsNotReady extends ServerDetailsState {} class Loading extends ServerDetailsState {} class Loaded extends ServerDetailsState { - const Loaded({ required this.serverInfo, required this.serverTimezone, diff --git a/lib/logic/cubit/server_installation/server_installation_repository.dart b/lib/logic/cubit/server_installation/server_installation_repository.dart index 075e357b..ea6948ec 100644 --- a/lib/logic/cubit/server_installation/server_installation_repository.dart +++ b/lib/logic/cubit/server_installation/server_installation_repository.dart @@ -1,5 +1,3 @@ -// ignore_for_file: always_specify_types - import 'dart:io'; import 'package:basic_utils/basic_utils.dart'; @@ -29,13 +27,11 @@ import 'package:selfprivacy/ui/components/brand_alert/brand_alert.dart'; import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; class IpNotFoundException implements Exception { - IpNotFoundException(this.message); final String message; } class ServerAuthorizationException implements Exception { - ServerAuthorizationException(this.message); final String message; } @@ -47,8 +43,10 @@ class ServerInstallationRepository { final String? hetznerToken = getIt().hetznerKey; final String? cloudflareToken = getIt().cloudFlareKey; final ServerDomain? serverDomain = getIt().serverDomain; - final BackblazeCredential? backblazeCredential = getIt().backblazeCredential; - final ServerHostingDetails? serverDetails = getIt().serverDetails; + final BackblazeCredential? backblazeCredential = + getIt().backblazeCredential; + final ServerHostingDetails? serverDetails = + getIt().serverDetails; if (box.get(BNames.hasFinalChecked, defaultValue: false)) { return ServerInstallationFinished( @@ -76,7 +74,11 @@ class ServerInstallationRepository { serverDetails: serverDetails, rootUser: box.get(BNames.rootUser), currentStep: _getCurrentRecoveryStep( - hetznerToken, cloudflareToken, serverDomain, serverDetails,), + hetznerToken, + cloudflareToken, + serverDomain, + serverDetails, + ), recoveryCapabilities: await getRecoveryCapabilities(serverDomain), ); } @@ -146,8 +148,11 @@ class ServerInstallationRepository { } } - Future> isDnsAddressesMatch(final String? domainName, final String? ip4, - final Map? skippedMatches,) async { + Future> isDnsAddressesMatch( + final String? domainName, + final String? ip4, + final Map? skippedMatches, + ) async { final List addresses = [ '$domainName', 'api.$domainName', @@ -228,9 +233,11 @@ class ServerInstallationRepository { isRed: true, onPressed: () async { await hetznerApi.deleteSelfprivacyServerAndAllVolumes( - domainName: domainName,); + domainName: domainName, + ); - final ServerHostingDetails serverDetails = await hetznerApi.createServer( + final ServerHostingDetails serverDetails = + await hetznerApi.createServer( cloudFlareKey: cloudFlareKey, rootUser: rootUser, domainName: domainName, @@ -284,7 +291,8 @@ class ServerInstallationRepository { isRed: true, onPressed: () async { await hetznerApi.deleteSelfprivacyServerAndAllVolumes( - domainName: cloudFlareDomain.domainName,); + domainName: cloudFlareDomain.domainName, + ); onCancel(); }, @@ -358,8 +366,10 @@ class ServerInstallationRepository { Future getServerIpFromDomain(final ServerDomain serverDomain) async { final List? lookup = await DnsUtils.lookupRecord( - serverDomain.domainName, RRecordType.A, - provider: DnsApiProvider.CLOUDFLARE,); + serverDomain.domainName, + RRecordType.A, + provider: DnsApiProvider.CLOUDFLARE, + ); if (lookup == null || lookup.isEmpty) { throw IpNotFoundException('No IP found for domain $serverDomain'); } @@ -369,22 +379,32 @@ class ServerInstallationRepository { Future getDeviceName() async { final DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); if (kIsWeb) { - return deviceInfo.webBrowserInfo - .then((final WebBrowserInfo value) => '${value.browserName} ${value.platform}'); + return deviceInfo.webBrowserInfo.then( + (final WebBrowserInfo value) => + '${value.browserName} ${value.platform}', + ); } else { if (Platform.isAndroid) { - return deviceInfo.androidInfo - .then((final AndroidDeviceInfo value) => '${value.model} ${value.version.release}'); + return deviceInfo.androidInfo.then( + (final AndroidDeviceInfo value) => + '${value.model} ${value.version.release}', + ); } else if (Platform.isIOS) { - return deviceInfo.iosInfo.then((final IosDeviceInfo value) => - '${value.utsname.machine} ${value.systemName} ${value.systemVersion}',); + return deviceInfo.iosInfo.then( + (final IosDeviceInfo value) => + '${value.utsname.machine} ${value.systemName} ${value.systemVersion}', + ); } else if (Platform.isLinux) { - return deviceInfo.linuxInfo.then((final LinuxDeviceInfo value) => value.prettyName); + return deviceInfo.linuxInfo + .then((final LinuxDeviceInfo value) => value.prettyName); } else if (Platform.isMacOS) { - return deviceInfo.macOsInfo - .then((final MacOsDeviceInfo value) => '${value.hostName} ${value.computerName}'); + return deviceInfo.macOsInfo.then( + (final MacOsDeviceInfo value) => + '${value.hostName} ${value.computerName}', + ); } else if (Platform.isWindows) { - return deviceInfo.windowsInfo.then((final WindowsDeviceInfo value) => value.computerName); + return deviceInfo.windowsInfo + .then((final WindowsDeviceInfo value) => value.computerName); } } return 'Unidentified'; @@ -401,7 +421,8 @@ class ServerInstallationRepository { ); final String serverIp = await getServerIpFromDomain(serverDomain); final ApiResponse apiResponse = await serverApi.authorizeDevice( - DeviceToken(device: await getDeviceName(), token: newDeviceKey),); + DeviceToken(device: await getDeviceName(), token: newDeviceKey), + ); if (apiResponse.isSuccess) { return ServerHostingDetails( @@ -434,7 +455,8 @@ class ServerInstallationRepository { ); final String serverIp = await getServerIpFromDomain(serverDomain); final ApiResponse apiResponse = await serverApi.useRecoveryToken( - DeviceToken(device: await getDeviceName(), token: recoveryKey),); + DeviceToken(device: await getDeviceName(), token: recoveryKey), + ); if (apiResponse.isSuccess) { return ServerHostingDetails( @@ -468,7 +490,8 @@ class ServerInstallationRepository { ); final String serverIp = await getServerIpFromDomain(serverDomain); if (recoveryCapabilities == ServerRecoveryCapabilities.legacy) { - final Map apiResponse = await serverApi.servicesPowerCheck(); + final Map apiResponse = + await serverApi.servicesPowerCheck(); if (apiResponse.isNotEmpty) { return ServerHostingDetails( apiToken: apiToken, @@ -488,9 +511,11 @@ class ServerInstallationRepository { ); } } - final ApiResponse deviceAuthKey = await serverApi.createDeviceToken(); + final ApiResponse deviceAuthKey = + await serverApi.createDeviceToken(); final ApiResponse apiResponse = await serverApi.authorizeDevice( - DeviceToken(device: await getDeviceName(), token: deviceAuthKey.data),); + DeviceToken(device: await getDeviceName(), token: deviceAuthKey.data), + ); if (apiResponse.isSuccess) { return ServerHostingDetails( @@ -522,7 +547,8 @@ class ServerInstallationRepository { ); final String? serverApiVersion = await serverApi.getApiVersion(); - final ApiResponse> users = await serverApi.getUsersList(withMainUser: true); + final ApiResponse> users = + await serverApi.getUsersList(withMainUser: true); if (serverApiVersion == null || !users.isSuccess) { return fallbackUser; } @@ -544,18 +570,22 @@ class ServerInstallationRepository { final HetznerApi hetznerApi = HetznerApi(); final List servers = await hetznerApi.getServers(); return servers - .map((final HetznerServerInfo server) => ServerBasicInfo( - id: server.id, - name: server.name, - ip: server.publicNet.ipv4.ip, - reverseDns: server.publicNet.ipv4.reverseDns, - created: server.created, - volumeId: server.volumes.isNotEmpty ? server.volumes[0] : 0, - ),) + .map( + (final HetznerServerInfo server) => ServerBasicInfo( + id: server.id, + name: server.name, + ip: server.publicNet.ipv4.ip, + reverseDns: server.publicNet.ipv4.reverseDns, + created: server.created, + volumeId: server.volumes.isNotEmpty ? server.volumes[0] : 0, + ), + ) .toList(); } - Future saveServerDetails(final ServerHostingDetails serverDetails) async { + Future saveServerDetails( + final ServerHostingDetails serverDetails, + ) async { await getIt().storeServerDetails(serverDetails); } @@ -569,7 +599,9 @@ class ServerInstallationRepository { getIt().init(); } - Future saveBackblazeKey(final BackblazeCredential backblazeCredential) async { + Future saveBackblazeKey( + final BackblazeCredential backblazeCredential, + ) async { await getIt().storeBackblazeCredential(backblazeCredential); } diff --git a/lib/logic/cubit/server_installation/server_installation_state.dart b/lib/logic/cubit/server_installation/server_installation_state.dart index 8cd0c2e1..b3128e71 100644 --- a/lib/logic/cubit/server_installation/server_installation_state.dart +++ b/lib/logic/cubit/server_installation/server_installation_state.dart @@ -1,5 +1,3 @@ -// ignore_for_file: always_specify_types - part of '../server_installation/server_installation_cubit.dart'; abstract class ServerInstallationState extends Equatable { @@ -45,8 +43,8 @@ abstract class ServerInstallationState extends Equatable { bool get isServerCreated => serverDetails != null; bool get isFullyInitilized => _fulfilementList.every((final el) => el!); - ServerSetupProgress get progress => - ServerSetupProgress.values[_fulfilementList.where((final el) => el!).length]; + ServerSetupProgress get progress => ServerSetupProgress + .values[_fulfilementList.where((final el) => el!).length]; int get porgressBar { if (progress.index < 6) { @@ -120,7 +118,6 @@ enum ServerSetupProgress { } class ServerInstallationNotFinished extends ServerInstallationState { - const ServerInstallationNotFinished({ required final super.isServerStarted, required final super.isServerResetedFirstTime, @@ -260,7 +257,6 @@ enum ServerRecoveryMethods { } class ServerInstallationRecovery extends ServerInstallationState { - const ServerInstallationRecovery({ required this.currentStep, required this.recoveryCapabilities, @@ -313,14 +309,14 @@ class ServerInstallationRecovery extends ServerInstallationState { ); ServerInstallationFinished finish() => ServerInstallationFinished( - hetznerKey: hetznerKey!, - cloudFlareKey: cloudFlareKey!, - backblazeCredential: backblazeCredential!, - serverDomain: serverDomain!, - rootUser: rootUser!, - serverDetails: serverDetails!, - isServerStarted: true, - isServerResetedFirstTime: true, - isServerResetedSecondTime: true, - ); + hetznerKey: hetznerKey!, + cloudFlareKey: cloudFlareKey!, + backblazeCredential: backblazeCredential!, + serverDomain: serverDomain!, + rootUser: rootUser!, + serverDetails: serverDetails!, + isServerStarted: true, + isServerResetedFirstTime: true, + isServerResetedSecondTime: true, + ); } diff --git a/lib/logic/cubit/services/services_state.dart b/lib/logic/cubit/services/services_state.dart index bec81fe1..ffe90aee 100644 --- a/lib/logic/cubit/services/services_state.dart +++ b/lib/logic/cubit/services/services_state.dart @@ -1,5 +1,3 @@ -// ignore_for_file: always_specify_types - part of 'services_cubit.dart'; class ServicesState extends ServerInstallationDependendState { diff --git a/lib/logic/cubit/users/users_cubit.dart b/lib/logic/cubit/users/users_cubit.dart index b96c5683..9b86c109 100644 --- a/lib/logic/cubit/users/users_cubit.dart +++ b/lib/logic/cubit/users/users_cubit.dart @@ -1,5 +1,3 @@ -// ignore_for_file: always_specify_types - import 'package:hive/hive.dart'; import 'package:selfprivacy/config/hive_config.dart'; import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart'; @@ -14,9 +12,13 @@ part 'users_state.dart'; class UsersCubit extends ServerInstallationDependendCubit { UsersCubit(final ServerInstallationCubit serverInstallationCubit) : super( - serverInstallationCubit, - const UsersState( - [], User(login: 'root'), User(login: 'loading...'),),); + serverInstallationCubit, + const UsersState( + [], + User(login: 'root'), + User(login: 'loading...'), + ), + ); Box box = Hive.box(BNames.usersBox); Box serverInstallationBox = Hive.box(BNames.serverInstallationBox); @@ -26,22 +28,35 @@ class UsersCubit extends ServerInstallationDependendCubit { Future load() async { if (serverInstallationCubit.state is ServerInstallationFinished) { final List loadedUsers = box.values.toList(); - final primaryUser = serverInstallationBox.get(BNames.rootUser, - defaultValue: const User(login: 'loading...'),); + final primaryUser = serverInstallationBox.get( + BNames.rootUser, + defaultValue: const User(login: 'loading...'), + ); final List rootKeys = [ ...serverInstallationBox.get(BNames.rootKeys, defaultValue: []) ]; if (loadedUsers.isNotEmpty) { - emit(UsersState( - loadedUsers, User(login: 'root', sshKeys: rootKeys), primaryUser,),); + emit( + UsersState( + loadedUsers, + User(login: 'root', sshKeys: rootKeys), + primaryUser, + ), + ); } - final ApiResponse> usersFromServer = await api.getUsersList(); + final ApiResponse> usersFromServer = + await api.getUsersList(); if (usersFromServer.isSuccess) { final List updatedList = mergeLocalAndServerUsers(loadedUsers, usersFromServer.data); - emit(UsersState( - updatedList, User(login: 'root', sshKeys: rootKeys), primaryUser,),); + emit( + UsersState( + updatedList, + User(login: 'root', sshKeys: rootKeys), + primaryUser, + ), + ); } final List usersWithSshKeys = await loadSshKeys(state.users); @@ -49,18 +64,26 @@ class UsersCubit extends ServerInstallationDependendCubit { box.clear(); box.addAll(usersWithSshKeys); - final User rootUserWithSshKeys = (await loadSshKeys([state.rootUser])).first; + final User rootUserWithSshKeys = + (await loadSshKeys([state.rootUser])).first; serverInstallationBox.put(BNames.rootKeys, rootUserWithSshKeys.sshKeys); final User primaryUserWithSshKeys = (await loadSshKeys([state.primaryUser])).first; serverInstallationBox.put(BNames.rootUser, primaryUserWithSshKeys); - emit(UsersState( - usersWithSshKeys, rootUserWithSshKeys, primaryUserWithSshKeys,),); + emit( + UsersState( + usersWithSshKeys, + rootUserWithSshKeys, + primaryUserWithSshKeys, + ), + ); } } List mergeLocalAndServerUsers( - final List localUsers, final List serverUsers,) { + final List localUsers, + final List serverUsers, + ) { // If local user not exists on server, add it with isFoundOnServer = false // If server user not exists on local, add it @@ -69,28 +92,34 @@ class UsersCubit extends ServerInstallationDependendCubit { for (final User localUser in localUsers) { if (serverUsersCopy.contains(localUser.login)) { - mergedUsers.add(User( - login: localUser.login, - isFoundOnServer: true, - password: localUser.password, - sshKeys: localUser.sshKeys, - ),); + mergedUsers.add( + User( + login: localUser.login, + isFoundOnServer: true, + password: localUser.password, + sshKeys: localUser.sshKeys, + ), + ); serverUsersCopy.remove(localUser.login); } else { - mergedUsers.add(User( - login: localUser.login, - isFoundOnServer: false, - password: localUser.password, - note: localUser.note, - ),); + mergedUsers.add( + User( + login: localUser.login, + isFoundOnServer: false, + password: localUser.password, + note: localUser.note, + ), + ); } } for (final String serverUser in serverUsersCopy) { - mergedUsers.add(User( - login: serverUser, - isFoundOnServer: true, - ),); + mergedUsers.add( + User( + login: serverUser, + isFoundOnServer: true, + ), + ); } return mergedUsers; @@ -103,31 +132,38 @@ class UsersCubit extends ServerInstallationDependendCubit { if (user.isFoundOnServer || user.login == 'root' || user.login == state.primaryUser.login) { - final ApiResponse> sshKeys = await api.getUserSshKeys(user); + final ApiResponse> sshKeys = + await api.getUserSshKeys(user); print('sshKeys for $user: ${sshKeys.data}'); if (sshKeys.isSuccess) { - updatedUsers.add(User( - login: user.login, - isFoundOnServer: true, - password: user.password, - sshKeys: sshKeys.data, - note: user.note, - ),); + updatedUsers.add( + User( + login: user.login, + isFoundOnServer: true, + password: user.password, + sshKeys: sshKeys.data, + note: user.note, + ), + ); } else { - updatedUsers.add(User( - login: user.login, - isFoundOnServer: true, - password: user.password, - note: user.note, - ),); + updatedUsers.add( + User( + login: user.login, + isFoundOnServer: true, + password: user.password, + note: user.note, + ), + ); } } else { - updatedUsers.add(User( - login: user.login, - isFoundOnServer: false, - password: user.password, - note: user.note, - ),); + updatedUsers.add( + User( + login: user.login, + isFoundOnServer: false, + password: user.password, + note: user.note, + ), + ); } } return updatedUsers; @@ -143,19 +179,26 @@ class UsersCubit extends ServerInstallationDependendCubit { final List usersWithSshKeys = await loadSshKeys(updatedUsers); box.clear(); box.addAll(usersWithSshKeys); - final User rootUserWithSshKeys = (await loadSshKeys([state.rootUser])).first; + final User rootUserWithSshKeys = + (await loadSshKeys([state.rootUser])).first; serverInstallationBox.put(BNames.rootKeys, rootUserWithSshKeys.sshKeys); final User primaryUserWithSshKeys = (await loadSshKeys([state.primaryUser])).first; serverInstallationBox.put(BNames.rootUser, primaryUserWithSshKeys); - emit(UsersState( - usersWithSshKeys, rootUserWithSshKeys, primaryUserWithSshKeys,),); + emit( + UsersState( + usersWithSshKeys, + rootUserWithSshKeys, + primaryUserWithSshKeys, + ), + ); return; } Future createUser(final User user) async { // If user exists on server, do nothing - if (state.users.any((final User u) => u.login == user.login && u.isFoundOnServer)) { + if (state.users + .any((final User u) => u.login == user.login && u.isFoundOnServer)) { return; } // If user is root or primary user, do nothing @@ -201,15 +244,17 @@ class UsersCubit extends ServerInstallationDependendCubit { .get(BNames.rootKeys, defaultValue: []) as List; rootKeys.add(publicKey); serverInstallationBox.put(BNames.rootKeys, rootKeys); - emit(state.copyWith( - rootUser: User( - login: state.rootUser.login, - isFoundOnServer: true, - password: state.rootUser.password, - sshKeys: rootKeys, - note: state.rootUser.note, + emit( + state.copyWith( + rootUser: User( + login: state.rootUser.login, + isFoundOnServer: true, + password: state.rootUser.password, + sshKeys: rootKeys, + note: state.rootUser.note, + ), ), - ),); + ); } } else { final ApiResponse result = await api.addUserSshKey(user, publicKey); @@ -227,9 +272,11 @@ class UsersCubit extends ServerInstallationDependendCubit { note: state.primaryUser.note, ); serverInstallationBox.put(BNames.rootUser, updatedUser); - emit(state.copyWith( - primaryUser: updatedUser, - ),); + emit( + state.copyWith( + primaryUser: updatedUser, + ), + ); } else { // If it is not primary user, update user final List userKeys = List.from(user.sshKeys); @@ -242,9 +289,11 @@ class UsersCubit extends ServerInstallationDependendCubit { note: user.note, ); await box.putAt(box.values.toList().indexOf(user), updatedUser); - emit(state.copyWith( - users: box.values.toList(), - ),); + emit( + state.copyWith( + users: box.values.toList(), + ), + ); } } } @@ -253,7 +302,8 @@ class UsersCubit extends ServerInstallationDependendCubit { Future deleteSshKey(final User user, final String publicKey) async { // All keys are deleted via api.deleteUserSshKey - final ApiResponse result = await api.deleteUserSshKey(user, publicKey); + final ApiResponse result = + await api.deleteUserSshKey(user, publicKey); if (result.isSuccess) { // If it is root user, delete key from root keys // If it is primary user, update primary user @@ -264,15 +314,17 @@ class UsersCubit extends ServerInstallationDependendCubit { .get(BNames.rootKeys, defaultValue: []) as List; rootKeys.remove(publicKey); serverInstallationBox.put(BNames.rootKeys, rootKeys); - emit(state.copyWith( - rootUser: User( - login: state.rootUser.login, - isFoundOnServer: true, - password: state.rootUser.password, - sshKeys: rootKeys, - note: state.rootUser.note, + emit( + state.copyWith( + rootUser: User( + login: state.rootUser.login, + isFoundOnServer: true, + password: state.rootUser.password, + sshKeys: rootKeys, + note: state.rootUser.note, + ), ), - ),); + ); return; } if (user.login == state.primaryUser.login) { @@ -287,9 +339,11 @@ class UsersCubit extends ServerInstallationDependendCubit { note: state.primaryUser.note, ); serverInstallationBox.put(BNames.rootUser, updatedUser); - emit(state.copyWith( - primaryUser: updatedUser, - ),); + emit( + state.copyWith( + primaryUser: updatedUser, + ), + ); return; } final List userKeys = List.from(user.sshKeys); @@ -302,15 +356,22 @@ class UsersCubit extends ServerInstallationDependendCubit { note: user.note, ); await box.putAt(box.values.toList().indexOf(user), updatedUser); - emit(state.copyWith( - users: box.values.toList(), - ),); + emit( + state.copyWith( + users: box.values.toList(), + ), + ); } } @override void clear() async { - emit(const UsersState( - [], User(login: 'root'), User(login: 'loading...'),),); + emit( + const UsersState( + [], + User(login: 'root'), + User(login: 'loading...'), + ), + ); } } diff --git a/lib/logic/cubit/users/users_state.dart b/lib/logic/cubit/users/users_state.dart index a02eb2c1..fa4ed1cd 100644 --- a/lib/logic/cubit/users/users_state.dart +++ b/lib/logic/cubit/users/users_state.dart @@ -1,5 +1,3 @@ -// ignore_for_file: always_specify_types - part of 'users_cubit.dart'; class UsersState extends ServerInstallationDependendState { @@ -16,15 +14,17 @@ class UsersState extends ServerInstallationDependendState { final List? users, final User? rootUser, final User? primaryUser, - }) => UsersState( - users ?? this.users, - rootUser ?? this.rootUser, - primaryUser ?? this.primaryUser, - ); + }) => + UsersState( + users ?? this.users, + rootUser ?? this.rootUser, + primaryUser ?? this.primaryUser, + ); - bool isLoginRegistered(final String login) => users.any((final User user) => user.login == login) || - login == rootUser.login || - login == primaryUser.login; + bool isLoginRegistered(final String login) => + users.any((final User user) => user.login == login) || + login == rootUser.login || + login == primaryUser.login; bool get isEmpty => users.isEmpty; } diff --git a/lib/logic/get_it/api_config.dart b/lib/logic/get_it/api_config.dart index 6a03e2ef..3f3e5ac0 100644 --- a/lib/logic/get_it/api_config.dart +++ b/lib/logic/get_it/api_config.dart @@ -1,5 +1,3 @@ -// ignore_for_file: always_specify_types - import 'package:hive/hive.dart'; import 'package:selfprivacy/config/hive_config.dart'; import 'package:selfprivacy/logic/models/hive/backblaze_bucket.dart'; diff --git a/lib/logic/get_it/console.dart b/lib/logic/get_it/console.dart index 2fc67b7d..290f31ab 100644 --- a/lib/logic/get_it/console.dart +++ b/lib/logic/get_it/console.dart @@ -1,5 +1,3 @@ -// ignore_for_file: always_specify_types - import 'package:flutter/material.dart'; import 'package:selfprivacy/logic/models/message.dart'; diff --git a/lib/logic/models/hive/user.dart b/lib/logic/models/hive/user.dart index 74809f7e..942ce9fe 100644 --- a/lib/logic/models/hive/user.dart +++ b/lib/logic/models/hive/user.dart @@ -1,5 +1,3 @@ -// ignore_for_file: always_specify_types - import 'dart:ui'; import 'package:equatable/equatable.dart'; @@ -39,5 +37,6 @@ class User extends Equatable { Color get color => stringToColor(login); @override - String toString() => '$login, ${isFoundOnServer ? 'found' : 'not found'}, ${sshKeys.length} ssh keys, note: $note'; + String toString() => + '$login, ${isFoundOnServer ? 'found' : 'not found'}, ${sshKeys.length} ssh keys, note: $note'; } diff --git a/lib/logic/models/hive/user.g.dart b/lib/logic/models/hive/user.g.dart index 57c08555..d9b28d65 100644 --- a/lib/logic/models/hive/user.g.dart +++ b/lib/logic/models/hive/user.g.dart @@ -1,7 +1,5 @@ // GENERATED CODE - DO NOT MODIFY BY HAND -// ignore_for_file: always_specify_types - part of 'user.dart'; // ************************************************************************** diff --git a/lib/logic/models/job.dart b/lib/logic/models/job.dart index d64b79e2..b04d7d05 100644 --- a/lib/logic/models/job.dart +++ b/lib/logic/models/job.dart @@ -1,5 +1,3 @@ -// ignore_for_file: always_specify_types - import 'package:easy_localization/easy_localization.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart'; diff --git a/lib/logic/models/json/auto_upgrade_settings.dart b/lib/logic/models/json/auto_upgrade_settings.dart index 848c6437..421f9b88 100644 --- a/lib/logic/models/json/auto_upgrade_settings.dart +++ b/lib/logic/models/json/auto_upgrade_settings.dart @@ -1,5 +1,3 @@ -// ignore_for_file: always_specify_types - import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; diff --git a/lib/logic/models/json/hetzner_server_info.dart b/lib/logic/models/json/hetzner_server_info.dart index b7e19346..ccf036a1 100644 --- a/lib/logic/models/json/hetzner_server_info.dart +++ b/lib/logic/models/json/hetzner_server_info.dart @@ -1,12 +1,9 @@ -// ignore_for_file: always_specify_types - import 'package:json_annotation/json_annotation.dart'; part 'hetzner_server_info.g.dart'; @JsonSerializable() class HetznerServerInfo { - HetznerServerInfo( this.id, this.name, @@ -41,7 +38,6 @@ class HetznerServerInfo { @JsonSerializable() class HetznerPublicNetInfo { - HetznerPublicNetInfo(this.ipv4); final HetznerIp4 ipv4; @@ -51,7 +47,6 @@ class HetznerPublicNetInfo { @JsonSerializable() class HetznerIp4 { - HetznerIp4(this.id, this.ip, this.blocked, this.reverseDns); final bool blocked; @JsonKey(name: 'dns_ptr') @@ -77,7 +72,6 @@ enum ServerStatus { @JsonSerializable() class HetznerServerTypeInfo { - HetznerServerTypeInfo(this.cores, this.memory, this.disk, this.prices); final int cores; final num memory; @@ -102,12 +96,12 @@ class HetznerPriceInfo { static HetznerPriceInfo fromJson(final Map json) => _$HetznerPriceInfoFromJson(json); - static double getPrice(final Map json) => double.parse(json['gross'] as String); + static double getPrice(final Map json) => + double.parse(json['gross'] as String); } @JsonSerializable() class HetznerLocation { - HetznerLocation(this.country, this.city, this.description, this.zone); final String country; final String city; diff --git a/lib/logic/models/json/recovery_token_status.dart b/lib/logic/models/json/recovery_token_status.dart index 2c455480..6e59b57d 100644 --- a/lib/logic/models/json/recovery_token_status.dart +++ b/lib/logic/models/json/recovery_token_status.dart @@ -1,5 +1,3 @@ -// ignore_for_file: always_specify_types - import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; @@ -7,7 +5,6 @@ part 'recovery_token_status.g.dart'; @JsonSerializable() class RecoveryKeyStatus extends Equatable { - factory RecoveryKeyStatus.fromJson(final Map json) => _$RecoveryKeyStatusFromJson(json); const RecoveryKeyStatus({ diff --git a/lib/logic/models/json/server_configurations.dart b/lib/logic/models/json/server_configurations.dart index 91f5f65d..8b4029ab 100644 --- a/lib/logic/models/json/server_configurations.dart +++ b/lib/logic/models/json/server_configurations.dart @@ -1,5 +1,3 @@ -// ignore_for_file: always_specify_types - import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; @@ -7,7 +5,6 @@ part 'server_configurations.g.dart'; @JsonSerializable(createToJson: true) class AutoUpgradeConfigurations extends Equatable { - factory AutoUpgradeConfigurations.fromJson(final Map json) => _$AutoUpgradeConfigurationsFromJson(json); const AutoUpgradeConfigurations({ diff --git a/lib/logic/models/message.dart b/lib/logic/models/message.dart index 44f30b4d..8bbc6dfd 100644 --- a/lib/logic/models/message.dart +++ b/lib/logic/models/message.dart @@ -4,16 +4,14 @@ final DateFormat formatter = DateFormat('hh:mm'); class Message { Message({this.text, this.type = MessageType.normal}) : time = DateTime.now(); + Message.warn({this.text}) + : type = MessageType.warning, + time = DateTime.now(); final String? text; final DateTime time; final MessageType type; String get timeString => formatter.format(time); - - static Message warn({final String? text}) => Message( - text: text, - type: MessageType.warning, - ); } enum MessageType { diff --git a/lib/logic/models/provider.dart b/lib/logic/models/provider.dart index 4e3191eb..6feb175b 100644 --- a/lib/logic/models/provider.dart +++ b/lib/logic/models/provider.dart @@ -1,5 +1,3 @@ -// ignore_for_file: always_specify_types - import 'package:equatable/equatable.dart'; import 'package:flutter/widgets.dart'; import 'package:selfprivacy/logic/models/state_types.dart'; diff --git a/lib/logic/models/server_basic_info.dart b/lib/logic/models/server_basic_info.dart index 7f0f2e1b..8670dc8c 100644 --- a/lib/logic/models/server_basic_info.dart +++ b/lib/logic/models/server_basic_info.dart @@ -1,7 +1,4 @@ -// ignore_for_file: always_specify_types - class ServerBasicInfo { - ServerBasicInfo({ required this.id, required this.name, @@ -19,7 +16,6 @@ class ServerBasicInfo { } class ServerBasicInfoWithValidators extends ServerBasicInfo { - ServerBasicInfoWithValidators.fromServerBasicInfo({ required final ServerBasicInfo serverBasicInfo, required final isIpValid, diff --git a/lib/logic/models/timezone_settings.dart b/lib/logic/models/timezone_settings.dart index 45348b64..22c84b44 100644 --- a/lib/logic/models/timezone_settings.dart +++ b/lib/logic/models/timezone_settings.dart @@ -1,9 +1,6 @@ -// ignore_for_file: always_specify_types - import 'package:timezone/timezone.dart'; class TimeZoneSettings { - factory TimeZoneSettings.fromString(final String string) { final Location location = timeZoneDatabase.locations[string]!; return TimeZoneSettings(location); @@ -13,6 +10,6 @@ class TimeZoneSettings { final Location timezone; Map toJson() => { - 'timezone': timezone.name, - }; + 'timezone': timezone.name, + }; } diff --git a/lib/main.dart b/lib/main.dart index f7234d26..f2c36392 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,3 @@ -// ignore_for_file: always_specify_types - import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -46,11 +44,14 @@ void main() async { ); BlocOverrides.runZoned( - () => runApp(Localization( + () => runApp( + Localization( child: MyApp( - lightThemeData: lightThemeData, - darkThemeData: darkThemeData, - ),),), + lightThemeData: lightThemeData, + darkThemeData: darkThemeData, + ), + ), + ), blocObserver: SimpleBlocObserver(), ); } @@ -67,11 +68,15 @@ class MyApp extends StatelessWidget { @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( + 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, @@ -97,8 +102,8 @@ class MyApp extends StatelessWidget { return widget!; }, ), + ), ), ), - ), - ); + ); } diff --git a/lib/theming/factory/app_theme_factory.dart b/lib/theming/factory/app_theme_factory.dart index 92979267..48f4b086 100644 --- a/lib/theming/factory/app_theme_factory.dart +++ b/lib/theming/factory/app_theme_factory.dart @@ -1,22 +1,22 @@ -// ignore_for_file: always_specify_types - import 'dart:io'; import 'package:dynamic_color/dynamic_color.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:material_color_utilities/palettes/core_palette.dart'; import 'package:system_theme/system_theme.dart'; import 'package:gtk_theme_fl/gtk_theme_fl.dart'; abstract class AppThemeFactory { AppThemeFactory._(); - static Future create( - {required final bool isDark, required final Color fallbackColor,}) => _createAppTheme( - isDark: isDark, - fallbackColor: fallbackColor, - ); + static Future create({ + required final bool isDark, + required final Color fallbackColor, + }) => + _createAppTheme( + isDark: isDark, + fallbackColor: fallbackColor, + ); static Future _createAppTheme({ required final Color fallbackColor, @@ -25,7 +25,8 @@ abstract class AppThemeFactory { ColorScheme? gtkColorsScheme; final Brightness brightness = isDark ? Brightness.dark : Brightness.light; - final ColorScheme? dynamicColorsScheme = await _getDynamicColors(brightness); + final ColorScheme? dynamicColorsScheme = + await _getDynamicColors(brightness); if (Platform.isLinux) { final GtkThemeData themeData = await GtkThemeData.initialize(); @@ -77,7 +78,8 @@ abstract class AppThemeFactory { static Future _getDynamicColors(final Brightness brightness) { try { return DynamicColorPlugin.getCorePalette().then( - (final CorePalette? corePallet) => corePallet?.toColorScheme(brightness: brightness),); + (final corePallet) => corePallet?.toColorScheme(brightness: brightness), + ); } on PlatformException { return Future.value(null); } diff --git a/lib/ui/components/action_button/action_button.dart b/lib/ui/components/action_button/action_button.dart index 580a69fb..3a518496 100644 --- a/lib/ui/components/action_button/action_button.dart +++ b/lib/ui/components/action_button/action_button.dart @@ -24,7 +24,7 @@ class ActionButton extends StatelessWidget { ), onPressed: () { navigator.pop(); - if (onPressed != null) onPressed!(); + onPressed?.call(); }, ); } diff --git a/lib/ui/components/brand_bottom_sheet/brand_bottom_sheet.dart b/lib/ui/components/brand_bottom_sheet/brand_bottom_sheet.dart index d5c6afbe..de322b05 100644 --- a/lib/ui/components/brand_bottom_sheet/brand_bottom_sheet.dart +++ b/lib/ui/components/brand_bottom_sheet/brand_bottom_sheet.dart @@ -1,5 +1,3 @@ -// ignore_for_file: always_specify_types - import 'package:flutter/material.dart'; import 'package:selfprivacy/config/brand_colors.dart'; diff --git a/lib/ui/components/brand_button/brand_button.dart b/lib/ui/components/brand_button/brand_button.dart index 55d7fddc..8951b70f 100644 --- a/lib/ui/components/brand_button/brand_button.dart +++ b/lib/ui/components/brand_button/brand_button.dart @@ -1,5 +1,3 @@ -// ignore_for_file: always_specify_types - import 'package:flutter/material.dart'; import 'package:selfprivacy/ui/components/brand_button/filled_button.dart'; import 'package:selfprivacy/ui/components/brand_text/brand_text.dart'; @@ -42,13 +40,13 @@ class BrandButton { child: TextButton(onPressed: onPressed, child: Text(title)), ); - static _IconTextButton emptyWithIconText({ + static IconTextButton emptyWithIconText({ required final VoidCallback onPressed, required final String title, required final Icon icon, final Key? key, }) => - _IconTextButton( + IconTextButton( key: key, title: title, onPressed: onPressed, @@ -56,8 +54,13 @@ class BrandButton { ); } -class _IconTextButton extends StatelessWidget { - const _IconTextButton({final super.key, this.onPressed, this.title, this.icon}); +class IconTextButton extends StatelessWidget { + const IconTextButton({ + final super.key, + this.onPressed, + this.title, + this.icon, + }); final VoidCallback? onPressed; final String? title; @@ -65,24 +68,24 @@ class _IconTextButton extends StatelessWidget { @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, - ) - ], + 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/brand_cards/brand_cards.dart b/lib/ui/components/brand_cards/brand_cards.dart index 138a674a..d8f48088 100644 --- a/lib/ui/components/brand_cards/brand_cards.dart +++ b/lib/ui/components/brand_cards/brand_cards.dart @@ -1,5 +1,3 @@ -// ignore_for_file: always_specify_types - import 'package:flutter/material.dart'; class BrandCards { @@ -24,8 +22,10 @@ class BrandCards { static Widget outlined({required final Widget child}) => _OutlinedCard( child: child, ); - static Widget filled( - {required final Widget child, final bool tertiary = false,}) => + static Widget filled({ + required final Widget child, + final bool tertiary = false, + }) => _FilledCard( tertiary: tertiary, child: child, @@ -38,7 +38,6 @@ class _BrandCard extends StatelessWidget { required this.padding, required this.shadow, required this.borderRadius, - final super.key, }); final Widget child; @@ -60,7 +59,6 @@ class _BrandCard extends StatelessWidget { class _OutlinedCard extends StatelessWidget { const _OutlinedCard({ - final super.key, required this.child, }); @@ -83,7 +81,6 @@ class _FilledCard extends StatelessWidget { const _FilledCard({ required this.child, required this.tertiary, - final super.key, }); final Widget child; diff --git a/lib/ui/components/brand_divider/brand_divider.dart b/lib/ui/components/brand_divider/brand_divider.dart index c3a3b5a3..03e44653 100644 --- a/lib/ui/components/brand_divider/brand_divider.dart +++ b/lib/ui/components/brand_divider/brand_divider.dart @@ -2,12 +2,12 @@ import 'package:flutter/material.dart'; import 'package:selfprivacy/config/brand_colors.dart'; class BrandDivider extends StatelessWidget { - const BrandDivider({super.key}); + const BrandDivider({final super.key}); @override Widget build(final BuildContext context) => Container( - width: double.infinity, - height: 1, - color: BrandColors.dividerColor, - ); + width: double.infinity, + height: 1, + color: BrandColors.dividerColor, + ); } diff --git a/lib/ui/components/brand_header/brand_header.dart b/lib/ui/components/brand_header/brand_header.dart index 7e0bd3e8..7366298b 100644 --- a/lib/ui/components/brand_header/brand_header.dart +++ b/lib/ui/components/brand_header/brand_header.dart @@ -1,5 +1,3 @@ -// ignore_for_file: always_specify_types - import 'package:flutter/material.dart'; import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart'; import 'package:selfprivacy/ui/components/brand_text/brand_text.dart'; @@ -18,24 +16,24 @@ class BrandHeader extends StatelessWidget { @override Widget build(final BuildContext context) => Container( - height: 52, - alignment: Alignment.centerLeft, - padding: EdgeInsets.only( - left: hasBackButton ? 1 : 15, - ), - child: Row( - children: [ - if (hasBackButton) ...[ - IconButton( - icon: const Icon(BrandIcons.arrowLeft), - onPressed: - onBackButtonPressed ?? () => Navigator.of(context).pop(), - ), - const SizedBox(width: 10), + height: 52, + alignment: Alignment.centerLeft, + padding: EdgeInsets.only( + left: hasBackButton ? 1 : 15, + ), + child: Row( + children: [ + if (hasBackButton) ...[ + IconButton( + icon: const Icon(BrandIcons.arrowLeft), + onPressed: + onBackButtonPressed ?? () => Navigator.of(context).pop(), + ), + const SizedBox(width: 10), + ], + BrandText.h4(title), + const Spacer(), ], - BrandText.h4(title), - const Spacer(), - ], - ), - ); + ), + ); } diff --git a/lib/ui/components/brand_hero_screen/brand_hero_screen.dart b/lib/ui/components/brand_hero_screen/brand_hero_screen.dart index 5061ec63..cdccc8d2 100644 --- a/lib/ui/components/brand_hero_screen/brand_hero_screen.dart +++ b/lib/ui/components/brand_hero_screen/brand_hero_screen.dart @@ -25,48 +25,50 @@ class BrandHeroScreen extends StatelessWidget { final VoidCallback? onBackButtonPressed; @override - Widget build(BuildContext context) => SafeArea( - child: Scaffold( - appBar: PreferredSize( - preferredSize: const Size.fromHeight(52.0), - child: BrandHeader( - title: headerTitle, - hasBackButton: hasBackButton, - onBackButtonPressed: onBackButtonPressed, + Widget build(final BuildContext context) => SafeArea( + child: Scaffold( + appBar: PreferredSize( + preferredSize: const Size.fromHeight(52.0), + child: BrandHeader( + title: headerTitle, + hasBackButton: hasBackButton, + onBackButtonPressed: onBackButtonPressed, + ), ), - ), - floatingActionButton: hasFlashButton ? const BrandFab() : null, - body: ListView( - padding: const EdgeInsets.all(16.0), - children: [ - if (heroIcon != null) - Container( - alignment: Alignment.bottomLeft, - child: Icon( - heroIcon, - size: 48.0, + floatingActionButton: hasFlashButton ? const BrandFab() : null, + body: ListView( + padding: const EdgeInsets.all(16.0), + children: [ + if (heroIcon != null) + Container( + alignment: Alignment.bottomLeft, + child: Icon( + heroIcon, + size: 48.0, + ), ), - ), - const SizedBox(height: 8.0), - if (heroTitle != null) - Text( - heroTitle!, - style: Theme.of(context).textTheme.headlineMedium!.copyWith( - color: Theme.of(context).colorScheme.onBackground, - ), - textAlign: TextAlign.start, - ), - const SizedBox(height: 8.0), - if (heroSubtitle != null) - Text(heroSubtitle!, + const SizedBox(height: 8.0), + if (heroTitle != null) + Text( + heroTitle!, + style: Theme.of(context).textTheme.headlineMedium!.copyWith( + color: Theme.of(context).colorScheme.onBackground, + ), + textAlign: TextAlign.start, + ), + const SizedBox(height: 8.0), + if (heroSubtitle != null) + Text( + heroSubtitle!, style: Theme.of(context).textTheme.bodyMedium!.copyWith( color: Theme.of(context).colorScheme.onBackground, ), - textAlign: TextAlign.start,), - const SizedBox(height: 16.0), - ...children, - ], + textAlign: TextAlign.start, + ), + const SizedBox(height: 16.0), + ...children, + ], + ), ), - ), - ); + ); } diff --git a/lib/ui/components/brand_loader/brand_loader.dart b/lib/ui/components/brand_loader/brand_loader.dart index 5dc6a6ea..59f1f177 100644 --- a/lib/ui/components/brand_loader/brand_loader.dart +++ b/lib/ui/components/brand_loader/brand_loader.dart @@ -2,17 +2,19 @@ import 'package:flutter/material.dart'; import 'package:easy_localization/easy_localization.dart'; class BrandLoader { - static _HorizontalLoader horizontal() => _HorizontalLoader(); + static HorizontalLoader horizontal() => const HorizontalLoader(); } -class _HorizontalLoader extends StatelessWidget { +class HorizontalLoader extends StatelessWidget { + const HorizontalLoader({final super.key}); + @override Widget build(final BuildContext context) => Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text('basis.wait'.tr()), - const SizedBox(height: 10), - const LinearProgressIndicator(minHeight: 3), - ], - ); + mainAxisSize: MainAxisSize.min, + children: [ + Text('basis.wait'.tr()), + const SizedBox(height: 10), + const LinearProgressIndicator(minHeight: 3), + ], + ); } diff --git a/lib/ui/components/brand_radio/brand_radio.dart b/lib/ui/components/brand_radio/brand_radio.dart index aca55505..60f41fb5 100644 --- a/lib/ui/components/brand_radio/brand_radio.dart +++ b/lib/ui/components/brand_radio/brand_radio.dart @@ -3,36 +3,36 @@ import 'package:selfprivacy/config/brand_colors.dart'; class BrandRadio extends StatelessWidget { const BrandRadio({ - super.key, required this.isChecked, + final 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, - ); + 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, - ); + color: isChecked ? BrandColors.primary : BrandColors.gray1, + width: 2, + ); } diff --git a/lib/ui/components/brand_radio_tile/brand_radio_tile.dart b/lib/ui/components/brand_radio_tile/brand_radio_tile.dart index 17b3ccff..5b18247d 100644 --- a/lib/ui/components/brand_radio_tile/brand_radio_tile.dart +++ b/lib/ui/components/brand_radio_tile/brand_radio_tile.dart @@ -4,10 +4,10 @@ import 'package:selfprivacy/ui/components/brand_text/brand_text.dart'; class BrandRadioTile extends StatelessWidget { const BrandRadioTile({ - super.key, required this.isChecked, required this.text, required this.onPress, + final super.key, }); final bool isChecked; @@ -16,20 +16,20 @@ class BrandRadioTile extends StatelessWidget { final VoidCallback onPress; @override - Widget build(BuildContext context) => GestureDetector( - onTap: onPress, - behavior: HitTestBehavior.translucent, - child: Padding( - padding: const EdgeInsets.all(2), - child: Row( - children: [ - BrandRadio( - isChecked: isChecked, - ), - const SizedBox(width: 9), - BrandText.h5(text) - ], + Widget build(final BuildContext context) => GestureDetector( + onTap: onPress, + behavior: HitTestBehavior.translucent, + child: Padding( + padding: const EdgeInsets.all(2), + child: Row( + children: [ + BrandRadio( + isChecked: isChecked, + ), + const SizedBox(width: 9), + BrandText.h5(text) + ], + ), ), - ), - ); + ); } diff --git a/lib/ui/components/brand_span_button/brand_span_button.dart b/lib/ui/components/brand_span_button/brand_span_button.dart index da36ee02..de19730e 100644 --- a/lib/ui/components/brand_span_button/brand_span_button.dart +++ b/lib/ui/components/brand_span_button/brand_span_button.dart @@ -14,18 +14,18 @@ class BrandSpanButton extends TextSpan { style: (style ?? const TextStyle()).copyWith(color: BrandColors.blue), ); - static BrandSpanButton link({ + BrandSpanButton.link({ required final String text, final String? urlString, final TextStyle? style, - }) => - BrandSpanButton( - text: text, - style: style, - onTap: () => _launchURL(urlString ?? text), - ); + }) : super( + recognizer: TapGestureRecognizer() + ..onTap = () => _launchURL(urlString ?? text), + text: text, + style: (style ?? const TextStyle()).copyWith(color: BrandColors.blue), + ); - static _launchURL(final String link) async { + static Future _launchURL(final String link) async { if (await canLaunchUrl(Uri.parse(link))) { await launchUrl(Uri.parse(link)); } else { diff --git a/lib/ui/components/brand_switch/brand_switch.dart b/lib/ui/components/brand_switch/brand_switch.dart index deb595a4..89396acc 100644 --- a/lib/ui/components/brand_switch/brand_switch.dart +++ b/lib/ui/components/brand_switch/brand_switch.dart @@ -11,9 +11,9 @@ class BrandSwitch extends StatelessWidget { final bool value; @override - Widget build(BuildContext context) => Switch( - activeColor: Theme.of(context).colorScheme.primary, - value: value, - onChanged: onChanged, - ); + 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 index e1a03062..194c0ac1 100644 --- a/lib/ui/components/brand_tab_bar/brand_tab_bar.dart +++ b/lib/ui/components/brand_tab_bar/brand_tab_bar.dart @@ -19,7 +19,7 @@ class _BrandTabBarState extends State { super.initState(); } - _listener() { + void _listener() { if (currentIndex != widget.controller!.index) { setState(() { currentIndex = widget.controller!.index; @@ -35,21 +35,26 @@ class _BrandTabBarState extends State { @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, - ); + 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, - ); + 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 index b64ed1c4..544ffcec 100644 --- a/lib/ui/components/brand_text/brand_text.dart +++ b/lib/ui/components/brand_text/brand_text.dart @@ -33,13 +33,20 @@ class BrandText extends StatelessWidget { textAlign: textAlign, ); - factory BrandText.onboardingTitle(final String text, {final TextStyle? style}) => + 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}) => + factory BrandText.h3( + final String text, { + final TextStyle? style, + final TextAlign? textAlign, + }) => BrandText( text, type: TextType.h3, @@ -85,22 +92,28 @@ class BrandText extends StatelessWidget { style: style, textAlign: textAlign, ); - factory BrandText.body1(final String? text, {final TextStyle? style}) => BrandText( + 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( + 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( + 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}) => + factory BrandText.buttonTitleText( + final String? text, { + final TextStyle? style, + }) => BrandText( text, type: TextType.buttonTitleText, @@ -118,8 +131,11 @@ class BrandText extends StatelessWidget { style: style, textAlign: textAlign, ); - factory BrandText.medium(final String? text, - {final TextStyle? style, final TextAlign? textAlign}) => + factory BrandText.medium( + final String? text, { + final TextStyle? style, + final TextAlign? textAlign, + }) => BrandText( text, type: TextType.medium, @@ -128,9 +144,9 @@ class BrandText extends StatelessWidget { ); const BrandText( this.text, { - super.key, - this.style, required this.type, + final super.key, + this.style, this.overflow, this.softWrap, this.textAlign, diff --git a/lib/ui/components/brand_timer/brand_timer.dart b/lib/ui/components/brand_timer/brand_timer.dart index b2ed1406..5d76d57d 100644 --- a/lib/ui/components/brand_timer/brand_timer.dart +++ b/lib/ui/components/brand_timer/brand_timer.dart @@ -7,9 +7,9 @@ import 'package:selfprivacy/utils/named_font_weight.dart'; class BrandTimer extends StatefulWidget { const BrandTimer({ - super.key, required this.startDateTime, required this.duration, + final super.key, }); final DateTime startDateTime; @@ -29,10 +29,11 @@ class _BrandTimerState extends State { super.initState(); } - _timerStart() { + void _timerStart() { _timeString = differenceFromStart; timer = Timer.periodic(const Duration(seconds: 1), (final Timer t) { - final Duration timePassed = DateTime.now().difference(widget.startDateTime); + final Duration timePassed = + DateTime.now().difference(widget.startDateTime); if (timePassed > widget.duration) { t.cancel(); } else { @@ -52,11 +53,11 @@ class _BrandTimerState extends State { @override Widget build(final BuildContext context) => BrandText.medium( - _timeString, - style: const TextStyle( - fontWeight: NamedFontWeight.demiBold, - ), - ); + _timeString, + style: const TextStyle( + fontWeight: NamedFontWeight.demiBold, + ), + ); void _getTime() { setState(() { diff --git a/lib/ui/components/icon_status_mask/icon_status_mask.dart b/lib/ui/components/icon_status_mask/icon_status_mask.dart index b781c3d1..0c507ede 100644 --- a/lib/ui/components/icon_status_mask/icon_status_mask.dart +++ b/lib/ui/components/icon_status_mask/icon_status_mask.dart @@ -4,9 +4,9 @@ import 'package:selfprivacy/logic/models/state_types.dart'; class IconStatusMask extends StatelessWidget { const IconStatusMask({ - super.key, required this.child, required this.status, + final super.key, }); final Icon child; diff --git a/lib/ui/components/jobs_content/jobs_content.dart b/lib/ui/components/jobs_content/jobs_content.dart index e15fad3a..bd8166de 100644 --- a/lib/ui/components/jobs_content/jobs_content.dart +++ b/lib/ui/components/jobs_content/jobs_content.dart @@ -13,106 +13,113 @@ import 'package:selfprivacy/ui/components/brand_loader/brand_loader.dart'; import 'package:selfprivacy/ui/components/brand_text/brand_text.dart'; class JobsContent extends StatelessWidget { - const JobsContent({super.key}); + const JobsContent({final super.key}); @override Widget build(final BuildContext context) => BlocBuilder( - builder: (final context, final state) { - late List widgets; - final ServerInstallationState installationState = context.read().state; - if (state is JobsStateEmpty) { - widgets = [ - const SizedBox(height: 80), - Center(child: BrandText.body1('jobs.empty'.tr())), - ]; - - if (installationState is ServerInstallationFinished) { + builder: (final context, final state) { + late List widgets; + final ServerInstallationState installationState = + context.read().state; + if (state is JobsStateEmpty) { widgets = [ - ...widgets, const SizedBox(height: 80), - BrandButton.rised( - onPressed: () => context.read().upgradeServer(), - text: 'jobs.upgradeServer'.tr(), - ), - const SizedBox(height: 10), - BrandButton.text( - onPressed: () { - final NavigationService nav = getIt(); - nav.showPopUpDialog(BrandAlert( - title: 'jobs.rebootServer'.tr(), - contentText: 'modals.3'.tr(), - actions: [ - ActionButton( - text: 'basis.cancel'.tr(), + Center(child: BrandText.body1('jobs.empty'.tr())), + ]; + + if (installationState is ServerInstallationFinished) { + widgets = [ + ...widgets, + const SizedBox(height: 80), + BrandButton.rised( + onPressed: () => context.read().upgradeServer(), + text: 'jobs.upgradeServer'.tr(), + ), + const SizedBox(height: 10), + BrandButton.text( + onPressed: () { + final NavigationService nav = getIt(); + nav.showPopUpDialog( + BrandAlert( + title: 'jobs.rebootServer'.tr(), + contentText: 'modals.3'.tr(), + actions: [ + ActionButton( + text: 'basis.cancel'.tr(), + ), + ActionButton( + onPressed: () => + {context.read().rebootServer()}, + text: 'modals.9'.tr(), + ) + ], ), - ActionButton( - onPressed: () => - {context.read().rebootServer()}, - text: 'modals.9'.tr(), - ) - ], - ),); - }, - title: 'jobs.rebootServer'.tr(), + ); + }, + title: 'jobs.rebootServer'.tr(), + ), + ]; + } + } else if (state is JobsStateLoading) { + widgets = [ + const SizedBox(height: 80), + BrandLoader.horizontal(), + ]; + } else if (state is JobsStateWithJobs) { + widgets = [ + ...state.jobList + .map( + (final j) => Row( + children: [ + Expanded( + child: BrandCards.small( + child: Text(j.title), + ), + ), + const SizedBox(width: 10), + ElevatedButton( + style: ElevatedButton.styleFrom( + primary: + 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, + ), + ), + ), + ], + ), + ) + .toList(), + const SizedBox(height: 20), + BrandButton.rised( + onPressed: () => context.read().applyAll(), + text: 'jobs.start'.tr(), ), ]; } - } else if (state is JobsStateLoading) { - widgets = [ - const SizedBox(height: 80), - BrandLoader.horizontal(), - ]; - } else if (state is JobsStateWithJobs) { - widgets = [ - ...state.jobList - .map( - (final j) => Row( - children: [ - Expanded( - child: BrandCards.small( - child: Text(j.title), - ), - ), - const SizedBox(width: 10), - ElevatedButton( - style: ElevatedButton.styleFrom( - primary: 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,),), - ), - ], - ), - ) - .toList(), - const SizedBox(height: 20), - BrandButton.rised( - onPressed: () => context.read().applyAll(), - text: 'jobs.start'.tr(), - ), - ]; - } - return ListView( - padding: paddingH15V0, - children: [ - const SizedBox(height: 15), - Center( - child: BrandText.h2( - 'jobs.title'.tr(), + return ListView( + padding: paddingH15V0, + children: [ + const SizedBox(height: 15), + Center( + child: BrandText.h2( + 'jobs.title'.tr(), + ), ), - ), - const SizedBox(height: 20), - ...widgets - ], - ); - }, - ); + const SizedBox(height: 20), + ...widgets + ], + ); + }, + ); } 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 dd91f4ab..49947c1b 100644 --- a/lib/ui/components/not_ready_card/not_ready_card.dart +++ b/lib/ui/components/not_ready_card/not_ready_card.dart @@ -6,47 +6,49 @@ import 'package:selfprivacy/utils/route_transitions/basic.dart'; import 'package:easy_localization/easy_localization.dart'; class NotReadyCard extends StatelessWidget { - const NotReadyCard({super.key}); + const NotReadyCard({final super.key}); @override Widget build(final BuildContext context) => Container( - padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 10), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(15), color: BrandColors.gray6,), - child: RichText( - text: TextSpan( - children: [ - TextSpan( - text: 'not_ready_card.1'.tr(), - style: const TextStyle(color: BrandColors.white), - ), - WidgetSpan( - child: Padding( - padding: const EdgeInsets.only(bottom: 0.5), - child: GestureDetector( - onTap: () => Navigator.of(context).push( - materialRoute( - const InitializingPage(), + padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 10), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + color: BrandColors.gray6, + ), + child: RichText( + text: TextSpan( + children: [ + TextSpan( + text: 'not_ready_card.1'.tr(), + style: const TextStyle(color: BrandColors.white), + ), + WidgetSpan( + child: Padding( + padding: const EdgeInsets.only(bottom: 0.5), + child: GestureDetector( + onTap: () => Navigator.of(context).push( + materialRoute( + const InitializingPage(), + ), ), - ), - child: Text( - 'not_ready_card.2'.tr(), - style: body1Style.copyWith( - color: Colors.white, - fontWeight: FontWeight.bold, - decoration: TextDecoration.underline, - // height: 1.1, + child: Text( + 'not_ready_card.2'.tr(), + style: body1Style.copyWith( + color: Colors.white, + fontWeight: FontWeight.bold, + decoration: TextDecoration.underline, + // height: 1.1, + ), ), ), ), ), - ), - TextSpan( - text: 'not_ready_card.3'.tr(), - style: const TextStyle(color: BrandColors.white), - ), - ], + TextSpan( + text: 'not_ready_card.3'.tr(), + style: const TextStyle(color: BrandColors.white), + ), + ], + ), ), - ), - ); + ); } diff --git a/lib/ui/components/one_page/one_page.dart b/lib/ui/components/one_page/one_page.dart index 66792716..d16dd5f3 100644 --- a/lib/ui/components/one_page/one_page.dart +++ b/lib/ui/components/one_page/one_page.dart @@ -6,9 +6,9 @@ import 'package:selfprivacy/ui/components/pre_styled_buttons/pre_styled_buttons. class OnePage extends StatelessWidget { const OnePage({ - super.key, required this.title, required this.child, + final super.key, }); final String title; @@ -16,32 +16,33 @@ class OnePage extends StatelessWidget { @override Widget build(final BuildContext context) => Scaffold( - appBar: PreferredSize( - preferredSize: const Size.fromHeight(52), - child: Column( - children: [ - Container( - height: 51, - alignment: Alignment.center, - padding: const EdgeInsets.symmetric(horizontal: 15), - child: BrandText.h4('basis.details'.tr()), - ), - const BrandDivider(), - ], - ), - ), - body: child, - bottomNavigationBar: SafeArea( - child: Container( - decoration: BoxDecoration(boxShadow: kElevationToShadow[3]), - height: kBottomNavigationBarHeight, - child: Container( - color: Theme.of(context).scaffoldBackgroundColor, - alignment: Alignment.center, - child: PreStyledButtons.close( - onPress: () => Navigator.of(context).pop(),), + appBar: PreferredSize( + preferredSize: const Size.fromHeight(52), + child: Column( + children: [ + Container( + height: 51, + alignment: Alignment.center, + padding: const EdgeInsets.symmetric(horizontal: 15), + child: BrandText.h4('basis.details'.tr()), + ), + const BrandDivider(), + ], ), ), - ), - ); + body: child, + bottomNavigationBar: SafeArea( + child: Container( + decoration: BoxDecoration(boxShadow: kElevationToShadow[3]), + height: kBottomNavigationBarHeight, + child: Container( + color: Theme.of(context).scaffoldBackgroundColor, + alignment: Alignment.center, + child: PreStyledButtons.close( + onPress: () => Navigator.of(context).pop(), + ), + ), + ), + ), + ); } diff --git a/lib/ui/components/pre_styled_buttons/close.dart b/lib/ui/components/pre_styled_buttons/close.dart index 3beb2eb4..48a1bddb 100644 --- a/lib/ui/components/pre_styled_buttons/close.dart +++ b/lib/ui/components/pre_styled_buttons/close.dart @@ -1,19 +1,19 @@ part of 'pre_styled_buttons.dart'; class _CloseButton extends StatelessWidget { - const _CloseButton({super.key, required this.onPress}); + const _CloseButton({required this.onPress}); final VoidCallback onPress; @override Widget build(final BuildContext context) => OutlinedButton( - onPressed: () => Navigator.of(context).pop(), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - BrandText.h4('basis.close'.tr()), - const Icon(Icons.close), - ], - ), - ); + onPressed: () => Navigator.of(context).pop(), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + BrandText.h4('basis.close'.tr()), + const Icon(Icons.close), + ], + ), + ); } diff --git a/lib/ui/components/pre_styled_buttons/flash.dart b/lib/ui/components/pre_styled_buttons/flash.dart index 5d70013d..3e780fd7 100644 --- a/lib/ui/components/pre_styled_buttons/flash.dart +++ b/lib/ui/components/pre_styled_buttons/flash.dart @@ -1,8 +1,6 @@ part of 'pre_styled_buttons.dart'; class _BrandFlashButton extends StatefulWidget { - const _BrandFlashButton({super.key}); - @override _BrandFlashButtonState createState() => _BrandFlashButtonState(); } @@ -15,7 +13,9 @@ class _BrandFlashButtonState extends State<_BrandFlashButton> @override void initState() { _animationController = AnimationController( - vsync: this, duration: const Duration(milliseconds: 800),); + vsync: this, + duration: const Duration(milliseconds: 800), + ); _colorTween = ColorTween( begin: BrandColors.black, end: BrandColors.primary, @@ -45,32 +45,34 @@ class _BrandFlashButtonState extends State<_BrandFlashButton> bool wasPrevStateIsEmpty = true; @override - Widget build(final BuildContext context) => BlocListener( - listener: (final context, final state) { - if (wasPrevStateIsEmpty && state is! JobsStateEmpty) { - wasPrevStateIsEmpty = false; - _animationController.forward(); - } else if (!wasPrevStateIsEmpty && state is JobsStateEmpty) { - wasPrevStateIsEmpty = true; + Widget build(final BuildContext context) => + BlocListener( + listener: (final context, final state) { + if (wasPrevStateIsEmpty && state is! JobsStateEmpty) { + wasPrevStateIsEmpty = false; + _animationController.forward(); + } else if (!wasPrevStateIsEmpty && state is JobsStateEmpty) { + wasPrevStateIsEmpty = true; - _animationController.reverse(); - } - }, - child: IconButton( - onPressed: () { - showBrandBottomSheet( - context: context, - builder: (final context) => const BrandBottomSheet( - isExpended: true, - child: JobsContent(), - ), - ); + _animationController.reverse(); + } }, - icon: AnimatedBuilder( + child: IconButton( + onPressed: () { + showBrandBottomSheet( + context: context, + builder: (final context) => const BrandBottomSheet( + isExpended: true, + child: JobsContent(), + ), + ); + }, + icon: AnimatedBuilder( animation: _colorTween, builder: (final context, final child) { final double v = _animationController.value; - final IconData icon = v > 0.5 ? Ionicons.flash : Ionicons.flash_outline; + 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( @@ -78,7 +80,8 @@ class _BrandFlashButtonState extends State<_BrandFlashButton> color: _colorTween.value, ), ); - },), - ), - ); + }, + ), + ), + ); } diff --git a/lib/ui/components/pre_styled_buttons/flash_fab.dart b/lib/ui/components/pre_styled_buttons/flash_fab.dart index 89f42e19..4ae29087 100644 --- a/lib/ui/components/pre_styled_buttons/flash_fab.dart +++ b/lib/ui/components/pre_styled_buttons/flash_fab.dart @@ -7,7 +7,7 @@ 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({final super.key}); @override State createState() => _BrandFabState(); @@ -21,7 +21,9 @@ class _BrandFabState extends State @override void initState() { _animationController = AnimationController( - vsync: this, duration: const Duration(milliseconds: 800),); + vsync: this, + duration: const Duration(milliseconds: 800), + ); super.initState(); } @@ -62,18 +64,20 @@ class _BrandFabState extends State ); }, 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, - ), - ); - },), + 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, + ), + ); + }, + ), ), ); } diff --git a/lib/ui/components/pre_styled_buttons/pre_styled_buttons.dart b/lib/ui/components/pre_styled_buttons/pre_styled_buttons.dart index cb50e2f9..ad9105fb 100644 --- a/lib/ui/components/pre_styled_buttons/pre_styled_buttons.dart +++ b/lib/ui/components/pre_styled_buttons/pre_styled_buttons.dart @@ -18,5 +18,5 @@ class PreStyledButtons { }) => _CloseButton(onPress: onPress); - static Widget flash() => const _BrandFlashButton(); + static Widget flash() => _BrandFlashButton(); } diff --git a/lib/ui/components/progress_bar/progress_bar.dart b/lib/ui/components/progress_bar/progress_bar.dart index 6e8d80c3..4de729f7 100644 --- a/lib/ui/components/progress_bar/progress_bar.dart +++ b/lib/ui/components/progress_bar/progress_bar.dart @@ -7,9 +7,9 @@ import 'package:selfprivacy/ui/components/brand_text/brand_text.dart'; class ProgressBar extends StatefulWidget { const ProgressBar({ - super.key, required this.steps, required this.activeIndex, + final super.key, }); final int activeIndex; @@ -23,9 +23,11 @@ class ProgressBar extends StatefulWidget { class _ProgressBarState extends State { @override Widget build(final BuildContext context) { - final double progress = 1 / widget.steps.length * (widget.activeIndex + 0.3); + final double progress = + 1 / widget.steps.length * (widget.activeIndex + 0.3); final bool isDark = context.watch().state.isDarkModeOn; - final TextStyle style = isDark ? progressTextStyleDark : progressTextStyleLight; + final TextStyle style = + isDark ? progressTextStyleDark : progressTextStyleLight; final Iterable allSteps = widget.steps.asMap().map( (final i, final step) { @@ -77,20 +79,20 @@ class _ProgressBarState extends State { ), child: LayoutBuilder( builder: (final _, final constraints) => AnimatedContainer( - width: constraints.maxWidth * progress, - height: 5, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(5), - gradient: const LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: BrandColors.stableGradientColors, - ), - ), - duration: const Duration( - milliseconds: 300, + width: constraints.maxWidth * progress, + height: 5, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(5), + gradient: const LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: BrandColors.stableGradientColors, ), ), + duration: const Duration( + milliseconds: 300, + ), + ), ), ), const SizedBox(height: 5), @@ -120,11 +122,15 @@ class _ProgressBarState extends State { text: TextSpan( style: progressTextStyleLight, children: [ - if (checked) const WidgetSpan( - child: Padding( - padding: EdgeInsets.only(bottom: 2, right: 2), - child: Icon(BrandIcons.check, size: 11), - ),) else TextSpan(text: '${index + 1}.', style: style), + if (checked) + const WidgetSpan( + child: Padding( + padding: EdgeInsets.only(bottom: 2, right: 2), + child: Icon(BrandIcons.check, size: 11), + ), + ) + else + TextSpan(text: '${index + 1}.', style: style), TextSpan(text: step, style: style) ], ), diff --git a/lib/ui/components/switch_block/switch_bloc.dart b/lib/ui/components/switch_block/switch_bloc.dart index 3aa89a33..ae593f1e 100644 --- a/lib/ui/components/switch_block/switch_bloc.dart +++ b/lib/ui/components/switch_block/switch_bloc.dart @@ -3,10 +3,10 @@ import 'package:selfprivacy/config/brand_colors.dart'; class SwitcherBlock extends StatelessWidget { const SwitcherBlock({ - super.key, required this.child, required this.isActive, required this.onChange, + final super.key, }); final Widget child; @@ -15,24 +15,25 @@ class SwitcherBlock extends StatelessWidget { @override Widget build(final BuildContext context) => Container( - padding: const EdgeInsets.only(top: 20, bottom: 5), - decoration: const BoxDecoration( + padding: const EdgeInsets.only(top: 20, bottom: 5), + decoration: const BoxDecoration( border: Border( - bottom: BorderSide(width: 1, color: BrandColors.dividerColor), - ),), - 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, + bottom: BorderSide(width: 1, color: BrandColors.dividerColor), ), - ], - ), - ); + ), + 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/pages/backup_details/backup_details.dart b/lib/ui/pages/backup_details/backup_details.dart index f7784a18..55a8dd12 100644 --- a/lib/ui/pages/backup_details/backup_details.dart +++ b/lib/ui/pages/backup_details/backup_details.dart @@ -18,7 +18,7 @@ import 'package:selfprivacy/ui/components/brand_cards/brand_cards.dart'; GlobalKey navigatorKey = GlobalKey(); class BackupDetails extends StatefulWidget { - const BackupDetails({super.key}); + const BackupDetails({final super.key}); @override State createState() => _BackupDetailsState(); @@ -30,14 +30,17 @@ class _BackupDetailsState extends State Widget build(final BuildContext context) { final bool isReady = context.watch().state is ServerInstallationFinished; - final bool isBackupInitialized = context.watch().state.isInitialized; - final BackupStatusEnum backupStatus = context.watch().state.status; + final bool isBackupInitialized = + context.watch().state.isInitialized; + final BackupStatusEnum backupStatus = + context.watch().state.status; final StateType providerState = isReady && isBackupInitialized ? (backupStatus == BackupStatusEnum.error ? StateType.warning : StateType.stable) : StateType.uninitialized; - final bool preventActions = context.watch().state.preventActions; + final bool preventActions = + context.watch().state.preventActions; final double backupProgress = context.watch().state.progress; final String backupError = context.watch().state.error; final List backups = context.watch().state.backups; @@ -84,7 +87,8 @@ class _BackupDetailsState extends State ListTile( title: Text( 'providers.backup.creating'.tr( - args: [(backupProgress * 100).round().toString()],), + args: [(backupProgress * 100).round().toString()], + ), style: Theme.of(context).textTheme.headline6, ), subtitle: LinearProgressIndicator( @@ -96,7 +100,8 @@ class _BackupDetailsState extends State ListTile( title: Text( 'providers.backup.restoring'.tr( - args: [(backupProgress * 100).round().toString()],), + args: [(backupProgress * 100).round().toString()], + ), style: Theme.of(context).textTheme.headline6, ), subtitle: LinearProgressIndicator( @@ -148,34 +153,44 @@ class _BackupDetailsState extends State ), if (backups.isNotEmpty) Column( - children: backups.map((final Backup backup) => ListTile( - onTap: preventActions - ? null - : () { - final NavigationService nav = getIt(); - nav.showPopUpDialog(BrandAlert( - title: 'providers.backup.restoring'.tr(), - contentText: 'providers.backup.restore_alert' - .tr(args: [backup.time.toString()]), - actions: [ - ActionButton( - text: 'basis.cancel'.tr(), - ), - ActionButton( - onPressed: () => { - context - .read() - .restoreBackup(backup.id) - }, - text: 'modals.yes'.tr(), - ) - ], - ),); - }, - title: Text( - '${MaterialLocalizations.of(context).formatShortDate(backup.time)} ${TimeOfDay.fromDateTime(backup.time).format(context)}', - ), - ),).toList(), + children: backups + .map( + (final Backup backup) => ListTile( + onTap: preventActions + ? null + : () { + final NavigationService nav = + getIt(); + nav.showPopUpDialog( + BrandAlert( + title: + 'providers.backup.restoring'.tr(), + contentText: + 'providers.backup.restore_alert'.tr( + args: [backup.time.toString()], + ), + actions: [ + ActionButton( + text: 'basis.cancel'.tr(), + ), + ActionButton( + onPressed: () => { + context + .read() + .restoreBackup(backup.id) + }, + text: 'modals.yes'.tr(), + ) + ], + ), + ); + }, + title: Text( + '${MaterialLocalizations.of(context).formatShortDate(backup.time)} ${TimeOfDay.fromDateTime(backup.time).format(context)}', + ), + ), + ) + .toList(), ), ], ), diff --git a/lib/ui/pages/devices/devices.dart b/lib/ui/pages/devices/devices.dart index e14f5263..5883064c 100644 --- a/lib/ui/pages/devices/devices.dart +++ b/lib/ui/pages/devices/devices.dart @@ -9,7 +9,7 @@ import 'package:selfprivacy/ui/pages/devices/new_device.dart'; import 'package:selfprivacy/utils/route_transitions/basic.dart'; class DevicesScreen extends StatefulWidget { - const DevicesScreen({super.key}); + const DevicesScreen({final super.key}); @override State createState() => _DevicesScreenState(); @@ -18,7 +18,8 @@ class DevicesScreen extends StatefulWidget { class _DevicesScreenState extends State { @override Widget build(final BuildContext context) { - final ApiDevicesState devicesStatus = context.watch().state; + final ApiDevicesState devicesStatus = + context.watch().state; return RefreshIndicator( onRefresh: () async { @@ -79,61 +80,68 @@ class _DevicesScreenState extends State { } class _DeviceTile extends StatelessWidget { - const _DeviceTile({super.key, required this.device}); + const _DeviceTile({required this.device}); final ApiToken device; @override Widget build(final BuildContext context) => ListTile( - contentPadding: const EdgeInsets.symmetric(horizontal: 0, vertical: 4), - title: Text(device.name), - subtitle: Text('devices.main_screen.access_granted_on' - .tr(args: [DateFormat.yMMMMd().format(device.date)]),), - onTap: device.isCaller - ? null - : () => _showConfirmationDialog(context, device), - ); + contentPadding: const EdgeInsets.symmetric(horizontal: 0, vertical: 4), + title: Text(device.name), + subtitle: Text( + 'devices.main_screen.access_granted_on' + .tr(args: [DateFormat.yMMMMd().format(device.date)]), + ), + onTap: device.isCaller + ? null + : () => _showConfirmationDialog(context, device), + ); - Future _showConfirmationDialog(final BuildContext context, final ApiToken device) => showDialog( + Future _showConfirmationDialog( + final BuildContext context, + final ApiToken device, + ) => + showDialog( context: context, builder: (final context) => AlertDialog( - title: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Icon(Icons.link_off_outlined), - const SizedBox(height: 16), - Text( - 'devices.revoke_device_alert.header'.tr(), - style: Theme.of(context).textTheme.headlineSmall, - textAlign: TextAlign.center, - ), - ], - ), - content: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'devices.revoke_device_alert.description' - .tr(args: [device.name]), - style: Theme.of(context).textTheme.bodyMedium,), - ], - ), - actions: [ - TextButton( - child: Text('devices.revoke_device_alert.no'.tr()), - onPressed: () { - Navigator.of(context).pop(); - }, - ), - TextButton( - child: Text('devices.revoke_device_alert.yes'.tr()), - onPressed: () { - context.read().deleteDevice(device); - Navigator.of(context).pop(); - }, + title: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Icon(Icons.link_off_outlined), + const SizedBox(height: 16), + Text( + 'devices.revoke_device_alert.header'.tr(), + style: Theme.of(context).textTheme.headlineSmall, + textAlign: TextAlign.center, ), ], ), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'devices.revoke_device_alert.description' + .tr(args: [device.name]), + style: Theme.of(context).textTheme.bodyMedium, + ), + ], + ), + actions: [ + TextButton( + child: Text('devices.revoke_device_alert.no'.tr()), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + TextButton( + child: Text('devices.revoke_device_alert.yes'.tr()), + onPressed: () { + context.read().deleteDevice(device); + Navigator.of(context).pop(); + }, + ), + ], + ), ); } diff --git a/lib/ui/pages/devices/new_device.dart b/lib/ui/pages/devices/new_device.dart index 4a152380..56f3d47f 100644 --- a/lib/ui/pages/devices/new_device.dart +++ b/lib/ui/pages/devices/new_device.dart @@ -7,74 +7,77 @@ import 'package:selfprivacy/ui/components/brand_button/filled_button.dart'; import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; class NewDeviceScreen extends StatelessWidget { - const NewDeviceScreen({super.key}); + const NewDeviceScreen({final super.key}); @override Widget build(final BuildContext context) => BrandHeroScreen( - heroTitle: 'devices.add_new_device_screen.header'.tr(), - heroSubtitle: 'devices.add_new_device_screen.description'.tr(), - hasBackButton: true, - hasFlashButton: false, - children: [ - FutureBuilder( - future: context.read().getNewDeviceKey(), - builder: (final BuildContext context, final AsyncSnapshot snapshot) { - if (snapshot.hasData) { - return _KeyDisplay( - newDeviceKey: snapshot.data.toString(), - ); - } else { - return const Center(child: CircularProgressIndicator()); - } - }, - ), - ], - ); + heroTitle: 'devices.add_new_device_screen.header'.tr(), + heroSubtitle: 'devices.add_new_device_screen.description'.tr(), + hasBackButton: true, + hasFlashButton: false, + children: [ + FutureBuilder( + future: context.read().getNewDeviceKey(), + builder: ( + final BuildContext context, + final AsyncSnapshot snapshot, + ) { + if (snapshot.hasData) { + return _KeyDisplay( + newDeviceKey: snapshot.data.toString(), + ); + } else { + return const Center(child: CircularProgressIndicator()); + } + }, + ), + ], + ); } class _KeyDisplay extends StatelessWidget { - const _KeyDisplay({super.key, required this.newDeviceKey}); + const _KeyDisplay({required this.newDeviceKey}); final String newDeviceKey; @override Widget build(final BuildContext context) => Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Divider(), - const SizedBox(height: 16), - Text( - newDeviceKey, - style: Theme.of(context).textTheme.bodyLarge!.copyWith( - fontSize: 24, - fontFamily: 'RobotoMono', - ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 16), - const Divider(), - const SizedBox(height: 16), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Icon( - Icons.info_outline, - color: Theme.of(context).colorScheme.onBackground, - ), - const SizedBox(height: 16), - Text( - 'devices.add_new_device_screen.tip'.tr(), - style: Theme.of(context).textTheme.bodyMedium!, - ), - ], - ), - const SizedBox(height: 16), - FilledButton( - child: Text( - 'basis.done'.tr(), + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Divider(), + const SizedBox(height: 16), + Text( + newDeviceKey, + style: Theme.of(context).textTheme.bodyLarge!.copyWith( + fontSize: 24, + fontFamily: 'RobotoMono', + ), + textAlign: TextAlign.center, ), - onPressed: () => Navigator.of(context).pop(), - ), - const SizedBox(height: 24), - ], - ); + const SizedBox(height: 16), + const Divider(), + const SizedBox(height: 16), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon( + Icons.info_outline, + color: Theme.of(context).colorScheme.onBackground, + ), + const SizedBox(height: 16), + Text( + 'devices.add_new_device_screen.tip'.tr(), + style: Theme.of(context).textTheme.bodyMedium!, + ), + ], + ), + const SizedBox(height: 16), + FilledButton( + child: Text( + 'basis.done'.tr(), + ), + onPressed: () => Navigator.of(context).pop(), + ), + const SizedBox(height: 24), + ], + ); } diff --git a/lib/ui/pages/dns_details/dns_details.dart b/lib/ui/pages/dns_details/dns_details.dart index 3e3cc67a..44d6db0d 100644 --- a/lib/ui/pages/dns_details/dns_details.dart +++ b/lib/ui/pages/dns_details/dns_details.dart @@ -8,14 +8,17 @@ import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.da import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart'; class DnsDetailsPage extends StatefulWidget { - const DnsDetailsPage({super.key}); + const DnsDetailsPage({final super.key}); @override State createState() => _DnsDetailsPageState(); } class _DnsDetailsPageState extends State { - Widget _getStateCard(final DnsRecordsStatus dnsState, final Function fixCallback) { + Widget _getStateCard( + final DnsRecordsStatus dnsState, + final Function fixCallback, + ) { String description = ''; String subtitle = ''; Icon icon = const Icon( @@ -66,7 +69,8 @@ class _DnsDetailsPageState extends State { Widget build(final BuildContext context) { final bool isReady = context.watch().state is ServerInstallationFinished; - final String domain = getIt().serverDomain?.domainName ?? ''; + final String domain = + getIt().serverDomain?.domainName ?? ''; final DnsRecordsState dnsCubit = context.watch().state; print(dnsCubit.dnsState); diff --git a/lib/ui/pages/more/about/about.dart b/lib/ui/pages/more/about/about.dart index bc1a3819..3d642adc 100644 --- a/lib/ui/pages/more/about/about.dart +++ b/lib/ui/pages/more/about/about.dart @@ -4,19 +4,21 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:selfprivacy/ui/components/brand_md/brand_md.dart'; class AboutPage extends StatelessWidget { - const AboutPage({super.key}); + const AboutPage({final super.key}); @override Widget build(final BuildContext context) => SafeArea( - child: Scaffold( - appBar: PreferredSize( - preferredSize: const Size.fromHeight(52), - child: BrandHeader( - title: 'more.about_project'.tr(), hasBackButton: true,), + child: Scaffold( + appBar: PreferredSize( + preferredSize: const Size.fromHeight(52), + child: BrandHeader( + title: 'more.about_project'.tr(), + hasBackButton: true, + ), + ), + body: const BrandMarkdown( + fileName: 'about', + ), ), - body: const BrandMarkdown( - fileName: 'about', - ), - ), - ); + ); } diff --git a/lib/ui/pages/more/app_settings/app_setting.dart b/lib/ui/pages/more/app_settings/app_setting.dart index 1dc50416..862815c1 100644 --- a/lib/ui/pages/more/app_settings/app_setting.dart +++ b/lib/ui/pages/more/app_settings/app_setting.dart @@ -13,7 +13,7 @@ import 'package:selfprivacy/utils/named_font_weight.dart'; import 'package:easy_localization/easy_localization.dart'; class AppSettingsPage extends StatefulWidget { - const AppSettingsPage({super.key}); + const AppSettingsPage({final super.key}); @override State createState() => _AppSettingsPageState(); @@ -22,14 +22,18 @@ class AppSettingsPage extends StatefulWidget { class _AppSettingsPageState extends State { @override Widget build(final BuildContext context) { - final bool isDarkModeOn = context.watch().state.isDarkModeOn; + final bool isDarkModeOn = + context.watch().state.isDarkModeOn; return SafeArea( - child: Builder(builder: (final context) => Scaffold( + child: Builder( + builder: (final context) => Scaffold( appBar: PreferredSize( preferredSize: const Size.fromHeight(52), child: BrandHeader( - title: 'more.settings.title'.tr(), hasBackButton: true,), + title: 'more.settings.title'.tr(), + hasBackButton: true, + ), ), body: ListView( padding: paddingH15V0, @@ -38,9 +42,11 @@ class _AppSettingsPageState extends State { Container( padding: const EdgeInsets.only(top: 20, bottom: 5), decoration: const BoxDecoration( - border: Border( - bottom: BorderSide(width: 1, color: BrandColors.dividerColor), - ),), + border: Border( + bottom: + BorderSide(width: 1, color: BrandColors.dividerColor), + ), + ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -65,9 +71,11 @@ class _AppSettingsPageState extends State { Container( padding: const EdgeInsets.only(top: 20, bottom: 5), decoration: const BoxDecoration( - border: Border( - bottom: BorderSide(width: 1, color: BrandColors.dividerColor), - ),), + border: Border( + bottom: + BorderSide(width: 1, color: BrandColors.dividerColor), + ), + ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -95,23 +103,24 @@ class _AppSettingsPageState extends State { showDialog( context: context, builder: (final _) => BrandAlert( - title: 'modals.3'.tr(), - contentText: 'modals.4'.tr(), - actions: [ - ActionButton( - text: 'modals.5'.tr(), - isRed: true, - onPressed: () { - context - .read() - .clearAppConfig(); - Navigator.of(context).pop(); - },), - ActionButton( - text: 'basis.cancel'.tr(), - ), - ], - ), + title: 'modals.3'.tr(), + contentText: 'modals.4'.tr(), + actions: [ + ActionButton( + text: 'modals.5'.tr(), + isRed: true, + onPressed: () { + context + .read() + .clearAppConfig(); + Navigator.of(context).pop(); + }, + ), + ActionButton( + text: 'basis.cancel'.tr(), + ), + ], + ), ); }, ), @@ -121,7 +130,8 @@ class _AppSettingsPageState extends State { deleteServer(context) ], ), - ),), + ), + ), ); } @@ -131,9 +141,10 @@ class _AppSettingsPageState extends State { return Container( padding: const EdgeInsets.only(top: 20, bottom: 5), decoration: const BoxDecoration( - border: Border( - bottom: BorderSide(width: 1, color: BrandColors.dividerColor), - ),), + border: Border( + bottom: BorderSide(width: 1, color: BrandColors.dividerColor), + ), + ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -156,31 +167,34 @@ class _AppSettingsPageState extends State { showDialog( context: context, builder: (final _) => BrandAlert( - title: 'modals.3'.tr(), - contentText: 'modals.6'.tr(), - actions: [ - ActionButton( - text: 'modals.7'.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(), - ), - ], - ), + title: 'modals.3'.tr(), + contentText: 'modals.6'.tr(), + actions: [ + ActionButton( + text: 'modals.7'.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( @@ -199,7 +213,6 @@ class _AppSettingsPageState extends State { class _TextColumn extends StatelessWidget { const _TextColumn({ - super.key, required this.title, required this.value, this.hasWarning = false, @@ -210,19 +223,21 @@ class _TextColumn extends StatelessWidget { 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, + 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)),), - ], - ); + ).merge(TextStyle(color: hasWarning ? BrandColors.warning : null)), + ), + ], + ); } diff --git a/lib/ui/pages/more/console/console.dart b/lib/ui/pages/more/console/console.dart index a3d046b6..76703444 100644 --- a/lib/ui/pages/more/console/console.dart +++ b/lib/ui/pages/more/console/console.dart @@ -8,7 +8,7 @@ import 'package:selfprivacy/ui/components/brand_divider/brand_divider.dart'; import 'package:selfprivacy/ui/components/brand_header/brand_header.dart'; class Console extends StatefulWidget { - const Console({super.key}); + const Console({final super.key}); @override State createState() => _ConsoleState(); @@ -32,68 +32,77 @@ class _ConsoleState extends State { @override Widget build(final BuildContext context) => SafeArea( - child: Scaffold( - appBar: PreferredSize( - preferredSize: const Size.fromHeight(53), - child: Column( - children: const [ - BrandHeader(title: 'Console', hasBackButton: true), - BrandDivider(), - ], + child: Scaffold( + appBar: PreferredSize( + preferredSize: const Size.fromHeight(53), + child: Column( + children: const [ + BrandHeader(title: 'Console', hasBackButton: true), + BrandDivider(), + ], + ), ), - ), - body: FutureBuilder( - future: getIt.allReady(), - builder: (final BuildContext context, final AsyncSnapshot snapshot) { - if (snapshot.hasData) { - final List messages = getIt.get().messages; + body: FutureBuilder( + future: getIt.allReady(), + builder: ( + final BuildContext context, + final AsyncSnapshot snapshot, + ) { + if (snapshot.hasData) { + final List messages = + getIt.get().messages; - return ListView( - reverse: true, - shrinkWrap: true, - children: [ - 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( + return ListView( + reverse: true, + shrinkWrap: true, + children: [ + 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), - ], - ), - ), - ); - }) - .toList() - .reversed,), - ], - ); - } else { - return Column( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: const [ - Text('Waiting for initialisation'), - SizedBox( - height: 16, - ), - CircularProgressIndicator(), - ], - ); - } - }, + isError ? BrandColors.red1 : null, + ), + ), + TextSpan(text: message.text), + ], + ), + ), + ); + }) + .toList() + .reversed, + ), + ], + ); + } else { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: const [ + Text('Waiting for initialisation'), + SizedBox( + height: 16, + ), + CircularProgressIndicator(), + ], + ); + } + }, + ), ), - ), - ); + ); } diff --git a/lib/ui/pages/more/info/info.dart b/lib/ui/pages/more/info/info.dart index baa82021..04de405e 100644 --- a/lib/ui/pages/more/info/info.dart +++ b/lib/ui/pages/more/info/info.dart @@ -7,28 +7,32 @@ import 'package:package_info/package_info.dart'; import 'package:easy_localization/easy_localization.dart'; class InfoPage extends StatelessWidget { - const InfoPage({super.key}); + const InfoPage({final super.key}); @override Widget build(final BuildContext context) => SafeArea( - child: Scaffold( - appBar: PreferredSize( - preferredSize: const Size.fromHeight(52), - child: BrandHeader(title: 'more.about_app'.tr(), hasBackButton: true), - ), - body: ListView( - padding: paddingH15V0, - children: [ - const BrandDivider(), - const SizedBox(height: 10), - FutureBuilder( + child: Scaffold( + appBar: PreferredSize( + preferredSize: const Size.fromHeight(52), + child: + BrandHeader(title: 'more.about_app'.tr(), hasBackButton: true), + ), + body: ListView( + padding: paddingH15V0, + children: [ + const BrandDivider(), + const SizedBox(height: 10), + FutureBuilder( future: _version(), - builder: (final context, final snapshot) => BrandText.body1('more.about_app_page.text' - .tr(args: [snapshot.data.toString()]),),), - ], + builder: (final context, final snapshot) => BrandText.body1( + 'more.about_app_page.text' + .tr(args: [snapshot.data.toString()]), + ), + ), + ], + ), ), - ), - ); + ); Future _version() async { final PackageInfo packageInfo = await PackageInfo.fromPlatform(); diff --git a/lib/ui/pages/more/more.dart b/lib/ui/pages/more/more.dart index eee3398e..5a02da3c 100644 --- a/lib/ui/pages/more/more.dart +++ b/lib/ui/pages/more/more.dart @@ -21,7 +21,7 @@ import 'package:selfprivacy/ui/pages/more/console/console.dart'; import 'package:selfprivacy/ui/pages/more/info/info.dart'; class MorePage extends StatelessWidget { - const MorePage({super.key}); + const MorePage({final super.key}); @override Widget build(final BuildContext context) { @@ -51,11 +51,12 @@ class MorePage extends StatelessWidget { ), if (isReady) _MoreMenuItem( - title: 'more.create_ssh_key'.tr(), - iconData: Ionicons.key_outline, - goTo: SshKeysPage( - user: context.read().state.rootUser, - ),), + title: 'more.create_ssh_key'.tr(), + iconData: Ionicons.key_outline, + goTo: SshKeysPage( + user: context.read().state.rootUser, + ), + ), if (isReady) _MoreMenuItem( iconData: Icons.password_outlined, @@ -105,7 +106,6 @@ class MorePage extends StatelessWidget { class _MoreMenuItem extends StatelessWidget { const _MoreMenuItem({ - super.key, required this.iconData, required this.title, this.subtitle, diff --git a/lib/ui/pages/onboarding/onboarding.dart b/lib/ui/pages/onboarding/onboarding.dart index 364f5380..dc5c8763 100644 --- a/lib/ui/pages/onboarding/onboarding.dart +++ b/lib/ui/pages/onboarding/onboarding.dart @@ -6,7 +6,7 @@ import 'package:selfprivacy/utils/route_transitions/basic.dart'; import 'package:easy_localization/easy_localization.dart'; class OnboardingPage extends StatefulWidget { - const OnboardingPage({super.key, required this.nextPage}); + const OnboardingPage({required this.nextPage, final super.key}); final Widget nextPage; @override @@ -23,111 +23,111 @@ class _OnboardingPageState extends State { @override Widget build(final BuildContext context) => SafeArea( - child: Scaffold( - body: PageView( - controller: pageController, - children: [ - _withPadding(firstPage()), - _withPadding(secondPage()), - ], + child: Scaffold( + body: PageView( + controller: pageController, + children: [ + _withPadding(firstPage()), + _withPadding(secondPage()), + ], + ), ), - ), - ); + ); Widget _withPadding(final Widget child) => Padding( - padding: const EdgeInsets.symmetric( - horizontal: 15, - ), - child: child, - ); + padding: const EdgeInsets.symmetric( + horizontal: 15, + ), + child: child, + ); Widget firstPage() => ConstrainedBox( - constraints: BoxConstraints( - maxHeight: MediaQuery.of(context).size.height, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 30), - BrandText.h2( - 'onboarding.page1_title'.tr(), - ), - const SizedBox(height: 20), - BrandText.body2('onboarding.page1_text'.tr()), - Flexible( - child: Center( - child: Image.asset( - _fileName( - context: context, - path: 'assets/images/onboarding', - fileExtention: 'png', - fileName: 'onboarding1', + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context).size.height, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 30), + BrandText.h2( + 'onboarding.page1_title'.tr(), + ), + const SizedBox(height: 20), + BrandText.body2('onboarding.page1_text'.tr()), + Flexible( + child: Center( + child: Image.asset( + _fileName( + context: context, + path: 'assets/images/onboarding', + fileExtention: 'png', + fileName: 'onboarding1', + ), ), ), ), - ), - BrandButton.rised( - onPressed: () { - pageController.animateToPage( - 1, - duration: const Duration(milliseconds: 300), - curve: Curves.easeIn, - ); - }, - text: 'basis.next'.tr(), - ), - const SizedBox(height: 30), - ], - ), - ); + BrandButton.rised( + onPressed: () { + pageController.animateToPage( + 1, + duration: const Duration(milliseconds: 300), + curve: Curves.easeIn, + ); + }, + text: 'basis.next'.tr(), + ), + const SizedBox(height: 30), + ], + ), + ); Widget secondPage() => ConstrainedBox( - constraints: BoxConstraints( - maxHeight: MediaQuery.of(context).size.height, - ), - child: Column( - children: [ - const SizedBox(height: 30), - BrandText.h2('onboarding.page2_title'.tr()), - const SizedBox(height: 20), - BrandText.body2('onboarding.page2_text'.tr()), - const SizedBox(height: 20), - Center( - child: Image.asset( - _fileName( - context: context, - path: 'assets/images/onboarding', - fileExtention: 'png', - fileName: 'logos_line', - ), - ), - ), - Flexible( - child: Center( + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context).size.height, + ), + child: Column( + children: [ + const SizedBox(height: 30), + BrandText.h2('onboarding.page2_title'.tr()), + const SizedBox(height: 20), + BrandText.body2('onboarding.page2_text'.tr()), + const SizedBox(height: 20), + Center( child: Image.asset( _fileName( context: context, path: 'assets/images/onboarding', fileExtention: 'png', - fileName: 'onboarding2', + fileName: 'logos_line', ), ), ), - ), - BrandButton.rised( - onPressed: () { - context.read().turnOffOnboarding(); - Navigator.of(context).pushAndRemoveUntil( - materialRoute(widget.nextPage), - (final route) => false, - ); - }, - text: 'basis.got_it'.tr(), - ), - const SizedBox(height: 30), - ], - ), - ); + Flexible( + child: Center( + child: Image.asset( + _fileName( + context: context, + path: 'assets/images/onboarding', + fileExtention: 'png', + fileName: 'onboarding2', + ), + ), + ), + ), + BrandButton.rised( + onPressed: () { + context.read().turnOffOnboarding(); + Navigator.of(context).pushAndRemoveUntil( + materialRoute(widget.nextPage), + (final route) => false, + ); + }, + text: 'basis.got_it'.tr(), + ), + const SizedBox(height: 30), + ], + ), + ); } String _fileName({ diff --git a/lib/ui/pages/providers/providers.dart b/lib/ui/pages/providers/providers.dart index 7168689a..97e4aeeb 100644 --- a/lib/ui/pages/providers/providers.dart +++ b/lib/ui/pages/providers/providers.dart @@ -21,7 +21,7 @@ import 'package:selfprivacy/utils/route_transitions/basic.dart'; GlobalKey navigatorKey = GlobalKey(); class ProvidersPage extends StatefulWidget { - const ProvidersPage({super.key}); + const ProvidersPage({final super.key}); @override State createState() => _ProvidersPageState(); @@ -32,8 +32,10 @@ class _ProvidersPageState extends State { Widget build(final BuildContext context) { final bool isReady = context.watch().state is ServerInstallationFinished; - final bool isBackupInitialized = context.watch().state.isInitialized; - final DnsRecordsStatus dnsStatus = context.watch().state.dnsState; + final bool isBackupInitialized = + context.watch().state.isInitialized; + final DnsRecordsStatus dnsStatus = + context.watch().state.dnsState; StateType getDnsStatus() { if (dnsStatus == DnsRecordsStatus.uninitialized || @@ -87,7 +89,7 @@ class _ProvidersPageState extends State { } class _Card extends StatelessWidget { - const _Card({super.key, required this.provider}); + const _Card({required this.provider}); final ProviderModel provider; @override @@ -122,17 +124,21 @@ class _Card extends StatelessWidget { message = domainName; stableText = 'providers.domain.status'.tr(); - onTap = () => Navigator.of(context).push(materialRoute( - const DnsDetailsPage(), - ),); + onTap = () => Navigator.of(context).push( + materialRoute( + const DnsDetailsPage(), + ), + ); break; case ProviderType.backup: title = 'providers.backup.card_title'.tr(); stableText = 'providers.backup.status'.tr(); - onTap = () => Navigator.of(context).push(materialRoute( - const BackupDetails(), - ),); + onTap = () => Navigator.of(context).push( + materialRoute( + const BackupDetails(), + ), + ); break; } return GestureDetector( diff --git a/lib/ui/pages/recovery_key/recovery_key.dart b/lib/ui/pages/recovery_key/recovery_key.dart index 9d329a20..d7167a6f 100644 --- a/lib/ui/pages/recovery_key/recovery_key.dart +++ b/lib/ui/pages/recovery_key/recovery_key.dart @@ -14,7 +14,7 @@ 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}); + const RecoveryKey({final super.key}); @override State createState() => _RecoveryKeyState(); @@ -61,7 +61,7 @@ class _RecoveryKeyState extends State { } class RecoveryKeyContent extends StatefulWidget { - const RecoveryKeyContent({super.key}); + const RecoveryKeyContent({final super.key}); @override State createState() => _RecoveryKeyContentState(); @@ -107,50 +107,51 @@ class _RecoveryKeyContentState extends State { } class RecoveryKeyStatusCard extends StatelessWidget { - const RecoveryKeyStatusCard({required this.isValid, super.key}); + const RecoveryKeyStatusCard({required this.isValid, final super.key}); final bool isValid; @override Widget build(final BuildContext context) => BrandCards.filled( - child: ListTile( - title: isValid - ? Text( - 'recovery_key.key_valid'.tr(), - style: Theme.of(context).textTheme.titleMedium!.copyWith( - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ) - : Text( - 'recovery_key.key_invalid'.tr(), - style: Theme.of(context).textTheme.titleMedium!.copyWith( - color: Theme.of(context).colorScheme.onErrorContainer, - ), - ), - leading: isValid - ? Icon( - Icons.check_circle_outlined, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ) - : Icon( - Icons.cancel_outlined, - color: Theme.of(context).colorScheme.onErrorContainer, - ), - tileColor: isValid - ? Theme.of(context).colorScheme.surfaceVariant - : Theme.of(context).colorScheme.errorContainer, - ), - ); + child: ListTile( + title: isValid + ? Text( + 'recovery_key.key_valid'.tr(), + style: Theme.of(context).textTheme.titleMedium!.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ) + : Text( + 'recovery_key.key_invalid'.tr(), + style: Theme.of(context).textTheme.titleMedium!.copyWith( + color: Theme.of(context).colorScheme.onErrorContainer, + ), + ), + leading: isValid + ? Icon( + Icons.check_circle_outlined, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ) + : Icon( + Icons.cancel_outlined, + color: Theme.of(context).colorScheme.onErrorContainer, + ), + tileColor: isValid + ? Theme.of(context).colorScheme.surfaceVariant + : Theme.of(context).colorScheme.errorContainer, + ), + ); } class RecoveryKeyInformation extends StatelessWidget { - const RecoveryKeyInformation({required this.state, super.key}); + const RecoveryKeyInformation({required this.state, final super.key}); final RecoveryKeyState state; @override Widget build(final BuildContext context) { - const EdgeInsets padding = EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0); + const EdgeInsets padding = + EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0); return SizedBox( width: double.infinity, child: Column( @@ -191,7 +192,7 @@ class RecoveryKeyInformation extends StatelessWidget { } class RecoveryKeyConfiguration extends StatefulWidget { - const RecoveryKeyConfiguration({super.key}); + const RecoveryKeyConfiguration({final super.key}); @override State createState() => _RecoveryKeyConfigurationState(); @@ -217,11 +218,13 @@ class _RecoveryKeyConfigurationState extends State { _isLoading = true; }); try { - final String token = await context.read().generateRecoveryKey( - numberOfUses: - _isAmountToggled ? int.tryParse(_amountController.text) : null, - expirationDate: _isExpirationToggled ? _selectedDate : null, - ); + final String token = + await context.read().generateRecoveryKey( + numberOfUses: _isAmountToggled + ? int.tryParse(_amountController.text) + : null, + expirationDate: _isExpirationToggled ? _selectedDate : null, + ); if (!mounted) return; setState(() { _isLoading = false; @@ -310,9 +313,10 @@ class _RecoveryKeyConfigurationState extends State { enabled: _isAmountToggled, controller: _amountController, decoration: InputDecoration( - border: const OutlineInputBorder(), - errorText: _isAmountError ? ' ' : null, - labelText: 'recovery_key.key_amount_field_title'.tr(),), + border: const OutlineInputBorder(), + errorText: _isAmountError ? ' ' : null, + labelText: 'recovery_key.key_amount_field_title'.tr(), + ), keyboardType: TextInputType.number, inputFormatters: [ FilteringTextInputFormatter.digitsOnly, @@ -352,9 +356,10 @@ class _RecoveryKeyConfigurationState extends State { }, readOnly: true, decoration: InputDecoration( - border: const OutlineInputBorder(), - errorText: _isExpirationError ? ' ' : null, - labelText: 'recovery_key.key_duedate_field_title'.tr(),), + border: const OutlineInputBorder(), + errorText: _isExpirationError ? ' ' : null, + labelText: 'recovery_key.key_duedate_field_title'.tr(), + ), keyboardType: TextInputType.number, inputFormatters: [ FilteringTextInputFormatter.digitsOnly, @@ -378,10 +383,11 @@ class _RecoveryKeyConfigurationState extends State { Future _selectDate(final BuildContext context) async { final DateTime? selected = await showDatePicker( - context: context, - initialDate: _selectedDate, - firstDate: DateTime.now(), - lastDate: DateTime(DateTime.now().year + 50),); + context: context, + initialDate: _selectedDate, + firstDate: DateTime.now(), + lastDate: DateTime(DateTime.now().year + 50), + ); if (selected != null && selected != _selectedDate) { setState( diff --git a/lib/ui/pages/recovery_key/recovery_key_receiving.dart b/lib/ui/pages/recovery_key/recovery_key_receiving.dart index f695dd47..7ae6adaf 100644 --- a/lib/ui/pages/recovery_key/recovery_key_receiving.dart +++ b/lib/ui/pages/recovery_key/recovery_key_receiving.dart @@ -4,45 +4,45 @@ import 'package:selfprivacy/ui/components/brand_button/filled_button.dart'; import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; class RecoveryKeyReceiving extends StatelessWidget { - const RecoveryKeyReceiving({required this.recoveryKey, super.key}); + const RecoveryKeyReceiving({required this.recoveryKey, final super.key}); final String recoveryKey; @override Widget build(final BuildContext context) => BrandHeroScreen( - heroTitle: 'recovery_key.key_main_header'.tr(), - heroSubtitle: 'recovery_key.key_receiving_description'.tr(), - hasBackButton: true, - hasFlashButton: false, - children: [ - const Divider(), - const SizedBox(height: 16), - Text( - recoveryKey, - style: Theme.of(context).textTheme.bodyLarge!.copyWith( - fontSize: 24, - fontFamily: 'RobotoMono', - ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 16), - const Divider(), - const SizedBox(height: 16), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Icon(Icons.info_outlined, size: 24), - const SizedBox(height: 16), - Text('recovery_key.key_receiving_info'.tr()), - ], - ), - const SizedBox(height: 16), - FilledButton( - title: 'recovery_key.key_receiving_done'.tr(), - onPressed: () { - Navigator.of(context).popUntil((final route) => route.isFirst); - }, - ), - ], - ); + heroTitle: 'recovery_key.key_main_header'.tr(), + heroSubtitle: 'recovery_key.key_receiving_description'.tr(), + hasBackButton: true, + hasFlashButton: false, + children: [ + const Divider(), + const SizedBox(height: 16), + Text( + recoveryKey, + style: Theme.of(context).textTheme.bodyLarge!.copyWith( + fontSize: 24, + fontFamily: 'RobotoMono', + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + const Divider(), + const SizedBox(height: 16), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Icon(Icons.info_outlined, size: 24), + const SizedBox(height: 16), + Text('recovery_key.key_receiving_info'.tr()), + ], + ), + const SizedBox(height: 16), + FilledButton( + title: 'recovery_key.key_receiving_done'.tr(), + onPressed: () { + Navigator.of(context).popUntil((final route) => route.isFirst); + }, + ), + ], + ); } diff --git a/lib/ui/pages/root_route.dart b/lib/ui/pages/root_route.dart index fb4efeed..d68e4a0e 100644 --- a/lib/ui/pages/root_route.dart +++ b/lib/ui/pages/root_route.dart @@ -9,7 +9,7 @@ import 'package:selfprivacy/ui/pages/users/users.dart'; import 'package:selfprivacy/ui/components/pre_styled_buttons/flash_fab.dart'; class RootPage extends StatefulWidget { - const RootPage({super.key}); + const RootPage({final super.key}); @override State createState() => _RootPageState(); @@ -92,7 +92,6 @@ class _RootPageState extends State with TickerProviderStateMixin { } class ChangeTab { - ChangeTab(this.onPress); final ValueChanged onPress; } diff --git a/lib/ui/pages/server_details/chart.dart b/lib/ui/pages/server_details/chart.dart index 0091b6b1..f7972e9c 100644 --- a/lib/ui/pages/server_details/chart.dart +++ b/lib/ui/pages/server_details/chart.dart @@ -1,8 +1,6 @@ part of 'server_details_screen.dart'; class _Chart extends StatelessWidget { - const _Chart({final super.key}); - @override Widget build(final BuildContext context) { final HetznerMetricsCubit cubit = context.watch(); @@ -129,44 +127,40 @@ class _Chart extends StatelessWidget { class Legend extends StatelessWidget { const Legend({ - final Key? key, required this.color, required this.text, - }) : super(key: key); + final super.key, + }); final String text; final Color color; @override - Widget build(final BuildContext context) { - return Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - _ColoredBox(color: color), - const SizedBox(width: 5), - BrandText.small(text), - ], - ); - } + Widget build(final BuildContext context) => Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + _ColoredBox(color: color), + const SizedBox(width: 5), + BrandText.small(text), + ], + ); } class _ColoredBox extends StatelessWidget { const _ColoredBox({ - final Key? key, required this.color, - }) : super(key: key); + }); final Color color; @override - Widget build(final BuildContext context) { - return Container( - width: 10, - height: 10, - decoration: BoxDecoration( + Widget build(final BuildContext context) => Container( + width: 10, + height: 10, + decoration: BoxDecoration( color: color.withOpacity(0.3), border: Border.all( color: color, - )), - ); - } + ), + ), + ); } diff --git a/lib/ui/pages/server_details/cpu_chart.dart b/lib/ui/pages/server_details/cpu_chart.dart index 07bd8539..35d6fff4 100644 --- a/lib/ui/pages/server_details/cpu_chart.dart +++ b/lib/ui/pages/server_details/cpu_chart.dart @@ -8,7 +8,7 @@ import 'package:intl/intl.dart'; class CpuChart extends StatelessWidget { const CpuChart({ - Key? key, + final Key? key, required this.data, required this.period, required this.start, @@ -20,9 +20,9 @@ class CpuChart extends StatelessWidget { List getSpots() { var i = 0; - List res = []; + final List res = []; - for (var d in data) { + for (final d in data) { res.add(FlSpot(i.toDouble(), d.value)); i++; } @@ -31,97 +31,96 @@ class CpuChart extends StatelessWidget { } @override - Widget build(BuildContext context) { - return LineChart( - LineChartData( - lineTouchData: LineTouchData(enabled: false), - lineBarsData: [ - LineChartBarData( - spots: getSpots(), - isCurved: true, - barWidth: 1, - color: Colors.red, - dotData: FlDotData( - show: false, + Widget build(final BuildContext context) => LineChart( + LineChartData( + lineTouchData: LineTouchData(enabled: false), + lineBarsData: [ + LineChartBarData( + spots: getSpots(), + isCurved: true, + barWidth: 1, + color: Colors.red, + dotData: FlDotData( + show: false, + ), ), - ), - ], - minY: 0, - maxY: 100, - minX: data.length - 200, - titlesData: FlTitlesData( - topTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)), - bottomTitles: AxisTitles( - sideTitles: SideTitles( - interval: 20, - reservedSize: 50, - getTitlesWidget: (value, titleMeta) { - return Padding( + ], + minY: 0, + maxY: 100, + minX: data.length - 200, + titlesData: FlTitlesData( + topTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)), + bottomTitles: AxisTitles( + sideTitles: SideTitles( + interval: 20, + reservedSize: 50, + getTitlesWidget: (final value, final titleMeta) => Padding( padding: const EdgeInsets.all(8.0), child: RotatedBox( quarterTurns: 1, - child: Text(bottomTitle(value.toInt()), - style: const TextStyle( - fontSize: 10, - color: Colors.purple, - fontWeight: FontWeight.bold, - )), - ), - ); - }, - showTitles: true, - ), - ), - leftTitles: AxisTitles( - sideTitles: SideTitles( - getTitlesWidget: (value, titleMeta) { - return Padding( - padding: const EdgeInsets.only(right: 15), child: Text( - value.toInt().toString(), - style: progressTextStyleLight.copyWith( - color: Theme.of(context).brightness == Brightness.dark - ? BrandColors.gray4 - : null, + bottomTitle(value.toInt()), + style: const TextStyle( + fontSize: 10, + color: Colors.purple, + fontWeight: FontWeight.bold, ), - )); - }, - interval: 25, - showTitles: false, + ), + ), + ), + showTitles: true, + ), + ), + leftTitles: AxisTitles( + sideTitles: SideTitles( + getTitlesWidget: (final value, final titleMeta) => Padding( + padding: const EdgeInsets.only(right: 15), + child: Text( + value.toInt().toString(), + style: progressTextStyleLight.copyWith( + color: Theme.of(context).brightness == Brightness.dark + ? BrandColors.gray4 + : null, + ), + ), + ), + interval: 25, + showTitles: false, + ), ), ), + gridData: FlGridData(show: true), ), - gridData: FlGridData(show: true), - ), - ); - } + ); bool checkToShowTitle( - double minValue, - double maxValue, - SideTitles sideTitles, - double appliedInterval, - double value, + final double minValue, + final double maxValue, + final SideTitles sideTitles, + final double appliedInterval, + final double value, ) { if (value < 0) { return false; } else if (value == 0) { return true; } - var localValue = value - minValue; - var v = localValue / 20; + + final localValue = value - minValue; + final v = localValue / 20; return v - v.floor() == 0; } - String bottomTitle(int value) { + String bottomTitle(final int value) { final hhmm = DateFormat('HH:mm'); - var day = DateFormat('MMMd'); + final day = DateFormat('MMMd'); String res; if (value <= 0) { return ''; } - var time = data[value].time; + + final time = data[value].time; switch (period) { case Period.hour: case Period.day: diff --git a/lib/ui/pages/server_details/header.dart b/lib/ui/pages/server_details/header.dart index b28a5efc..a10bc1cf 100644 --- a/lib/ui/pages/server_details/header.dart +++ b/lib/ui/pages/server_details/header.dart @@ -2,60 +2,57 @@ part of 'server_details_screen.dart'; class _Header extends StatelessWidget { const _Header({ - Key? key, required this.providerState, required this.tabController, - }) : super(key: key); + }); final StateType providerState; final TabController tabController; @override - Widget build(BuildContext context) { - return Row( - children: [ - IconStatusMask( - status: providerState, - child: const Icon( - BrandIcons.server, - size: 40, - color: Colors.white, - ), - ), - const SizedBox(width: 10), - BrandText.h2('providers.server.card_title'.tr()), - const Spacer(), - Padding( - padding: const EdgeInsets.symmetric( - vertical: 4, - horizontal: 2, - ), - child: PopupMenuButton<_PopupMenuItemType>( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10.0), + Widget build(final BuildContext context) => Row( + children: [ + IconStatusMask( + status: providerState, + child: const Icon( + BrandIcons.server, + size: 40, + color: Colors.white, ), - onSelected: (_PopupMenuItemType result) { - switch (result) { - case _PopupMenuItemType.setting: - tabController.animateTo(1); - break; - } - }, - icon: const Icon(Icons.more_vert), - itemBuilder: (BuildContext context) => [ - PopupMenuItem<_PopupMenuItemType>( - value: _PopupMenuItemType.setting, - child: Container( - padding: const EdgeInsets.only(left: 5), - child: Text('basis.settings'.tr()), - ), - ), - ], ), - ), - ], - ); - } + const SizedBox(width: 10), + BrandText.h2('providers.server.card_title'.tr()), + const Spacer(), + Padding( + padding: const EdgeInsets.symmetric( + vertical: 4, + horizontal: 2, + ), + child: PopupMenuButton<_PopupMenuItemType>( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10.0), + ), + onSelected: (final _PopupMenuItemType result) { + switch (result) { + case _PopupMenuItemType.setting: + tabController.animateTo(1); + break; + } + }, + icon: const Icon(Icons.more_vert), + itemBuilder: (final BuildContext context) => [ + PopupMenuItem<_PopupMenuItemType>( + value: _PopupMenuItemType.setting, + child: Container( + padding: const EdgeInsets.only(left: 5), + child: Text('basis.settings'.tr()), + ), + ), + ], + ), + ), + ], + ); } enum _PopupMenuItemType { setting } diff --git a/lib/ui/pages/server_details/network_charts.dart b/lib/ui/pages/server_details/network_charts.dart index 31b3dd21..838b4ebb 100644 --- a/lib/ui/pages/server_details/network_charts.dart +++ b/lib/ui/pages/server_details/network_charts.dart @@ -10,7 +10,7 @@ import 'package:intl/intl.dart'; class NetworkChart extends StatelessWidget { const NetworkChart({ - Key? key, + final Key? key, required this.listData, required this.period, required this.start, @@ -20,11 +20,11 @@ class NetworkChart extends StatelessWidget { final Period period; final DateTime start; - List getSpots(data) { + List getSpots(final data) { var i = 0; - List res = []; + final List res = []; - for (var d in data) { + for (final d in data) { res.add(FlSpot(i.toDouble(), d.value)); i++; } @@ -33,120 +33,119 @@ class NetworkChart extends StatelessWidget { } @override - Widget build(BuildContext context) { - return SizedBox( - height: 150, - width: MediaQuery.of(context).size.width, - child: LineChart( - LineChartData( - lineTouchData: LineTouchData(enabled: false), - lineBarsData: [ - LineChartBarData( - spots: getSpots(listData[0]), - isCurved: true, - barWidth: 1, - color: Colors.red, - dotData: FlDotData( - show: false, + Widget build(final BuildContext context) => SizedBox( + height: 150, + width: MediaQuery.of(context).size.width, + child: LineChart( + LineChartData( + lineTouchData: LineTouchData(enabled: false), + lineBarsData: [ + LineChartBarData( + spots: getSpots(listData[0]), + isCurved: true, + barWidth: 1, + color: Colors.red, + dotData: FlDotData( + show: false, + ), ), - ), - LineChartBarData( - spots: getSpots(listData[1]), - isCurved: true, - barWidth: 1, - color: Colors.green, - dotData: FlDotData( - show: false, + LineChartBarData( + spots: getSpots(listData[1]), + isCurved: true, + barWidth: 1, + color: Colors.green, + dotData: FlDotData( + show: false, + ), ), - ), - ], - minY: 0, - maxY: [ - ...listData[0].map((e) => e.value), - ...listData[1].map((e) => e.value) - ].reduce(max) * - 1.2, - minX: listData[0].length - 200, - titlesData: FlTitlesData( - topTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)), - bottomTitles: AxisTitles( - sideTitles: SideTitles( - interval: 20, - reservedSize: 50, - getTitlesWidget: (value, titleMeta) { - return Padding( + ], + minY: 0, + maxY: [ + ...listData[0].map((final e) => e.value), + ...listData[1].map((final e) => e.value) + ].reduce(max) * + 1.2, + minX: listData[0].length - 200, + titlesData: FlTitlesData( + topTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)), + bottomTitles: AxisTitles( + sideTitles: SideTitles( + interval: 20, + reservedSize: 50, + getTitlesWidget: (final value, final titleMeta) => Padding( padding: const EdgeInsets.all(8.0), child: RotatedBox( quarterTurns: 1, - child: Text(bottomTitle(value.toInt()), - style: const TextStyle( - fontSize: 10, - color: Colors.purple, - fontWeight: FontWeight.bold, - )), - ), - ); - }, - showTitles: true, - ), - ), - leftTitles: AxisTitles( - sideTitles: SideTitles( - reservedSize: 50, - getTitlesWidget: (value, titleMeta) { - return Padding( - padding: const EdgeInsets.only(right: 5), child: Text( - value.toInt().toString(), - style: progressTextStyleLight.copyWith( - color: Theme.of(context).brightness == Brightness.dark - ? BrandColors.gray4 - : null, + bottomTitle(value.toInt()), + style: const TextStyle( + fontSize: 10, + color: Colors.purple, + fontWeight: FontWeight.bold, ), - )); - }, - interval: [ - ...listData[0].map((e) => e.value), - ...listData[1].map((e) => e.value) - ].reduce(max) * - 2 / - 10, - showTitles: false, + ), + ), + ), + showTitles: true, + ), + ), + leftTitles: AxisTitles( + sideTitles: SideTitles( + reservedSize: 50, + getTitlesWidget: (final value, final titleMeta) => Padding( + padding: const EdgeInsets.only(right: 5), + child: Text( + value.toInt().toString(), + style: progressTextStyleLight.copyWith( + color: Theme.of(context).brightness == Brightness.dark + ? BrandColors.gray4 + : null, + ), + ), + ), + interval: [ + ...listData[0].map((final e) => e.value), + ...listData[1].map((final e) => e.value) + ].reduce(max) * + 2 / + 10, + showTitles: false, + ), ), ), + gridData: FlGridData(show: true), ), - gridData: FlGridData(show: true), ), - ), - ); - } + ); bool checkToShowTitle( - double minValue, - double maxValue, - SideTitles sideTitles, - double appliedInterval, - double value, + final double minValue, + final double maxValue, + final SideTitles sideTitles, + final double appliedInterval, + final double value, ) { if (value < 0) { return false; } else if (value == 0) { return true; } - var diff = value - minValue; - var finalValue = diff / 20; + + final diff = value - minValue; + final finalValue = diff / 20; return finalValue - finalValue.floor() == 0; } - String bottomTitle(int value) { + String bottomTitle(final int value) { final hhmm = DateFormat('HH:mm'); - var day = DateFormat('MMMd'); + final day = DateFormat('MMMd'); String res; if (value <= 0) { return ''; } - var time = listData[0][value].time; + + final time = listData[0][value].time; switch (period) { case Period.hour: case Period.day: diff --git a/lib/ui/pages/server_details/server_details_screen.dart b/lib/ui/pages/server_details/server_details_screen.dart index 5eb3be3e..1f442220 100644 --- a/lib/ui/pages/server_details/server_details_screen.dart +++ b/lib/ui/pages/server_details/server_details_screen.dart @@ -22,8 +22,8 @@ import 'package:selfprivacy/utils/named_font_weight.dart'; import 'package:selfprivacy/utils/route_transitions/basic.dart'; import 'package:timezone/timezone.dart'; -import 'cpu_chart.dart'; -import 'network_charts.dart'; +import 'package:selfprivacy/ui/pages/server_details/cpu_chart.dart'; +import 'package:selfprivacy/ui/pages/server_details/network_charts.dart'; part 'chart.dart'; part 'header.dart'; @@ -34,7 +34,7 @@ part 'time_zone/time_zone.dart'; var navigatorKey = GlobalKey(); class ServerDetailsScreen extends StatefulWidget { - const ServerDetailsScreen({Key? key}) : super(key: key); + const ServerDetailsScreen({final super.key}); @override State createState() => _ServerDetailsScreenState(); @@ -60,13 +60,13 @@ class _ServerDetailsScreenState extends State } @override - Widget build(BuildContext context) { - var isReady = context.watch().state + Widget build(final BuildContext context) { + final bool isReady = context.watch().state is ServerInstallationFinished; - var providerState = isReady ? StateType.stable : StateType.uninitialized; + final providerState = isReady ? StateType.stable : StateType.uninitialized; return BlocProvider( - create: (context) => ServerDetailsCubit()..check(), + create: (final context) => ServerDetailsCubit()..check(), child: Scaffold( appBar: PreferredSize( preferredSize: const Size.fromHeight(52), @@ -97,19 +97,20 @@ class _ServerDetailsScreenState extends State crossAxisAlignment: CrossAxisAlignment.start, children: [ _Header( - providerState: providerState, - tabController: tabController), + providerState: providerState, + tabController: tabController, + ), BrandText.body1('providers.server.bottom_sheet.1'.tr()), ], ), ), const SizedBox(height: 10), BlocProvider( - create: (context) => HetznerMetricsCubit()..restart(), - child: const _Chart(), + create: (final context) => HetznerMetricsCubit()..restart(), + child: _Chart(), ), const SizedBox(height: 20), - const _TextDetails(), + _TextDetails(), ], ), ), diff --git a/lib/ui/pages/server_details/server_settings.dart b/lib/ui/pages/server_details/server_settings.dart index 93393632..18d425e6 100644 --- a/lib/ui/pages/server_details/server_settings.dart +++ b/lib/ui/pages/server_details/server_settings.dart @@ -2,15 +2,14 @@ part of 'server_details_screen.dart'; class _ServerSettings extends StatelessWidget { const _ServerSettings({ - Key? key, required this.tabController, - }) : super(key: key); + }); final TabController tabController; @override - Widget build(BuildContext context) { - var serverDetailsState = context.watch().state; + Widget build(final BuildContext context) { + final serverDetailsState = context.watch().state; if (serverDetailsState is ServerDetailsNotReady) { return const Text('not ready'); } else if (serverDetailsState is! Loaded) { @@ -37,7 +36,7 @@ class _ServerSettings extends StatelessWidget { ), const BrandDivider(), SwitcherBlock( - onChange: (_) {}, + onChange: (final _) {}, isActive: serverDetailsState.autoUpgradeSettings.enable, child: const _TextColumn( title: 'Allow Auto-upgrade', @@ -46,7 +45,7 @@ class _ServerSettings extends StatelessWidget { ), ), SwitcherBlock( - onChange: (_) {}, + onChange: (final _) {}, isActive: serverDetailsState.autoUpgradeSettings.allowReboot, child: const _TextColumn( title: 'Reboot after upgrade', @@ -71,60 +70,55 @@ class _ServerSettings extends StatelessWidget { class _Button extends StatelessWidget { const _Button({ - Key? key, required this.onTap, required this.child, - }) : super(key: key); + }); final Widget child; final VoidCallback onTap; @override - Widget build(BuildContext context) { - return InkWell( - onTap: onTap, - child: Container( - padding: const EdgeInsets.only(top: 20, bottom: 5), - decoration: const BoxDecoration( + Widget build(final BuildContext context) => InkWell( + onTap: onTap, + child: Container( + padding: const EdgeInsets.only(top: 20, bottom: 5), + decoration: const BoxDecoration( border: Border( - bottom: BorderSide(width: 1, color: BrandColors.dividerColor), - )), - child: child, - ), - ); - } + bottom: BorderSide(width: 1, color: BrandColors.dividerColor), + ), + ), + child: child, + ), + ); } class _TextColumn extends StatelessWidget { const _TextColumn({ - Key? key, required this.title, required this.value, this.hasWarning = false, - }) : super(key: key); + }); final String title; final String value; final bool hasWarning; @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - BrandText.body1( - title, - style: TextStyle(color: hasWarning ? BrandColors.warning : null), - ), - const SizedBox(height: 5), - BrandText.body1( - value, - style: TextStyle( - fontSize: 13, - height: 1.53, - color: hasWarning ? BrandColors.warning : BrandColors.gray1, + 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: TextStyle( + fontSize: 13, + height: 1.53, + color: hasWarning ? BrandColors.warning : BrandColors.gray1, + ), + ), + ], + ); } diff --git a/lib/ui/pages/server_details/text_details.dart b/lib/ui/pages/server_details/text_details.dart index 397f7da1..2285d305 100644 --- a/lib/ui/pages/server_details/text_details.dart +++ b/lib/ui/pages/server_details/text_details.dart @@ -1,19 +1,17 @@ part of 'server_details_screen.dart'; class _TextDetails extends StatelessWidget { - const _TextDetails({Key? key}) : super(key: key); - @override - Widget build(BuildContext context) { - var details = context.watch().state; + Widget build(final BuildContext context) { + final details = context.watch().state; if (details is ServerDetailsLoading || details is ServerDetailsInitial) { return _TempMessage(message: 'basis.loading'.tr()); } else if (details is ServerDetailsNotReady) { return _TempMessage(message: 'basis.no_data'.tr()); } else if (details is Loaded) { - var data = details.serverInfo; - var checkTime = details.checkTime; + final data = details.serverInfo; + final checkTime = details.checkTime; return Column( children: [ Center(child: BrandText.h3('providers.server.bottom_sheet.2'.tr())), @@ -128,44 +126,38 @@ class _TextDetails extends StatelessWidget { } } - Widget getRowTitle(String title) { - return Padding( - padding: const EdgeInsets.only(right: 10), - child: BrandText.h5( - title, - textAlign: TextAlign.right, - ), - ); - } + Widget getRowTitle(final String title) => Padding( + padding: const EdgeInsets.only(right: 10), + child: BrandText.h5( + title, + textAlign: TextAlign.right, + ), + ); - Widget getRowValue(String title, {bool isBold = false}) { - return BrandText.body1( - title, - style: isBold - ? const TextStyle( - fontWeight: NamedFontWeight.demiBold, - ) - : null, - ); - } + Widget getRowValue(final String title, {final bool isBold = false}) => + BrandText.body1( + title, + style: isBold + ? const TextStyle( + fontWeight: NamedFontWeight.demiBold, + ) + : null, + ); } class _TempMessage extends StatelessWidget { const _TempMessage({ - Key? key, required this.message, - }) : super(key: key); + }); final String message; @override - Widget build(BuildContext context) { - return SizedBox( - height: MediaQuery.of(context).size.height - 100, - child: Center( - child: BrandText.body2(message), - ), - ); - } + Widget build(final BuildContext context) => SizedBox( + height: MediaQuery.of(context).size.height - 100, + child: Center( + child: BrandText.body2(message), + ), + ); } final DateFormat formatter = DateFormat('HH:mm:ss'); 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 402aeb26..33799c35 100644 --- a/lib/ui/pages/server_details/time_zone/time_zone.dart +++ b/lib/ui/pages/server_details/time_zone/time_zone.dart @@ -1,11 +1,13 @@ part of '../server_details_screen.dart'; final List locations = timeZoneDatabase.locations.values.toList() - ..sort((l1, l2) => - l1.currentTimeZone.offset.compareTo(l2.currentTimeZone.offset)); + ..sort( + (final l1, final l2) => + l1.currentTimeZone.offset.compareTo(l2.currentTimeZone.offset), + ); class SelectTimezone extends StatefulWidget { - const SelectTimezone({Key? key}) : super(key: key); + const SelectTimezone({final super.key}); @override State createState() => _SelectTimezoneState(); @@ -20,15 +22,20 @@ class _SelectTimezoneState extends State { super.initState(); } - void _afterLayout(_) { - var t = DateTime.now().timeZoneOffset; - var index = locations.indexWhere((element) => - Duration(milliseconds: element.currentTimeZone.offset) == t); + void _afterLayout(final _) { + final t = DateTime.now().timeZoneOffset; + final index = locations.indexWhere( + (final element) => + Duration(milliseconds: element.currentTimeZone.offset) == t, + ); print(t); if (index >= 0) { - controller.animateTo(60.0 * index, - duration: const Duration(milliseconds: 300), curve: Curves.easeIn); + controller.animateTo( + 60.0 * index, + duration: const Duration(milliseconds: 300), + curve: Curves.easeIn, + ); } } @@ -39,69 +46,69 @@ class _SelectTimezoneState extends State { } @override - Widget build(BuildContext context) { - return Scaffold( - appBar: const PreferredSize( - preferredSize: Size.fromHeight(52), - child: BrandHeader( - title: 'select timezone', - hasBackButton: true, + Widget build(final BuildContext context) => Scaffold( + appBar: const PreferredSize( + preferredSize: Size.fromHeight(52), + child: BrandHeader( + title: 'select timezone', + hasBackButton: true, + ), ), - ), - body: ListView( - controller: controller, - children: locations - .asMap() - .map((key, value) { - var duration = - Duration(milliseconds: value.currentTimeZone.offset); - var area = value.currentTimeZone.abbreviation - .replaceAll(RegExp(r'[\d+()-]'), ''); + body: ListView( + controller: controller, + children: locations + .asMap() + .map((final key, final value) { + final duration = + Duration(milliseconds: value.currentTimeZone.offset); + final area = value.currentTimeZone.abbreviation + .replaceAll(RegExp(r'[\d+()-]'), ''); - String timezoneName = value.name; - if (context.locale.toString() == 'ru') { - timezoneName = russian[value.name] ?? - () { - var arr = value.name.split('/')..removeAt(0); - return arr.join('/'); - }(); - } + String timezoneName = value.name; + if (context.locale.toString() == 'ru') { + timezoneName = russian[value.name] ?? + () { + final arr = value.name.split('/')..removeAt(0); + return arr.join('/'); + }(); + } - return MapEntry( - key, - Container( - height: 60, - padding: const EdgeInsets.symmetric(horizontal: 20), - decoration: const BoxDecoration( - border: Border( + return MapEntry( + key, + Container( + height: 60, + padding: const EdgeInsets.symmetric(horizontal: 20), + decoration: const BoxDecoration( + border: Border( bottom: BorderSide( - color: BrandColors.dividerColor, - )), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - BrandText.body1( - timezoneName, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, + color: BrandColors.dividerColor, ), ), - BrandText.small( + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + BrandText.body1( + timezoneName, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + BrandText.small( 'GMT ${duration.toDayHourMinuteFormat()} ${area.isNotEmpty ? '($area)' : ''}', style: const TextStyle( fontSize: 13, - )), - ], + ), + ), + ], + ), ), - ), - ); - }) - .values - .toList(), - ), - ); - } + ); + }) + .values + .toList(), + ), + ); } diff --git a/lib/ui/pages/services/services.dart b/lib/ui/pages/services/services.dart index c4939615..0b8ea12d 100644 --- a/lib/ui/pages/services/services.dart +++ b/lib/ui/pages/services/services.dart @@ -21,7 +21,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:selfprivacy/utils/ui_helpers.dart'; import 'package:url_launcher/url_launcher_string.dart'; -import '../root_route.dart'; +import 'package:selfprivacy/ui/pages/root_route.dart'; const switchableServices = [ ServiceTypes.passwordManager, @@ -32,14 +32,14 @@ const switchableServices = [ ]; class ServicesPage extends StatefulWidget { - const ServicesPage({Key? key}) : super(key: key); + const ServicesPage({final super.key}); @override State createState() => _ServicesPageState(); } -void _launchURL(url) async { - var canLaunch = await canLaunchUrlString(url); +void _launchURL(final url) async { + final canLaunch = await canLaunchUrlString(url); if (canLaunch) { try { @@ -56,8 +56,8 @@ void _launchURL(url) async { class _ServicesPageState extends State { @override - Widget build(BuildContext context) { - var isReady = context.watch().state + Widget build(final BuildContext context) { + final isReady = context.watch().state is ServerInstallationFinished; return Scaffold( @@ -74,12 +74,14 @@ class _ServicesPageState extends State { const SizedBox(height: 24), if (!isReady) ...[const NotReadyCard(), const SizedBox(height: 24)], ...ServiceTypes.values - .map((t) => Padding( - padding: const EdgeInsets.only( - bottom: 30, - ), - child: _Card(serviceType: t), - )) + .map( + (final t) => Padding( + padding: const EdgeInsets.only( + bottom: 30, + ), + child: _Card(serviceType: t), + ), + ) .toList() ], ), @@ -88,31 +90,32 @@ class _ServicesPageState extends State { } class _Card extends StatelessWidget { - const _Card({Key? key, required this.serviceType}) : super(key: key); + const _Card({required this.serviceType}); final ServiceTypes serviceType; @override - Widget build(BuildContext context) { - var isReady = context.watch().state + Widget build(final BuildContext context) { + final isReady = context.watch().state is ServerInstallationFinished; - var changeTab = context.read().onPress; + final changeTab = context.read().onPress; - var serviceState = context.watch().state; - var jobsCubit = context.watch(); - var jobState = jobsCubit.state; + final serviceState = context.watch().state; + final jobsCubit = context.watch(); + final jobState = jobsCubit.state; - var switchableService = switchableServices.contains(serviceType); - var hasSwitchJob = switchableService && + final switchableService = switchableServices.contains(serviceType); + final hasSwitchJob = switchableService && jobState is JobsStateWithJobs && - jobState.jobList - .any((el) => el is ServiceToggleJob && el.type == serviceType); + jobState.jobList.any( + (final el) => el is ServiceToggleJob && el.type == serviceType, + ); - var isSwitchOn = isReady && + final isSwitchOn = isReady && (!switchableServices.contains(serviceType) || serviceState.isEnableByType(serviceType)); - var config = context.watch().state; - var domainName = UiHelpers.getDomainName(config); + final config = context.watch().state; + final domainName = UiHelpers.getDomainName(config); return GestureDetector( onTap: isSwitchOn @@ -120,16 +123,14 @@ class _Card extends StatelessWidget { context: context, // isScrollControlled: true, // backgroundColor: Colors.transparent, - builder: (BuildContext context) { - return _ServiceDetails( - serviceType: serviceType, - status: - isSwitchOn ? StateType.stable : StateType.uninitialized, - title: serviceType.title, - icon: serviceType.icon, - changeTab: changeTab, - ); - }, + builder: (final BuildContext context) => _ServiceDetails( + serviceType: serviceType, + status: + isSwitchOn ? StateType.stable : StateType.uninitialized, + title: serviceType.title, + icon: serviceType.icon, + changeTab: changeTab, + ), ) : null, child: BrandCards.big( @@ -146,12 +147,13 @@ class _Card extends StatelessWidget { if (isReady && switchableService) ...[ const Spacer(), Builder( - builder: (context) { + builder: (final context) { late bool isActive; if (hasSwitchJob) { - isActive = ((jobState).jobList.firstWhere((el) => - el is ServiceToggleJob && - el.type == serviceType) as ServiceToggleJob) + isActive = (jobState.jobList.firstWhere( + (final el) => + el is ServiceToggleJob && el.type == serviceType, + ) as ServiceToggleJob) .needToTurnOn; } else { isActive = serviceState.isEnableByType(serviceType); @@ -159,7 +161,7 @@ class _Card extends StatelessWidget { return BrandSwitch( value: isActive, - onChanged: (value) => + onChanged: (final value) => jobsCubit.createOrRemoveServiceToggleJob( ServiceToggleJob( type: serviceType, @@ -186,7 +188,8 @@ class _Card extends StatelessWidget { children: [ GestureDetector( onTap: () => _launchURL( - 'https://${serviceType.subdomain}.$domainName'), + 'https://${serviceType.subdomain}.$domainName', + ), child: Text( '${serviceType.subdomain}.$domainName', style: TextStyle( @@ -200,16 +203,18 @@ class _Card extends StatelessWidget { ], ), if (serviceType == ServiceTypes.mail) - Column(children: [ - Text( - domainName, - style: TextStyle( - color: Theme.of(context).colorScheme.primary, - decoration: TextDecoration.underline, + Column( + children: [ + Text( + domainName, + style: TextStyle( + color: Theme.of(context).colorScheme.primary, + decoration: TextDecoration.underline, + ), ), - ), - const SizedBox(height: 10), - ]), + const SizedBox(height: 10), + ], + ), BrandText.body2(serviceType.loginInfo), const SizedBox(height: 10), BrandText.body2(serviceType.subtitle), @@ -244,13 +249,12 @@ class _Card extends StatelessWidget { class _ServiceDetails extends StatelessWidget { const _ServiceDetails({ - Key? key, required this.serviceType, required this.icon, required this.status, required this.title, required this.changeTab, - }) : super(key: key); + }); final ServiceTypes serviceType; final IconData icon; @@ -259,13 +263,13 @@ class _ServiceDetails extends StatelessWidget { final ValueChanged changeTab; @override - Widget build(BuildContext context) { + Widget build(final BuildContext context) { late Widget child; - var config = context.watch().state; - var domainName = UiHelpers.getDomainName(config); + final config = context.watch().state; + final domainName = UiHelpers.getDomainName(config); - var linksStyle = body1Style.copyWith( + final linksStyle = body1Style.copyWith( fontSize: 15, color: Theme.of(context).brightness == Brightness.dark ? Colors.white @@ -274,7 +278,7 @@ class _ServiceDetails extends StatelessWidget { decoration: TextDecoration.underline, ); - var textStyle = body1Style.copyWith( + final textStyle = body1Style.copyWith( color: Theme.of(context).brightness == Brightness.dark ? Colors.white : BrandColors.black, @@ -282,163 +286,171 @@ class _ServiceDetails extends StatelessWidget { switch (serviceType) { case ServiceTypes.mail: child = RichText( - text: TextSpan( - children: [ - TextSpan( - text: 'services.mail.bottom_sheet.1'.tr(args: [domainName]), - style: textStyle, - ), - const WidgetSpan(child: SizedBox(width: 5)), - WidgetSpan( - child: Padding( - padding: const EdgeInsets.only(bottom: 0.8), - child: GestureDetector( - child: Text( - 'services.mail.bottom_sheet.2'.tr(), - style: linksStyle, + text: TextSpan( + children: [ + TextSpan( + text: 'services.mail.bottom_sheet.1'.tr(args: [domainName]), + style: textStyle, + ), + const WidgetSpan(child: SizedBox(width: 5)), + WidgetSpan( + child: Padding( + padding: const EdgeInsets.only(bottom: 0.8), + child: GestureDetector( + child: Text( + 'services.mail.bottom_sheet.2'.tr(), + style: linksStyle, + ), + onTap: () { + Navigator.of(context).pop(); + changeTab(2); + }, ), - onTap: () { - Navigator.of(context).pop(); - changeTab(2); - }, ), ), - ), - ], - )); + ], + ), + ); break; case ServiceTypes.messenger: child = RichText( - text: TextSpan( - children: [ - TextSpan( - text: 'services.messenger.bottom_sheet.1'.tr(args: [domainName]), - style: textStyle, - ) - ], - )); + text: TextSpan( + children: [ + TextSpan( + text: + 'services.messenger.bottom_sheet.1'.tr(args: [domainName]), + style: textStyle, + ) + ], + ), + ); break; case ServiceTypes.passwordManager: child = RichText( - text: TextSpan( - children: [ - TextSpan( - text: 'services.password_manager.bottom_sheet.1' - .tr(args: [domainName]), - style: textStyle, - ), - const WidgetSpan(child: SizedBox(width: 5)), - WidgetSpan( - child: Padding( - padding: const EdgeInsets.only(bottom: 0.8), - child: GestureDetector( - onTap: () => _launchURL('https://password.$domainName'), - child: Text( - 'password.$domainName', - style: linksStyle, + text: TextSpan( + children: [ + TextSpan( + text: 'services.password_manager.bottom_sheet.1' + .tr(args: [domainName]), + style: textStyle, + ), + const WidgetSpan(child: SizedBox(width: 5)), + WidgetSpan( + child: Padding( + padding: const EdgeInsets.only(bottom: 0.8), + child: GestureDetector( + onTap: () => _launchURL('https://password.$domainName'), + child: Text( + 'password.$domainName', + style: linksStyle, + ), ), ), ), - ), - ], - )); + ], + ), + ); break; case ServiceTypes.video: child = RichText( - text: TextSpan( - children: [ - TextSpan( - text: 'services.video.bottom_sheet.1'.tr(args: [domainName]), - style: textStyle, - ), - const WidgetSpan(child: SizedBox(width: 5)), - WidgetSpan( - child: Padding( - padding: const EdgeInsets.only(bottom: 0.8), - child: GestureDetector( - onTap: () => _launchURL('https://meet.$domainName'), - child: Text( - 'meet.$domainName', - style: linksStyle, + text: TextSpan( + children: [ + TextSpan( + text: 'services.video.bottom_sheet.1'.tr(args: [domainName]), + style: textStyle, + ), + const WidgetSpan(child: SizedBox(width: 5)), + WidgetSpan( + child: Padding( + padding: const EdgeInsets.only(bottom: 0.8), + child: GestureDetector( + onTap: () => _launchURL('https://meet.$domainName'), + child: Text( + 'meet.$domainName', + style: linksStyle, + ), ), ), ), - ), - ], - )); + ], + ), + ); break; case ServiceTypes.cloud: child = RichText( - text: TextSpan( - children: [ - TextSpan( - text: 'services.cloud.bottom_sheet.1'.tr(args: [domainName]), - style: textStyle, - ), - const WidgetSpan(child: SizedBox(width: 5)), - WidgetSpan( - child: Padding( - padding: const EdgeInsets.only(bottom: 0.8), - child: GestureDetector( - onTap: () => _launchURL('https://cloud.$domainName'), - child: Text( - 'cloud.$domainName', - style: linksStyle, + text: TextSpan( + children: [ + TextSpan( + text: 'services.cloud.bottom_sheet.1'.tr(args: [domainName]), + style: textStyle, + ), + const WidgetSpan(child: SizedBox(width: 5)), + WidgetSpan( + child: Padding( + padding: const EdgeInsets.only(bottom: 0.8), + child: GestureDetector( + onTap: () => _launchURL('https://cloud.$domainName'), + child: Text( + 'cloud.$domainName', + style: linksStyle, + ), ), ), ), - ), - ], - )); + ], + ), + ); break; case ServiceTypes.socialNetwork: child = RichText( - text: TextSpan( - children: [ - TextSpan( - text: 'services.social_network.bottom_sheet.1' - .tr(args: [domainName]), - style: textStyle, - ), - const WidgetSpan(child: SizedBox(width: 5)), - WidgetSpan( - child: Padding( - padding: const EdgeInsets.only(bottom: 0.8), - child: GestureDetector( - onTap: () => _launchURL('https://social.$domainName'), - child: Text( - 'social.$domainName', - style: linksStyle, + text: TextSpan( + children: [ + TextSpan( + text: 'services.social_network.bottom_sheet.1' + .tr(args: [domainName]), + style: textStyle, + ), + const WidgetSpan(child: SizedBox(width: 5)), + WidgetSpan( + child: Padding( + padding: const EdgeInsets.only(bottom: 0.8), + child: GestureDetector( + onTap: () => _launchURL('https://social.$domainName'), + child: Text( + 'social.$domainName', + style: linksStyle, + ), ), ), ), - ), - ], - )); + ], + ), + ); break; case ServiceTypes.git: child = RichText( - text: TextSpan( - children: [ - TextSpan( - text: 'services.git.bottom_sheet.1'.tr(args: [domainName]), - style: textStyle, - ), - const WidgetSpan(child: SizedBox(width: 5)), - WidgetSpan( - child: Padding( - padding: const EdgeInsets.only(bottom: 0.8), - child: GestureDetector( - onTap: () => _launchURL('https://git.$domainName'), - child: Text( - 'git.$domainName', - style: linksStyle, + text: TextSpan( + children: [ + TextSpan( + text: 'services.git.bottom_sheet.1'.tr(args: [domainName]), + style: textStyle, + ), + const WidgetSpan(child: SizedBox(width: 5)), + WidgetSpan( + child: Padding( + padding: const EdgeInsets.only(bottom: 0.8), + child: GestureDetector( + onTap: () => _launchURL('https://git.$domainName'), + child: Text( + 'git.$domainName', + style: linksStyle, + ), ), ), ), - ), - ], - )); + ], + ), + ); break; case ServiceTypes.vpn: child = Text( diff --git a/lib/ui/pages/setup/initializing.dart b/lib/ui/pages/setup/initializing.dart index e3fd0e77..1710113b 100644 --- a/lib/ui/pages/setup/initializing.dart +++ b/lib/ui/pages/setup/initializing.dart @@ -21,16 +21,16 @@ import 'package:selfprivacy/ui/pages/setup/recovering/recovery_routing.dart'; import 'package:selfprivacy/utils/route_transitions/basic.dart'; class InitializingPage extends StatelessWidget { - const InitializingPage({Key? key}) : super(key: key); + const InitializingPage({final super.key}); @override - Widget build(BuildContext context) { - var cubit = context.watch(); + Widget build(final BuildContext context) { + final cubit = context.watch(); if (cubit.state is ServerInstallationRecovery) { return const RecoveryRouting(); } else { - var actualInitializingPage = [ + final actualInitializingPage = [ () => _stepHetzner(cubit), () => _stepCloudflare(cubit), () => _stepBackblaze(cubit), @@ -44,7 +44,7 @@ class InitializingPage extends StatelessWidget { ][cubit.state.progress.index](); return BlocListener( - listener: (context, state) { + listener: (final context, final state) { if (cubit.state is ServerInstallationFinished) { Navigator.of(context) .pushReplacement(materialRoute(const RootPage())); @@ -82,43 +82,48 @@ class InitializingPage extends StatelessWidget { ), ), 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: [ + 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, + ); + }, + ), + ), + if (cubit.state is ServerInstallationFinished) + Container() + else Container( alignment: Alignment.center, child: BrandButton.text( - title: cubit.state is ServerInstallationFinished - ? 'basis.close'.tr() - : 'basis.later'.tr(), + title: 'basis.connect_to_existing'.tr(), onPressed: () { - Navigator.of(context).pushAndRemoveUntil( - materialRoute(const RootPage()), - (predicate) => false, + Navigator.of(context).push( + materialRoute( + const RecoveryRouting(), + ), ); }, ), - ), - (cubit.state is ServerInstallationFinished) - ? Container() - : Container( - alignment: Alignment.center, - child: BrandButton.text( - title: 'basis.connect_to_existing'.tr(), - onPressed: () { - Navigator.of(context).push(materialRoute( - const RecoveryRouting())); - }, - ), - ) - ], - )), + ) + ], + ), + ), ], ), ), @@ -128,324 +133,338 @@ class InitializingPage extends StatelessWidget { } } - Widget _stepHetzner(ServerInstallationCubit serverInstallationCubit) { - return BlocProvider( - create: (context) => HetznerFormCubit(serverInstallationCubit), - child: Builder(builder: (context) { - var formCubitState = context.watch().state; - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Image.asset( - 'assets/images/logos/hetzner.png', - width: 150, - ), - const SizedBox(height: 10), - BrandText.h2('initializing.1'.tr()), - const SizedBox(height: 10), - BrandText.body2('initializing.2'.tr()), - const Spacer(), - CubitFormTextField( - formFieldCubit: context.read().apiKey, - textAlign: TextAlign.center, - scrollPadding: const EdgeInsets.only(bottom: 70), - decoration: const InputDecoration( - hintText: 'Hetzner API Token', - ), - ), - const Spacer(), - 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_hetzner')), - title: 'initializing.how'.tr(), - ), - ], - ); - }), - ); - } + Widget _stepHetzner(final ServerInstallationCubit serverInstallationCubit) => + BlocProvider( + create: (final context) => HetznerFormCubit(serverInstallationCubit), + child: Builder( + builder: (final context) { + final formCubitState = context.watch().state; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Image.asset( + 'assets/images/logos/hetzner.png', + width: 150, + ), + const SizedBox(height: 10), + BrandText.h2('initializing.1'.tr()), + const SizedBox(height: 10), + BrandText.body2('initializing.2'.tr()), + const Spacer(), + CubitFormTextField( + formFieldCubit: context.read().apiKey, + textAlign: TextAlign.center, + scrollPadding: const EdgeInsets.only(bottom: 70), + decoration: const InputDecoration( + hintText: 'Hetzner API Token', + ), + ), + const Spacer(), + 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_hetzner'), + ), + title: 'initializing.how'.tr(), + ), + ], + ); + }, + ), + ); - void _showModal(BuildContext context, Widget widget) { + void _showModal(final BuildContext context, final Widget widget) { showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, - builder: (BuildContext context) { - return widget; - }, + builder: (final BuildContext context) => widget, ); } - Widget _stepCloudflare(ServerInstallationCubit initializingCubit) { - return BlocProvider( - create: (context) => CloudFlareFormCubit(initializingCubit), - child: Builder(builder: (context) { - var formCubitState = context.watch().state; + Widget _stepCloudflare(final ServerInstallationCubit initializingCubit) => + BlocProvider( + create: (final context) => CloudFlareFormCubit(initializingCubit), + child: Builder( + builder: (final context) { + final formCubitState = context.watch().state; - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Image.asset( - 'assets/images/logos/cloudflare.png', - width: 150, - ), - const SizedBox(height: 10), - BrandText.h2('initializing.3'.tr()), - const SizedBox(height: 10), - BrandText.body2('initializing.4'.tr()), - const Spacer(), - CubitFormTextField( - formFieldCubit: context.read().apiKey, - textAlign: TextAlign.center, - scrollPadding: const EdgeInsets.only(bottom: 70), - decoration: InputDecoration( - hintText: 'initializing.5'.tr(), - ), - ), - const Spacer(), - 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_cloudflare', - )), - title: 'initializing.how'.tr(), - ), - ], - ); - }), - ); - } - - Widget _stepBackblaze(ServerInstallationCubit initializingCubit) { - return BlocProvider( - create: (context) => BackblazeFormCubit(initializingCubit), - child: Builder(builder: (context) { - var formCubitState = context.watch().state; - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Image.asset( - 'assets/images/logos/backblaze.png', - height: 50, - ), - const SizedBox(height: 10), - BrandText.h2('initializing.6'.tr()), - const SizedBox(height: 10), - const Spacer(), - CubitFormTextField( - formFieldCubit: context.read().keyId, - textAlign: TextAlign.center, - scrollPadding: const EdgeInsets.only(bottom: 70), - decoration: const InputDecoration( - hintText: 'KeyID', - ), - ), - const Spacer(), - CubitFormTextField( - formFieldCubit: context.read().applicationKey, - textAlign: TextAlign.center, - scrollPadding: const EdgeInsets.only(bottom: 70), - decoration: const InputDecoration( - hintText: 'Master Application Key', - ), - ), - const Spacer(), - 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', - )), - title: 'initializing.how'.tr(), - ), - ], - ); - }), - ); - } - - Widget _stepDomain(ServerInstallationCubit initializingCubit) { - return BlocProvider( - create: (context) => DomainSetupCubit(initializingCubit)..load(), - child: Builder(builder: (context) { - DomainSetupState state = context.watch().state; - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Image.asset( - 'assets/images/logos/cloudflare.png', - width: 150, - ), - const SizedBox(height: 30), - BrandText.h2('basis.domain'.tr()), - const SizedBox(height: 10), - if (state is Empty) BrandText.body2('initializing.7'.tr()), - if (state is Loading) - BrandText.body2( - state.type == LoadingTypes.loadingDomain - ? 'initializing.8'.tr() - : 'basis.saving'.tr(), - ), - if (state is MoreThenOne) - BrandText.body2( - 'initializing.9'.tr(), - ), - if (state is Loaded) ...[ - const SizedBox(height: 10), - Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.end, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Expanded( - child: BrandText.h3( - state.domain, - textAlign: TextAlign.center, - ), - ), - SizedBox( - width: 56, - child: BrandButton.rised( - onPressed: () => context.read().load(), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: const [ - Icon( - Icons.refresh, - color: Colors.white, - ), - ], - ), - ), - ), - ], - ) - ], - if (state is Empty) ...[ - const SizedBox(height: 30), - BrandButton.rised( - onPressed: () => context.read().load(), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon( - Icons.refresh, - color: Colors.white, - ), - const SizedBox(width: 10), - BrandText.buttonTitleText('Обновить cписок'), - ], + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Image.asset( + 'assets/images/logos/cloudflare.png', + width: 150, ), - ), - ], - if (state is Loaded) ...[ - const SizedBox(height: 30), - BrandButton.rised( - onPressed: () => context.read().saveDomain(), - text: 'initializing.10'.tr(), - ), - ], - const SizedBox( - height: 10, - width: double.infinity, - ), - ], - ); - }), - ); - } - - Widget _stepUser(ServerInstallationCubit initializingCubit) { - return BlocProvider( - create: (context) => - RootUserFormCubit(initializingCubit, FieldCubitFactory(context)), - child: Builder(builder: (context) { - var formCubitState = context.watch().state; - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - BrandText.h2('initializing.22'.tr()), - const SizedBox(height: 10), - BrandText.body2('initializing.23'.tr()), - const Spacer(), - CubitFormTextField( - formFieldCubit: context.read().userName, - textAlign: TextAlign.center, - scrollPadding: const EdgeInsets.only(bottom: 70), - decoration: InputDecoration( - hintText: 'basis.nickname'.tr(), - ), - ), - const SizedBox(height: 10), - BlocBuilder, FieldCubitState>( - bloc: context.read().isVisible, - builder: (context, state) { - var isVisible = state.value; - return CubitFormTextField( - obscureText: !isVisible, - formFieldCubit: context.read().password, + const SizedBox(height: 10), + BrandText.h2('initializing.3'.tr()), + const SizedBox(height: 10), + BrandText.body2('initializing.4'.tr()), + const Spacer(), + CubitFormTextField( + formFieldCubit: context.read().apiKey, 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), - ), - suffixIconConstraints: const BoxConstraints(minWidth: 60), - prefixIconConstraints: const BoxConstraints(maxWidth: 60), - prefixIcon: Container(), + hintText: 'initializing.5'.tr(), ), - ); - }, - ), - const Spacer(), - BrandButton.rised( - onPressed: formCubitState.isSubmitting - ? null - : () => context.read().trySubmit(), - text: 'basis.connect'.tr(), - ), - ], - ); - }), - ); - } + ), + const Spacer(), + 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_cloudflare', + ), + ), + title: 'initializing.how'.tr(), + ), + ], + ); + }, + ), + ); - Widget _stepServer(ServerInstallationCubit appConfigCubit) { - var isLoading = + Widget _stepBackblaze(final ServerInstallationCubit initializingCubit) => + BlocProvider( + create: (final context) => BackblazeFormCubit(initializingCubit), + child: Builder( + builder: (final context) { + final formCubitState = context.watch().state; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Image.asset( + 'assets/images/logos/backblaze.png', + height: 50, + ), + const SizedBox(height: 10), + BrandText.h2('initializing.6'.tr()), + const SizedBox(height: 10), + const Spacer(), + CubitFormTextField( + formFieldCubit: context.read().keyId, + textAlign: TextAlign.center, + scrollPadding: const EdgeInsets.only(bottom: 70), + decoration: const InputDecoration( + hintText: 'KeyID', + ), + ), + const Spacer(), + CubitFormTextField( + formFieldCubit: + context.read().applicationKey, + textAlign: TextAlign.center, + scrollPadding: const EdgeInsets.only(bottom: 70), + decoration: const InputDecoration( + hintText: 'Master Application Key', + ), + ), + const Spacer(), + 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', + ), + ), + title: 'initializing.how'.tr(), + ), + ], + ); + }, + ), + ); + + Widget _stepDomain(final ServerInstallationCubit initializingCubit) => + BlocProvider( + create: (final context) => DomainSetupCubit(initializingCubit)..load(), + child: Builder( + builder: (final context) { + final DomainSetupState state = + context.watch().state; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Image.asset( + 'assets/images/logos/cloudflare.png', + width: 150, + ), + const SizedBox(height: 30), + BrandText.h2('basis.domain'.tr()), + const SizedBox(height: 10), + if (state is Empty) BrandText.body2('initializing.7'.tr()), + if (state is Loading) + BrandText.body2( + state.type == LoadingTypes.loadingDomain + ? 'initializing.8'.tr() + : 'basis.saving'.tr(), + ), + if (state is MoreThenOne) + BrandText.body2( + 'initializing.9'.tr(), + ), + if (state is Loaded) ...[ + const SizedBox(height: 10), + Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: BrandText.h3( + state.domain, + textAlign: TextAlign.center, + ), + ), + SizedBox( + width: 56, + child: BrandButton.rised( + onPressed: () => + context.read().load(), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: const [ + Icon( + Icons.refresh, + color: Colors.white, + ), + ], + ), + ), + ), + ], + ) + ], + if (state is Empty) ...[ + const SizedBox(height: 30), + BrandButton.rised( + onPressed: () => context.read().load(), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + Icons.refresh, + color: Colors.white, + ), + const SizedBox(width: 10), + BrandText.buttonTitleText('Обновить cписок'), + ], + ), + ), + ], + if (state is Loaded) ...[ + const SizedBox(height: 30), + BrandButton.rised( + onPressed: () => + context.read().saveDomain(), + text: 'initializing.10'.tr(), + ), + ], + const SizedBox( + height: 10, + width: double.infinity, + ), + ], + ); + }, + ), + ); + + Widget _stepUser(final ServerInstallationCubit initializingCubit) => + BlocProvider( + create: (final context) => + RootUserFormCubit(initializingCubit, FieldCubitFactory(context)), + child: Builder( + builder: (final context) { + final formCubitState = context.watch().state; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + BrandText.h2('initializing.22'.tr()), + const SizedBox(height: 10), + BrandText.body2('initializing.23'.tr()), + const Spacer(), + CubitFormTextField( + formFieldCubit: context.read().userName, + textAlign: TextAlign.center, + scrollPadding: const EdgeInsets.only(bottom: 70), + decoration: InputDecoration( + hintText: 'basis.nickname'.tr(), + ), + ), + const SizedBox(height: 10), + 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), + ), + suffixIconConstraints: + const BoxConstraints(minWidth: 60), + prefixIconConstraints: + const BoxConstraints(maxWidth: 60), + prefixIcon: Container(), + ), + ); + }, + ), + const Spacer(), + BrandButton.rised( + onPressed: formCubitState.isSubmitting + ? null + : () => context.read().trySubmit(), + text: 'basis.connect'.tr(), + ), + ], + ); + }, + ), + ); + + Widget _stepServer(final ServerInstallationCubit appConfigCubit) { + final bool isLoading = (appConfigCubit.state as ServerInstallationNotFinished).isLoading; - return Builder(builder: (context) { - return Column( + return Builder( + builder: (final context) => Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Spacer(flex: 2), @@ -454,20 +473,21 @@ class InitializingPage extends StatelessWidget { BrandText.body2('initializing.11'.tr()), const Spacer(), BrandButton.rised( - onPressed: isLoading - ? null - : () => appConfigCubit.createServerAndSetDnsRecords(), + onPressed: + isLoading ? null : appConfigCubit.createServerAndSetDnsRecords, text: isLoading ? 'basis.loading'.tr() : 'initializing.11'.tr(), ), ], - ); - }); + ), + ); } - Widget _stepCheck(ServerInstallationCubit appConfigCubit) { + Widget _stepCheck(final ServerInstallationCubit appConfigCubit) { assert( - appConfigCubit.state is ServerInstallationNotFinished, 'wrong state'); - var state = appConfigCubit.state as TimerState; + appConfigCubit.state is ServerInstallationNotFinished, + 'wrong state', + ); + final state = appConfigCubit.state as TimerState; late int doneCount; late String? text; if (state.isServerResetedSecondTime) { @@ -483,8 +503,8 @@ class InitializingPage extends StatelessWidget { text = 'initializing.15'.tr(); doneCount = 0; } - return Builder(builder: (context) { - return Column( + return Builder( + builder: (final context) => Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 15), @@ -497,9 +517,9 @@ class InitializingPage extends StatelessWidget { const SizedBox(height: 10), if (doneCount == 0 && state.dnsMatches != null) Column( - children: state.dnsMatches!.entries.map((entry) { - var domain = entry.key; - var isCorrect = entry.value; + 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), @@ -524,37 +544,32 @@ class InitializingPage extends StatelessWidget { ), if (state.isLoading) BrandText.body2('initializing.17'.tr()), ], - ); - }); - } - - Widget _addCard(Widget child) { - return Container( - height: 450, - padding: paddingH15V0, - child: BrandCards.big(child: child), + ), ); } + + Widget _addCard(final Widget child) => Container( + height: 450, + padding: paddingH15V0, + child: BrandCards.big(child: child), + ); } class _HowTo extends StatelessWidget { const _HowTo({ - Key? key, required this.fileName, - }) : super(key: key); + }); final String fileName; @override - Widget build(BuildContext context) { - return BrandBottomSheet( - isExpended: true, - child: Padding( - padding: paddingH15V0, - child: BrandMarkdown( - fileName: fileName, + Widget build(final BuildContext context) => BrandBottomSheet( + isExpended: true, + child: Padding( + padding: paddingH15V0, + child: BrandMarkdown( + fileName: fileName, + ), ), - ), - ); - } + ); } 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 1c02f34d..d9fb3952 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,5 +1,3 @@ -// ignore_for_file: always_specify_types - 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'; @@ -11,51 +9,52 @@ import 'package:selfprivacy/logic/cubit/server_installation/server_installation_ import 'package:selfprivacy/logic/cubit/forms/factories/field_cubit_factory.dart'; class RecoverByNewDeviceKeyInstruction extends StatelessWidget { - const RecoverByNewDeviceKeyInstruction({Key? key}) : super(key: key); + const RecoverByNewDeviceKeyInstruction({final super.key}); @override - Widget build(BuildContext context) { - return BrandHeroScreen( - heroTitle: 'recovering.recovery_main_header'.tr(), - heroSubtitle: 'recovering.method_device_description'.tr(), - hasBackButton: true, - hasFlashButton: false, - onBackButtonPressed: () => - context.read().revertRecoveryStep(), - children: [ - FilledButton( - title: 'recovering.method_device_button'.tr(), - onPressed: () => Navigator.of(context) - .push(materialRoute(const RecoverByNewDeviceKeyInput())), - ) - ], - ); - } + Widget build(final BuildContext context) => BrandHeroScreen( + heroTitle: 'recovering.recovery_main_header'.tr(), + heroSubtitle: 'recovering.method_device_description'.tr(), + hasBackButton: true, + hasFlashButton: false, + onBackButtonPressed: () => + context.read().revertRecoveryStep(), + children: [ + FilledButton( + title: 'recovering.method_device_button'.tr(), + onPressed: () => Navigator.of(context) + .push(materialRoute(const RecoverByNewDeviceKeyInput())), + ) + ], + ); } class RecoverByNewDeviceKeyInput extends StatelessWidget { - const RecoverByNewDeviceKeyInput({Key? key}) : super(key: key); + const RecoverByNewDeviceKeyInput({final super.key}); @override - Widget build(BuildContext context) { - ServerInstallationCubit appConfig = context.watch(); + Widget build(final BuildContext context) { + final ServerInstallationCubit appConfig = + context.watch(); return BlocProvider( - create: (BuildContext context) => RecoveryDeviceFormCubit( + create: (final BuildContext context) => RecoveryDeviceFormCubit( appConfig, FieldCubitFactory(context), ServerRecoveryMethods.newDeviceKey, ), child: BlocListener( - listener: (BuildContext context, ServerInstallationState state) { + listener: + (final BuildContext context, final ServerInstallationState state) { if (state is ServerInstallationRecovery && state.currentStep != RecoveryStep.newDeviceKey) { Navigator.of(context).pop(); } }, child: Builder( - builder: (BuildContext context) { - FormCubitState formCubitState = context.watch().state; + builder: (final BuildContext context) { + final FormCubitState formCubitState = + context.watch().state; return BrandHeroScreen( heroTitle: 'recovering.recovery_main_header'.tr(), 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 fa96c04e..6d3831f9 100644 --- a/lib/ui/pages/setup/recovering/recover_by_old_token.dart +++ b/lib/ui/pages/setup/recovering/recover_by_old_token.dart @@ -1,5 +1,3 @@ -// ignore_for_file: always_specify_types - 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'; @@ -12,47 +10,51 @@ import 'package:selfprivacy/logic/cubit/forms/factories/field_cubit_factory.dart class RecoverByOldTokenInstruction extends StatelessWidget { @override - const RecoverByOldTokenInstruction( - {super.key, required this.instructionFilename,}); + const RecoverByOldTokenInstruction({ + required this.instructionFilename, + final super.key, + }); @override - Widget build(final BuildContext context) => BlocListener( - listener: (final context, final state) { - if (state is ServerInstallationRecovery && - state.currentStep != RecoveryStep.selecting) { - Navigator.of(context).pop(); - } - }, - child: BrandHeroScreen( - heroTitle: 'recovering.recovery_main_header'.tr(), - hasBackButton: true, - hasFlashButton: false, - onBackButtonPressed: () => - context.read().revertRecoveryStep(), - children: [ - BrandMarkdown( - fileName: instructionFilename, - ), - const SizedBox(height: 16), - FilledButton( - title: 'recovering.method_device_button'.tr(), - onPressed: () => context - .read() - .selectRecoveryMethod(ServerRecoveryMethods.oldToken), - ) - ], - ), - ); + Widget build(final BuildContext context) => + BlocListener( + listener: (final context, final state) { + if (state is ServerInstallationRecovery && + state.currentStep != RecoveryStep.selecting) { + Navigator.of(context).pop(); + } + }, + child: BrandHeroScreen( + heroTitle: 'recovering.recovery_main_header'.tr(), + hasBackButton: true, + hasFlashButton: false, + onBackButtonPressed: () => + context.read().revertRecoveryStep(), + children: [ + BrandMarkdown( + fileName: instructionFilename, + ), + const SizedBox(height: 16), + FilledButton( + title: 'recovering.method_device_button'.tr(), + onPressed: () => context + .read() + .selectRecoveryMethod(ServerRecoveryMethods.oldToken), + ) + ], + ), + ); final String instructionFilename; } class RecoverByOldToken extends StatelessWidget { - const RecoverByOldToken({super.key}); + const RecoverByOldToken({final super.key}); @override Widget build(final BuildContext context) { - final ServerInstallationCubit appConfig = context.watch(); + final ServerInstallationCubit appConfig = + context.watch(); return BlocProvider( create: (final context) => RecoveryDeviceFormCubit( @@ -62,7 +64,8 @@ class RecoverByOldToken extends StatelessWidget { ), child: Builder( builder: (final context) { - final FormCubitState formCubitState = context.watch().state; + final FormCubitState formCubitState = + context.watch().state; return BrandHeroScreen( heroTitle: 'recovering.recovery_main_header'.tr(), 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 51a930a8..a6cc44cd 100644 --- a/lib/ui/pages/setup/recovering/recover_by_recovery_key.dart +++ b/lib/ui/pages/setup/recovering/recover_by_recovery_key.dart @@ -8,11 +8,12 @@ import 'package:selfprivacy/ui/components/brand_button/filled_button.dart'; import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; class RecoverByRecoveryKey extends StatelessWidget { - const RecoverByRecoveryKey({final Key? key}) : super(key: key); + const RecoverByRecoveryKey({final super.key}); @override Widget build(final BuildContext context) { - ServerInstallationCubit appConfig = context.watch(); + final ServerInstallationCubit appConfig = + context.watch(); return BlocProvider( create: (final context) => RecoveryDeviceFormCubit( @@ -22,7 +23,8 @@ class RecoverByRecoveryKey extends StatelessWidget { ), child: Builder( builder: (final context) { - FormCubitState formCubitState = context.watch().state; + final FormCubitState formCubitState = + context.watch().state; return BrandHeroScreen( heroTitle: 'recovering.recovery_main_header'.tr(), diff --git a/lib/ui/pages/setup/recovering/recovery_confirm_backblaze.dart b/lib/ui/pages/setup/recovering/recovery_confirm_backblaze.dart index d14955d7..2513b054 100644 --- a/lib/ui/pages/setup/recovering/recovery_confirm_backblaze.dart +++ b/lib/ui/pages/setup/recovering/recovery_confirm_backblaze.dart @@ -14,48 +14,53 @@ class RecoveryConfirmBackblaze extends StatelessWidget { @override Widget build(final BuildContext context) { - ServerInstallationCubit appConfig = context.watch(); + final ServerInstallationCubit appConfig = + context.watch(); return BlocProvider( create: (final BuildContext context) => BackblazeFormCubit(appConfig), - child: Builder(builder: (final BuildContext context) { - FormCubitState formCubitState = context.watch().state; + child: Builder( + builder: (final BuildContext context) { + final FormCubitState formCubitState = + context.watch().state; - return BrandHeroScreen( - heroTitle: 'recovering.confirm_backblaze'.tr(), - heroSubtitle: 'recovering.confirm_backblaze_description'.tr(), - hasBackButton: true, - hasFlashButton: false, - children: [ - CubitFormTextField( - formFieldCubit: context.read().keyId, - decoration: const InputDecoration( - border: OutlineInputBorder(), - labelText: 'KeyID', + return BrandHeroScreen( + heroTitle: 'recovering.confirm_backblaze'.tr(), + heroSubtitle: 'recovering.confirm_backblaze_description'.tr(), + hasBackButton: true, + hasFlashButton: false, + children: [ + CubitFormTextField( + formFieldCubit: context.read().keyId, + decoration: const InputDecoration( + border: OutlineInputBorder(), + labelText: 'KeyID', + ), ), - ), - const SizedBox(height: 16), - CubitFormTextField( - formFieldCubit: context.read().applicationKey, - decoration: const InputDecoration( - border: OutlineInputBorder(), - labelText: 'Master Application Key', + const SizedBox(height: 16), + CubitFormTextField( + formFieldCubit: + context.read().applicationKey, + decoration: const InputDecoration( + border: OutlineInputBorder(), + labelText: 'Master Application Key', + ), ), - ), - const SizedBox(height: 16), - BrandButton.rised( - onPressed: formCubitState.isSubmitting - ? null - : () => context.read().trySubmit(), - text: 'basis.connect'.tr(), - ), - const SizedBox(height: 16), - BrandButton.text( - onPressed: () => showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (final BuildContext context) => const BrandBottomSheet( + const SizedBox(height: 16), + BrandButton.rised( + onPressed: formCubitState.isSubmitting + ? null + : () => context.read().trySubmit(), + text: 'basis.connect'.tr(), + ), + const SizedBox(height: 16), + BrandButton.text( + onPressed: () => showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (final BuildContext context) => + const BrandBottomSheet( isExpended: true, child: Padding( padding: paddingH15V0, @@ -64,12 +69,13 @@ class RecoveryConfirmBackblaze extends StatelessWidget { ), ), ), + ), + 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 1de939b1..b64ca6ae 100644 --- a/lib/ui/pages/setup/recovering/recovery_confirm_cloudflare.dart +++ b/lib/ui/pages/setup/recovering/recovery_confirm_cloudflare.dart @@ -1,5 +1,3 @@ -// ignore_for_file: always_specify_types - import 'package:cubit_form/cubit_form.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; @@ -12,46 +10,50 @@ import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.da import 'package:selfprivacy/ui/components/brand_md/brand_md.dart'; class RecoveryConfirmCloudflare extends StatelessWidget { - const RecoveryConfirmCloudflare({super.key}); + const RecoveryConfirmCloudflare({final super.key}); @override Widget build(final BuildContext context) { - final ServerInstallationCubit appConfig = context.watch(); + final ServerInstallationCubit appConfig = + context.watch(); return BlocProvider( create: (final BuildContext context) => CloudFlareFormCubit(appConfig), - child: Builder(builder: (final BuildContext context) { - final FormCubitState formCubitState = context.watch().state; + child: Builder( + builder: (final BuildContext context) { + final FormCubitState formCubitState = + context.watch().state; - return BrandHeroScreen( - heroTitle: 'recovering.confirm_cloudflare'.tr(), - heroSubtitle: 'recovering.confirm_cloudflare_description'.tr(args: [ - appConfig.state.serverDomain?.domainName ?? 'your domain' - ],), - hasBackButton: true, - hasFlashButton: false, - children: [ - CubitFormTextField( - formFieldCubit: context.read().apiKey, - decoration: InputDecoration( - border: const OutlineInputBorder(), - labelText: 'initializing.5'.tr(), + return BrandHeroScreen( + heroTitle: 'recovering.confirm_cloudflare'.tr(), + heroSubtitle: 'recovering.confirm_cloudflare_description'.tr( + args: [appConfig.state.serverDomain?.domainName ?? 'your domain'], + ), + hasBackButton: true, + hasFlashButton: false, + children: [ + CubitFormTextField( + formFieldCubit: context.read().apiKey, + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: 'initializing.5'.tr(), + ), ), - ), - const SizedBox(height: 16), - BrandButton.rised( - onPressed: formCubitState.isSubmitting - ? null - : () => context.read().trySubmit(), - text: 'basis.connect'.tr(), - ), - const SizedBox(height: 16), - BrandButton.text( - onPressed: () => showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (final BuildContext context) => const BrandBottomSheet( + const SizedBox(height: 16), + BrandButton.rised( + onPressed: formCubitState.isSubmitting + ? null + : () => context.read().trySubmit(), + text: 'basis.connect'.tr(), + ), + const SizedBox(height: 16), + BrandButton.text( + onPressed: () => showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (final BuildContext context) => + const BrandBottomSheet( isExpended: true, child: Padding( padding: paddingH15V0, @@ -60,12 +62,13 @@ class RecoveryConfirmCloudflare extends StatelessWidget { ), ), ), + ), + 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 fd7658ab..110425ef 100644 --- a/lib/ui/pages/setup/recovering/recovery_confirm_server.dart +++ b/lib/ui/pages/setup/recovering/recovery_confirm_server.dart @@ -8,7 +8,7 @@ import 'package:selfprivacy/ui/components/brand_cards/brand_cards.dart'; import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; class RecoveryConfirmServer extends StatefulWidget { - const RecoveryConfirmServer({super.key}); + const RecoveryConfirmServer({final super.key}); @override State createState() => _RecoveryConfirmServerState(); @@ -17,252 +17,252 @@ class RecoveryConfirmServer extends StatefulWidget { class _RecoveryConfirmServerState extends State { bool _isExtended = false; - bool _isServerFound(List servers) { - return servers - .where((server) => server.isIpValid && server.isReverseDnsValid) - .length == - 1; - } + bool _isServerFound(final List servers) => + servers + .where((final server) => server.isIpValid && server.isReverseDnsValid) + .length == + 1; ServerBasicInfoWithValidators _firstValidServer( - List servers) { - return servers - .where((server) => server.isIpValid && server.isReverseDnsValid) - .first; - } + final List servers, + ) => + servers + .where((final server) => server.isIpValid && server.isReverseDnsValid) + .first; @override - Widget build(BuildContext context) { - return BrandHeroScreen( - heroTitle: _isExtended - ? 'recovering.choose_server'.tr() - : 'recovering.confirm_server'.tr(), - heroSubtitle: _isExtended - ? 'recovering.choose_server_description'.tr() - : 'recovering.confirm_server_description'.tr(), - hasBackButton: true, - hasFlashButton: false, - children: [ - FutureBuilder>( - future: context - .read() - .getServersOnHetznerAccount(), - builder: (context, snapshot) { - if (snapshot.hasData) { - final servers = snapshot.data; - return Column( - children: [ - if (servers != null && servers.isNotEmpty) - Column( - children: [ - if (servers.length == 1 || - (!_isExtended && _isServerFound(servers))) - confirmServer(context, _firstValidServer(servers), - servers.length > 1), - if (servers.length > 1 && - (_isExtended || !_isServerFound(servers))) - chooseServer(context, servers), - ], - ), - if (servers?.isEmpty ?? true) - Center( - child: Text( - 'recovering.no_servers'.tr(), - style: Theme.of(context).textTheme.headline6, + Widget build(final BuildContext context) => BrandHeroScreen( + heroTitle: _isExtended + ? 'recovering.choose_server'.tr() + : 'recovering.confirm_server'.tr(), + heroSubtitle: _isExtended + ? 'recovering.choose_server_description'.tr() + : 'recovering.confirm_server_description'.tr(), + hasBackButton: true, + hasFlashButton: false, + children: [ + FutureBuilder>( + future: context + .read() + .getServersOnHetznerAccount(), + builder: (final context, final snapshot) { + if (snapshot.hasData) { + final servers = snapshot.data; + return Column( + children: [ + if (servers != null && servers.isNotEmpty) + Column( + children: [ + if (servers.length == 1 || + (!_isExtended && _isServerFound(servers))) + confirmServer( + context, + _firstValidServer(servers), + servers.length > 1, + ), + if (servers.length > 1 && + (_isExtended || !_isServerFound(servers))) + chooseServer(context, servers), + ], ), - ), - ], - ); - } else { - return const Center( - child: CircularProgressIndicator(), - ); - } - }, - ) - ], - ); - } + if (servers?.isEmpty ?? true) + Center( + child: Text( + 'recovering.no_servers'.tr(), + style: Theme.of(context).textTheme.headline6, + ), + ), + ], + ); + } else { + return const Center( + child: CircularProgressIndicator(), + ); + } + }, + ) + ], + ); Widget confirmServer( - BuildContext context, - ServerBasicInfoWithValidators server, - bool showMoreServersButton, - ) { - return Column( - children: [ - serverCard( - context: context, - server: server, - ), - const SizedBox(height: 16), - FilledButton( - title: 'recovering.confirm_server_accept'.tr(), - onPressed: () => _showConfirmationDialog(context, server), - ), - const SizedBox(height: 16), - if (showMoreServersButton) - BrandButton.text( - title: 'recovering.confirm_server_decline'.tr(), - onPressed: () => setState(() => _isExtended = true), + final BuildContext context, + final ServerBasicInfoWithValidators server, + final bool showMoreServersButton, + ) => + Column( + children: [ + serverCard( + context: context, + server: server, ), - ], - ); - } + const SizedBox(height: 16), + FilledButton( + title: 'recovering.confirm_server_accept'.tr(), + onPressed: () => _showConfirmationDialog(context, server), + ), + const SizedBox(height: 16), + if (showMoreServersButton) + BrandButton.text( + title: 'recovering.confirm_server_decline'.tr(), + onPressed: () => setState(() => _isExtended = true), + ), + ], + ); Widget chooseServer( - BuildContext context, List servers) { - return Column( - children: [ - for (final server in servers) - Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: serverCard( - context: context, - server: server, - onTap: () => _showConfirmationDialog(context, server), - ), - ), - ], - ); - } - - Widget serverCard( - {required BuildContext context, - required ServerBasicInfoWithValidators server, - VoidCallback? onTap}) { - return BrandCards.filled( - child: ListTile( - contentPadding: - const EdgeInsets.symmetric(horizontal: 16, vertical: 16), - onTap: onTap, - title: Text( - server.name, - style: Theme.of(context).textTheme.titleMedium!.copyWith( - color: Theme.of(context).colorScheme.onSurface, + final BuildContext context, + final List servers, + ) => + Column( + children: [ + for (final server in servers) + Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: serverCard( + context: context, + server: server, + onTap: () => _showConfirmationDialog(context, server), ), - ), - leading: Icon( - Icons.dns_outlined, - color: Theme.of(context).colorScheme.onSurface, - ), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon( - server.isReverseDnsValid ? Icons.check : Icons.close, - color: Theme.of(context).colorScheme.onSurface, - ), - const SizedBox(width: 8), - Expanded( - child: Text( - 'rDNS: ${server.reverseDns}', - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - color: Theme.of(context).colorScheme.onSurface, - ), - ), - ), - ], ), - Row( - children: [ - Icon( - server.isIpValid ? Icons.check : Icons.close, + ], + ); + + Widget serverCard({ + required final BuildContext context, + required final ServerBasicInfoWithValidators server, + final VoidCallback? onTap, + }) => + BrandCards.filled( + child: ListTile( + contentPadding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 16), + onTap: onTap, + title: Text( + server.name, + style: Theme.of(context).textTheme.titleMedium!.copyWith( color: Theme.of(context).colorScheme.onSurface, ), - const SizedBox(width: 8), - Expanded( - child: Text( - 'IP: ${server.ip}', - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - color: Theme.of(context).colorScheme.onSurface, - ), + ), + leading: Icon( + Icons.dns_outlined, + color: Theme.of(context).colorScheme.onSurface, + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + server.isReverseDnsValid ? Icons.check : Icons.close, + color: Theme.of(context).colorScheme.onSurface, ), - ), - ], + const SizedBox(width: 8), + Expanded( + child: Text( + 'rDNS: ${server.reverseDns}', + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: Theme.of(context).colorScheme.onSurface, + ), + ), + ), + ], + ), + Row( + children: [ + Icon( + server.isIpValid ? Icons.check : Icons.close, + color: Theme.of(context).colorScheme.onSurface, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + 'IP: ${server.ip}', + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: Theme.of(context).colorScheme.onSurface, + ), + ), + ), + ], + ), + ], + ), + ), + ); + + Future _showConfirmationDialog( + final BuildContext context, + final ServerBasicInfoWithValidators server, + ) => + showDialog( + context: context, + builder: (final context) => AlertDialog( + title: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Icon(Icons.warning_amber_outlined), + const SizedBox(height: 16), + Text( + 'recovering.modal_confirmation_title'.tr(), + style: Theme.of(context).textTheme.headlineSmall, + textAlign: TextAlign.center, + ), + ], + ), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'recovering.modal_confirmation_description'.tr(), + style: Theme.of(context).textTheme.bodyMedium, + ), + const SizedBox(height: 12), + const Divider(), + const SizedBox(height: 12), + Text( + server.name, + style: Theme.of(context).textTheme.titleMedium, + textAlign: TextAlign.start, + ), + const SizedBox(height: 8), + IsValidStringDisplay( + isValid: server.isReverseDnsValid, + textIfValid: 'recovering.modal_confirmation_dns_valid'.tr(), + textIfInvalid: 'recovering.modal_confirmation_dns_invalid'.tr(), + ), + const SizedBox(height: 8), + IsValidStringDisplay( + isValid: server.isIpValid, + textIfValid: 'recovering.modal_confirmation_ip_valid'.tr(), + textIfInvalid: 'recovering.modal_confirmation_ip_invalid'.tr(), + ), + ], + ), + actions: [ + TextButton( + child: Text('modals.no'.tr()), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + TextButton( + child: Text('modals.yes'.tr()), + onPressed: () { + context.read().setServerId(server); + Navigator.of(context).pop(); + }, ), ], ), - ), - ); - } - - _showConfirmationDialog( - BuildContext context, ServerBasicInfoWithValidators server) => - showDialog( - context: context, - builder: (context) { - return AlertDialog( - title: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Icon(Icons.warning_amber_outlined), - const SizedBox(height: 16), - Text( - 'recovering.modal_confirmation_title'.tr(), - style: Theme.of(context).textTheme.headlineSmall, - textAlign: TextAlign.center, - ), - ], - ), - content: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('recovering.modal_confirmation_description'.tr(), - style: Theme.of(context).textTheme.bodyMedium), - const SizedBox(height: 12), - const Divider(), - const SizedBox(height: 12), - Text( - server.name, - style: Theme.of(context).textTheme.titleMedium, - textAlign: TextAlign.start, - ), - const SizedBox(height: 8), - IsValidStringDisplay( - isValid: server.isReverseDnsValid, - textIfValid: 'recovering.modal_confirmation_dns_valid'.tr(), - textIfInvalid: - 'recovering.modal_confirmation_dns_invalid'.tr(), - ), - const SizedBox(height: 8), - IsValidStringDisplay( - isValid: server.isIpValid, - textIfValid: 'recovering.modal_confirmation_ip_valid'.tr(), - textIfInvalid: - 'recovering.modal_confirmation_ip_invalid'.tr(), - ), - ], - ), - actions: [ - TextButton( - child: Text('modals.no'.tr()), - onPressed: () { - Navigator.of(context).pop(); - }, - ), - TextButton( - child: Text('modals.yes'.tr()), - onPressed: () { - context.read().setServerId(server); - Navigator.of(context).pop(); - }, - ), - ], - ); - }, ); } class IsValidStringDisplay extends StatelessWidget { const IsValidStringDisplay({ - super.key, required this.isValid, required this.textIfValid, required this.textIfInvalid, + final super.key, }); final bool isValid; @@ -270,15 +270,15 @@ class IsValidStringDisplay extends StatelessWidget { final String textIfInvalid; @override - Widget build(BuildContext context) { - return Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - isValid - ? Icon(Icons.check, color: Theme.of(context).colorScheme.onSurface) - : Icon(Icons.close, color: Theme.of(context).colorScheme.error), - const SizedBox(width: 8), - Expanded( + Widget build(final BuildContext context) => Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (isValid) + Icon(Icons.check, color: Theme.of(context).colorScheme.onSurface) + else + Icon(Icons.close, color: Theme.of(context).colorScheme.error), + const SizedBox(width: 8), + Expanded( child: isValid ? Text( textIfValid, @@ -291,8 +291,8 @@ class IsValidStringDisplay extends StatelessWidget { style: Theme.of(context).textTheme.bodyMedium!.copyWith( color: Theme.of(context).colorScheme.error, ), - )), - ], - ); - } + ), + ), + ], + ); } diff --git a/lib/ui/pages/setup/recovering/recovery_hentzner_connected.dart b/lib/ui/pages/setup/recovering/recovery_hentzner_connected.dart index 6973ae2d..29506e8b 100644 --- a/lib/ui/pages/setup/recovering/recovery_hentzner_connected.dart +++ b/lib/ui/pages/setup/recovering/recovery_hentzner_connected.dart @@ -11,23 +11,25 @@ import 'package:selfprivacy/logic/cubit/server_installation/server_installation_ import 'package:selfprivacy/ui/components/brand_md/brand_md.dart'; class RecoveryHetznerConnected extends StatelessWidget { - const RecoveryHetznerConnected({final Key? key}) : super(key: key); + const RecoveryHetznerConnected({final super.key}); @override Widget build(final BuildContext context) { - ServerInstallationCubit appConfig = context.watch(); + final ServerInstallationCubit appConfig = + context.watch(); return BlocProvider( create: (final BuildContext context) => HetznerFormCubit(appConfig), child: Builder( builder: (final BuildContext context) { - FormCubitState formCubitState = context.watch().state; + final FormCubitState formCubitState = + context.watch().state; return BrandHeroScreen( heroTitle: 'recovering.hetzner_connected'.tr(), - heroSubtitle: 'recovering.hetzner_connected_description'.tr(args: [ - appConfig.state.serverDomain?.domainName ?? 'your domain' - ]), + heroSubtitle: 'recovering.hetzner_connected_description'.tr( + args: [appConfig.state.serverDomain?.domainName ?? 'your domain'], + ), hasBackButton: true, hasFlashButton: false, children: [ @@ -52,17 +54,16 @@ class RecoveryHetznerConnected extends StatelessWidget { context: context, isScrollControlled: true, backgroundColor: Colors.transparent, - builder: (final BuildContext context) { - return const BrandBottomSheet( - isExpended: true, - child: Padding( - padding: paddingH15V0, - child: BrandMarkdown( - fileName: 'how_hetzner', - ), + builder: (final BuildContext context) => + const BrandBottomSheet( + isExpended: true, + child: Padding( + padding: paddingH15V0, + child: BrandMarkdown( + fileName: 'how_hetzner', ), - ); - }, + ), + ), ), ), ], diff --git a/lib/ui/pages/setup/recovering/recovery_method_select.dart b/lib/ui/pages/setup/recovering/recovery_method_select.dart index a8e4860c..8abe3a27 100644 --- a/lib/ui/pages/setup/recovering/recovery_method_select.dart +++ b/lib/ui/pages/setup/recovering/recovery_method_select.dart @@ -9,119 +9,125 @@ import 'package:selfprivacy/ui/pages/setup/recovering/recover_by_old_token.dart' import 'package:selfprivacy/utils/route_transitions/basic.dart'; class RecoveryMethodSelect extends StatelessWidget { - const RecoveryMethodSelect({Key? key}) : super(key: key); + const RecoveryMethodSelect({final super.key}); @override - Widget build(BuildContext context) { - return BrandHeroScreen( - heroTitle: 'recovering.recovery_main_header'.tr(), - heroSubtitle: 'recovering.method_select_description'.tr(), - hasBackButton: true, - hasFlashButton: false, - children: [ - BrandCards.outlined( - child: ListTile( - title: Text( - 'recovering.method_select_other_device'.tr(), - style: Theme.of(context).textTheme.titleMedium, - ), - leading: const Icon(Icons.offline_share_outlined), - onTap: () => context - .read() - .selectRecoveryMethod(ServerRecoveryMethods.newDeviceKey), - ), - ), - const SizedBox(height: 16), - BrandCards.outlined( - child: ListTile( - title: Text( - 'recovering.method_select_recovery_key'.tr(), - style: Theme.of(context).textTheme.titleMedium, - ), - leading: const Icon(Icons.password_outlined), - onTap: () => context - .read() - .selectRecoveryMethod(ServerRecoveryMethods.recoveryKey), - ), - ), - const SizedBox(height: 16), - BrandButton.text( - title: 'recovering.method_select_nothing'.tr(), - onPressed: () => Navigator.of(context) - .push(materialRoute(const RecoveryFallbackMethodSelect())), - ) - ], - ); - } -} - -class RecoveryFallbackMethodSelect extends StatelessWidget { - const RecoveryFallbackMethodSelect({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return BlocListener( - listener: (context, state) { - if (state is ServerInstallationRecovery && - state.recoveryCapabilities == - ServerRecoveryCapabilities.loginTokens && - state.currentStep != RecoveryStep.selecting) { - Navigator.of(context).pop(); - } - }, - child: BrandHeroScreen( + Widget build(final BuildContext context) => BrandHeroScreen( heroTitle: 'recovering.recovery_main_header'.tr(), - heroSubtitle: 'recovering.fallback_select_description'.tr(), + heroSubtitle: 'recovering.method_select_description'.tr(), hasBackButton: true, hasFlashButton: false, children: [ BrandCards.outlined( child: ListTile( title: Text( - 'recovering.fallback_select_token_copy'.tr(), + 'recovering.method_select_other_device'.tr(), style: Theme.of(context).textTheme.titleMedium, ), - leading: const Icon(Icons.vpn_key), - onTap: () => Navigator.of(context) - .push(materialRoute(const RecoverByOldTokenInstruction( - instructionFilename: 'how_fallback_old', - ))), + leading: const Icon(Icons.offline_share_outlined), + onTap: () => context + .read() + .selectRecoveryMethod(ServerRecoveryMethods.newDeviceKey), ), ), const SizedBox(height: 16), BrandCards.outlined( child: ListTile( title: Text( - 'recovering.fallback_select_root_ssh'.tr(), + 'recovering.method_select_recovery_key'.tr(), style: Theme.of(context).textTheme.titleMedium, ), - leading: const Icon(Icons.terminal), - onTap: () => Navigator.of(context) - .push(materialRoute(const RecoverByOldTokenInstruction( - instructionFilename: 'how_fallback_ssh', - ))), + leading: const Icon(Icons.password_outlined), + onTap: () => context + .read() + .selectRecoveryMethod(ServerRecoveryMethods.recoveryKey), ), ), const SizedBox(height: 16), - BrandCards.outlined( - child: ListTile( - title: Text( - 'recovering.fallback_select_provider_console'.tr(), - style: Theme.of(context).textTheme.titleMedium, - ), - subtitle: Text( - 'recovering.fallback_select_provider_console_hint'.tr(), - style: Theme.of(context).textTheme.bodyMedium, - ), - leading: const Icon(Icons.web), - onTap: () => Navigator.of(context) - .push(materialRoute(const RecoverByOldTokenInstruction( - instructionFilename: 'how_fallback_terminal', - ))), - ), - ), + BrandButton.text( + title: 'recovering.method_select_nothing'.tr(), + onPressed: () => Navigator.of(context) + .push(materialRoute(const RecoveryFallbackMethodSelect())), + ) ], - ), - ); - } + ); +} + +class RecoveryFallbackMethodSelect extends StatelessWidget { + const RecoveryFallbackMethodSelect({final super.key}); + + @override + Widget build(final BuildContext context) => + BlocListener( + listener: (final context, final state) { + if (state is ServerInstallationRecovery && + state.recoveryCapabilities == + ServerRecoveryCapabilities.loginTokens && + state.currentStep != RecoveryStep.selecting) { + Navigator.of(context).pop(); + } + }, + child: BrandHeroScreen( + heroTitle: 'recovering.recovery_main_header'.tr(), + heroSubtitle: 'recovering.fallback_select_description'.tr(), + hasBackButton: true, + hasFlashButton: false, + children: [ + BrandCards.outlined( + child: ListTile( + title: Text( + 'recovering.fallback_select_token_copy'.tr(), + style: Theme.of(context).textTheme.titleMedium, + ), + leading: const Icon(Icons.vpn_key), + onTap: () => Navigator.of(context).push( + materialRoute( + const RecoverByOldTokenInstruction( + instructionFilename: 'how_fallback_old', + ), + ), + ), + ), + ), + const SizedBox(height: 16), + BrandCards.outlined( + child: ListTile( + title: Text( + 'recovering.fallback_select_root_ssh'.tr(), + style: Theme.of(context).textTheme.titleMedium, + ), + leading: const Icon(Icons.terminal), + onTap: () => Navigator.of(context).push( + materialRoute( + const RecoverByOldTokenInstruction( + instructionFilename: 'how_fallback_ssh', + ), + ), + ), + ), + ), + const SizedBox(height: 16), + BrandCards.outlined( + child: ListTile( + title: Text( + 'recovering.fallback_select_provider_console'.tr(), + style: Theme.of(context).textTheme.titleMedium, + ), + subtitle: Text( + 'recovering.fallback_select_provider_console_hint'.tr(), + style: Theme.of(context).textTheme.bodyMedium, + ), + leading: const Icon(Icons.web), + onTap: () => Navigator.of(context).push( + materialRoute( + const RecoverByOldTokenInstruction( + instructionFilename: 'how_fallback_terminal', + ), + ), + ), + ), + ), + ], + ), + ); } diff --git a/lib/ui/pages/setup/recovering/recovery_routing.dart b/lib/ui/pages/setup/recovering/recovery_routing.dart index 191ee1e6..e5a86074 100644 --- a/lib/ui/pages/setup/recovering/recovery_routing.dart +++ b/lib/ui/pages/setup/recovering/recovery_routing.dart @@ -16,11 +16,11 @@ import 'package:selfprivacy/ui/pages/setup/recovering/recovery_hentzner_connecte import 'package:selfprivacy/ui/pages/setup/recovering/recovery_method_select.dart'; class RecoveryRouting extends StatelessWidget { - const RecoveryRouting({Key? key}) : super(key: key); + const RecoveryRouting({final super.key}); @override - Widget build(BuildContext context) { - var serverInstallation = context.watch().state; + Widget build(final BuildContext context) { + final serverInstallation = context.watch().state; Widget currentPage = const SelectDomainToRecover(); @@ -61,9 +61,9 @@ class RecoveryRouting extends StatelessWidget { } return BlocListener( - listener: (context, state) { + listener: (final context, final state) { if (state is ServerInstallationFinished) { - Navigator.of(context).popUntil((route) => route.isFirst); + Navigator.of(context).popUntil((final route) => route.isFirst); } }, child: AnimatedSwitcher( @@ -75,21 +75,23 @@ class RecoveryRouting extends StatelessWidget { } class SelectDomainToRecover extends StatelessWidget { - const SelectDomainToRecover({Key? key}) : super(key: key); + const SelectDomainToRecover({final super.key}); @override - Widget build(BuildContext context) { - var serverInstallation = context.watch(); + Widget build(final BuildContext context) { + final serverInstallation = context.watch(); return BlocProvider( - create: (context) => RecoveryDomainFormCubit( - serverInstallation, FieldCubitFactory(context)), + create: (final context) => RecoveryDomainFormCubit( + serverInstallation, + FieldCubitFactory(context), + ), child: Builder( - builder: (context) { - var formCubitState = context.watch().state; + builder: (final context) { + final formCubitState = context.watch().state; return BlocListener( - listener: (context, state) { + listener: (final context, final state) { if (state is ServerInstallationRecovery) { if (state.currentStep == RecoveryStep.selecting) { if (state.recoveryCapabilities == @@ -108,7 +110,7 @@ class SelectDomainToRecover extends StatelessWidget { hasFlashButton: false, onBackButtonPressed: serverInstallation is ServerInstallationRecovery - ? () => serverInstallation.clearAppConfig() + ? serverInstallation.clearAppConfig : null, children: [ CubitFormTextField( diff --git a/lib/ui/pages/ssh_keys/new_ssh_key.dart b/lib/ui/pages/ssh_keys/new_ssh_key.dart index abeda0db..247590b7 100644 --- a/lib/ui/pages/ssh_keys/new_ssh_key.dart +++ b/lib/ui/pages/ssh_keys/new_ssh_key.dart @@ -1,76 +1,76 @@ part of 'ssh_keys.dart'; class _NewSshKey extends StatelessWidget { + const _NewSshKey(this.user); final User user; - const _NewSshKey(this.user); - @override - Widget build(BuildContext context) { - return BrandBottomSheet( - child: BlocProvider( - create: (context) { - var jobCubit = context.read(); - var jobState = jobCubit.state; - if (jobState is JobsStateWithJobs) { - var jobs = jobState.jobList; - for (var job in jobs) { - if (job is CreateSSHKeyJob && job.user.login == user.login) { - user.sshKeys.add(job.publicKey); + 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.jobList; + 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: (context) { - var formCubitState = context.watch().state; + return SshFormCubit( + jobsCubit: jobCubit, + user: user, + ); + }, + child: Builder( + builder: (final context) { + final formCubitState = context.watch().state; - return BlocListener( - listener: (context, 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/ssh_keys/ssh_keys.dart b/lib/ui/pages/ssh_keys/ssh_keys.dart index 6fc5087b..4059ba63 100644 --- a/lib/ui/pages/ssh_keys/ssh_keys.dart +++ b/lib/ui/pages/ssh_keys/ssh_keys.dart @@ -8,136 +8,137 @@ import 'package:selfprivacy/ui/components/brand_cards/brand_cards.dart'; import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart'; -import '../../../config/brand_colors.dart'; -import '../../../config/brand_theme.dart'; -import '../../../logic/cubit/jobs/jobs_cubit.dart'; -import '../../../logic/models/hive/user.dart'; -import '../../components/brand_button/brand_button.dart'; -import '../../components/brand_header/brand_header.dart'; +import 'package:selfprivacy/config/brand_colors.dart'; +import 'package:selfprivacy/config/brand_theme.dart'; +import 'package:selfprivacy/logic/cubit/jobs/jobs_cubit.dart'; +import 'package:selfprivacy/logic/models/hive/user.dart'; +import 'package:selfprivacy/ui/components/brand_button/brand_button.dart'; +import 'package:selfprivacy/ui/components/brand_header/brand_header.dart'; part 'new_ssh_key.dart'; // Get user object as a parameter class SshKeysPage extends StatefulWidget { + const SshKeysPage({required this.user, final super.key}); final User user; - const SshKeysPage({Key? key, required this.user}) : super(key: key); - @override State createState() => _SshKeysPageState(); } class _SshKeysPageState extends State { @override - Widget build(BuildContext context) { - return BrandHeroScreen( - heroTitle: 'ssh.title'.tr(), - heroSubtitle: widget.user.login, - heroIcon: BrandIcons.key, - children: [ - if (widget.user.login == 'root') - Column( - children: [ - // Show alert card if user is root - BrandCards.outlined( - child: ListTile( - leading: Icon( - Icons.warning_rounded, - color: Theme.of(context).colorScheme.error, + Widget build(final BuildContext context) => BrandHeroScreen( + heroTitle: 'ssh.title'.tr(), + heroSubtitle: widget.user.login, + heroIcon: BrandIcons.key, + children: [ + if (widget.user.login == 'root') + Column( + children: [ + // Show alert card if user is root + BrandCards.outlined( + child: ListTile( + leading: Icon( + Icons.warning_rounded, + color: Theme.of(context).colorScheme.error, + ), + title: Text('ssh.root.title'.tr()), + subtitle: Text('ssh.root.subtitle'.tr()), ), - title: Text('ssh.root.title'.tr()), - subtitle: Text('ssh.root.subtitle'.tr()), + ) + ], + ), + BrandCards.outlined( + child: Column( + children: [ + ListTile( + title: Text( + 'ssh.create'.tr(), + style: Theme.of(context).textTheme.headline6, + ), + leading: const Icon(Icons.add_circle_outline_rounded), + onTap: () { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (final BuildContext context) => Padding( + padding: MediaQuery.of(context).viewInsets, + child: _NewSshKey(widget.user), + ), + ); + }, ), - ) - ], - ), - BrandCards.outlined( - child: Column( - children: [ - ListTile( - title: Text( - 'ssh.create'.tr(), - style: Theme.of(context).textTheme.headline6, - ), - leading: const Icon(Icons.add_circle_outline_rounded), - onTap: () { - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (BuildContext context) { - return Padding( - padding: MediaQuery.of(context).viewInsets, - child: _NewSshKey(widget.user)); - }, - ); - }, - ), - const Divider(height: 0), - // show a list of ListTiles with ssh keys - // Clicking on one should delete it - Column( - children: widget.user.sshKeys.map((key) { - final publicKey = - key.split(' ').length > 1 ? key.split(' ')[1] : key; - final keyType = key.split(' ')[0]; - final keyName = key.split(' ').length > 2 - ? key.split(' ')[2] - : 'ssh.no_key_name'.tr(); - return ListTile( + const Divider(height: 0), + // show a list of ListTiles with ssh keys + // Clicking on one should delete it + Column( + children: widget.user.sshKeys.map((final String key) { + final publicKey = + key.split(' ').length > 1 ? key.split(' ')[1] : key; + final keyType = key.split(' ')[0]; + final keyName = key.split(' ').length > 2 + ? key.split(' ')[2] + : 'ssh.no_key_name'.tr(); + return ListTile( title: Text('$keyName ($keyType)'), // do not overflow text - subtitle: Text(publicKey, - maxLines: 1, overflow: TextOverflow.ellipsis), + subtitle: Text( + publicKey, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), onTap: () { showDialog( context: context, - builder: (context) { - return AlertDialog( - title: Text('ssh.delete'.tr()), - content: SingleChildScrollView( - child: ListBody( - children: [ - Text('ssh.delete_confirm_question'.tr()), - Text('$keyName ($keyType)'), - Text(publicKey), - ], - ), + builder: (final BuildContext context) => AlertDialog( + title: Text('ssh.delete'.tr()), + content: SingleChildScrollView( + child: ListBody( + children: [ + Text('ssh.delete_confirm_question'.tr()), + Text('$keyName ($keyType)'), + Text(publicKey), + ], ), - actions: [ - TextButton( - child: Text('basis.cancel'.tr()), - onPressed: () { - Navigator.of(context).pop(); - }, - ), - TextButton( - child: Text( - 'basis.delete'.tr(), - style: const TextStyle( - color: BrandColors.red1, - ), + ), + actions: [ + TextButton( + child: Text('basis.cancel'.tr()), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + TextButton( + child: Text( + 'basis.delete'.tr(), + style: const TextStyle( + color: BrandColors.red1, ), - onPressed: () { - context.read().addJob( - DeleteSSHKeyJob( - user: widget.user, publicKey: key)); - Navigator.of(context) - ..pop() - ..pop(); - }, ), - ], - ); - }, + onPressed: () { + context.read().addJob( + DeleteSSHKeyJob( + user: widget.user, + publicKey: key, + ), + ); + Navigator.of(context) + ..pop() + ..pop(); + }, + ), + ], + ), ); - }); - }).toList(), - ) - ], + }, + ); + }).toList(), + ) + ], + ), ), - ), - ], - ); - } + ], + ); } diff --git a/lib/ui/pages/users/add_user_fab.dart b/lib/ui/pages/users/add_user_fab.dart index c527a60b..a78f056d 100644 --- a/lib/ui/pages/users/add_user_fab.dart +++ b/lib/ui/pages/users/add_user_fab.dart @@ -1,25 +1,22 @@ part of 'users.dart'; class AddUserFab extends StatelessWidget { - const AddUserFab({Key? key}) : super(key: key); + const AddUserFab({final super.key}); @override - Widget build(BuildContext context) { - return FloatingActionButton.small( - heroTag: 'new_user_fab', - onPressed: () { - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (BuildContext context) { - return Padding( - padding: MediaQuery.of(context).viewInsets, - child: const NewUser()); - }, - ); - }, - child: const Icon(Icons.person_add_outlined), - ); - } + 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 2e5c5906..847003d3 100644 --- a/lib/ui/pages/users/empty.dart +++ b/lib/ui/pages/users/empty.dart @@ -1,35 +1,33 @@ part of 'users.dart'; class _NoUsers extends StatelessWidget { - const _NoUsers({Key? key, required this.text}) : super(key: key); + const _NoUsers({required this.text}); final String text; @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(BrandIcons.users, size: 50, color: BrandColors.grey7), - const SizedBox(height: 20), - BrandText.h2( - 'users.nobody_here'.tr(), - style: const TextStyle( - color: BrandColors.grey7, + Widget build(final BuildContext context) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(BrandIcons.users, size: 50, color: BrandColors.grey7), + const SizedBox(height: 20), + BrandText.h2( + 'users.nobody_here'.tr(), + style: const TextStyle( + color: BrandColors.grey7, + ), ), - ), - const SizedBox(height: 10), - BrandText.medium( - text, - textAlign: TextAlign.center, - style: const TextStyle( - color: BrandColors.grey7, + const SizedBox(height: 10), + BrandText.medium( + text, + textAlign: TextAlign.center, + style: const TextStyle( + color: BrandColors.grey7, + ), ), - ), - ], - ), - ); - } + ], + ), + ); } diff --git a/lib/ui/pages/users/new_user.dart b/lib/ui/pages/users/new_user.dart index 4dd434b5..72cb6387 100644 --- a/lib/ui/pages/users/new_user.dart +++ b/lib/ui/pages/users/new_user.dart @@ -1,24 +1,25 @@ part of 'users.dart'; class NewUser extends StatelessWidget { - const NewUser({Key? key}) : super(key: key); + const NewUser({final super.key}); @override - Widget build(BuildContext context) { - var config = context.watch().state; + Widget build(final BuildContext context) { + final ServerInstallationState config = + context.watch().state; - var domainName = UiHelpers.getDomainName(config); + final String domainName = UiHelpers.getDomainName(config); return BrandBottomSheet( child: BlocProvider( - create: (context) { - var jobCubit = context.read(); - var jobState = jobCubit.state; - var users = []; + create: (final BuildContext context) { + final jobCubit = context.read(); + final jobState = jobCubit.state; + final users = []; users.addAll(context.read().state.users); if (jobState is JobsStateWithJobs) { - var jobs = jobState.jobList; - for (var job in jobs) { + final jobs = jobState.jobList; + for (final job in jobs) { if (job is CreateUserJob) { users.add(job.user); } @@ -29,73 +30,80 @@ class NewUser extends StatelessWidget { fieldFactory: FieldCubitFactory(context), ); }, - child: Builder(builder: (context) { - var formCubitState = context.watch().state; + child: Builder( + builder: (final BuildContext context) { + final FormCubitState formCubitState = + context.watch().state; - return BlocListener( - listener: (context, 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: [ - 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, + 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: [ + IntrinsicHeight( + child: CubitFormTextField( + formFieldCubit: context.read().login, + decoration: InputDecoration( + labelText: 'users.login'.tr(), + suffixText: '@$domainName', ), ), ), - ), - 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), - ], + 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/user.dart b/lib/ui/pages/users/user.dart index c59d53fc..69a2e5dc 100644 --- a/lib/ui/pages/users/user.dart +++ b/lib/ui/pages/users/user.dart @@ -1,49 +1,51 @@ part of 'users.dart'; class _User extends StatelessWidget { - const _User({Key? key, required this.user, required this.isRootUser}) - : super(key: key); + const _User({ + required this.user, + required this.isRootUser, + }); final User user; final bool isRootUser; @override - Widget build(BuildContext context) { - return InkWell( - onTap: () { - showBrandBottomSheet( - context: context, - builder: (BuildContext context) { - return _UserDetails(user: user, isRootUser: isRootUser); - }, - ); - }, - child: Container( - padding: paddingH15V0, - height: 48, - child: Row( - children: [ - Container( - width: 17, - height: 17, - decoration: BoxDecoration( - color: user.color, - shape: BoxShape.circle, + Widget build(final BuildContext context) => InkWell( + onTap: () { + showBrandBottomSheet( + context: context, + builder: (final BuildContext context) => + _UserDetails(user: user, isRootUser: isRootUser), + ); + }, + child: Container( + padding: paddingH15V0, + height: 48, + child: Row( + children: [ + Container( + width: 17, + height: 17, + decoration: BoxDecoration( + color: user.color, + shape: BoxShape.circle, + ), ), - ), - 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)), - ), - ], + 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, + ), + ), + ), + ], + ), ), - ), - ); - } + ); } diff --git a/lib/ui/pages/users/user_details.dart b/lib/ui/pages/users/user_details.dart index bd7cbb2d..f7e212c8 100644 --- a/lib/ui/pages/users/user_details.dart +++ b/lib/ui/pages/users/user_details.dart @@ -2,7 +2,7 @@ part of 'users.dart'; class _UserDetails extends StatelessWidget { const _UserDetails({ - Key? key, + final Key? key, required this.user, required this.isRootUser, }) : super(key: key); @@ -10,10 +10,11 @@ class _UserDetails extends StatelessWidget { final User user; final bool isRootUser; @override - Widget build(BuildContext context) { - var config = context.watch().state; + Widget build(final BuildContext context) { + final ServerInstallationState config = + context.watch().state; - var domainName = UiHelpers.getDomainName(config); + final String domainName = UiHelpers.getDomainName(config); return BrandBottomSheet( isExpended: true, @@ -44,60 +45,54 @@ class _UserDetails extends StatelessWidget { shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10.0), ), - onSelected: (PopupMenuItemType result) { + onSelected: (final PopupMenuItemType result) { switch (result) { case PopupMenuItemType.delete: showDialog( context: context, - builder: (context) { - return AlertDialog( - title: Text('basis.confirmation'.tr()), - content: SingleChildScrollView( - child: ListBody( - children: [ - Text('users.delete_confirm_question' - .tr()), - ], - ), - ), - actions: [ - TextButton( - child: Text('basis.cancel'.tr()), - onPressed: () { - Navigator.of(context).pop(); - }, - ), - TextButton( - child: Text( - 'basis.delete'.tr(), - style: const TextStyle( - color: BrandColors.red1, - ), + builder: (final BuildContext context) => + AlertDialog( + title: Text('basis.confirmation'.tr()), + content: SingleChildScrollView( + child: ListBody( + children: [ + Text( + 'users.delete_confirm_question'.tr(), + ), + ], + ), + ), + actions: [ + TextButton( + child: Text('basis.cancel'.tr()), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + TextButton( + child: Text( + 'basis.delete'.tr(), + style: const TextStyle( + color: BrandColors.red1, ), - onPressed: () { - context.read().addJob( - DeleteUserJob(user: user)); - Navigator.of(context) - ..pop() - ..pop(); - }, ), - ], - ); - }, + onPressed: () { + context + .read() + .addJob(DeleteUserJob(user: user)); + Navigator.of(context) + ..pop() + ..pop(); + }, + ), + ], + ), ); break; } }, icon: const Icon(Icons.more_vert), - itemBuilder: (BuildContext context) => [ - // PopupMenuItem( - // value: PopupMenuItemType.reset, - // child: Container( - // padding: EdgeInsets.only(left: 5), - // child: Text('users.reset_password'.tr()), - // ), - // ), + itemBuilder: (final BuildContext context) => [ PopupMenuItem( value: PopupMenuItemType.delete, child: Container( @@ -114,18 +109,19 @@ class _UserDetails extends StatelessWidget { ), const Spacer(), Padding( - padding: const EdgeInsets.symmetric( - vertical: 20, - horizontal: 15, - ), - child: AutoSizeText( - user.login, - style: headline1Style, - softWrap: true, - minFontSize: 9, - maxLines: 3, - overflow: TextOverflow.ellipsis, - )), + padding: const EdgeInsets.symmetric( + vertical: 20, + horizontal: 15, + ), + child: AutoSizeText( + user.login, + style: headline1Style, + softWrap: true, + minFontSize: 9, + maxLines: 3, + overflow: TextOverflow.ellipsis, + ), + ), ], ), ), @@ -158,21 +154,25 @@ class _UserDetails extends StatelessWidget { const BrandDivider(), const SizedBox(height: 20), ListTile( - onTap: () { - Navigator.of(context) - .push(materialRoute(SshKeysPage(user: user))); - }, - title: Text('ssh.title'.tr()), - subtitle: user.sshKeys.isNotEmpty - ? Text('ssh.subtitle_with_keys' - .tr(args: [user.sshKeys.length.toString()])) - : Text('ssh.subtitle_without_keys'.tr()), - trailing: const Icon(BrandIcons.key)), + onTap: () { + Navigator.of(context) + .push(materialRoute(SshKeysPage(user: user))); + }, + title: Text('ssh.title'.tr()), + subtitle: user.sshKeys.isNotEmpty + ? Text( + 'ssh.subtitle_with_keys' + .tr(args: [user.sshKeys.length.toString()]), + ) + : Text('ssh.subtitle_without_keys'.tr()), + trailing: const Icon(BrandIcons.key), + ), const SizedBox(height: 20), ListTile( onTap: () { Share.share( - 'login: ${user.login}, password: ${user.password}'); + 'login: ${user.login}, password: ${user.password}', + ); }, title: Text( 'users.send_registration_data'.tr(), diff --git a/lib/ui/pages/users/users.dart b/lib/ui/pages/users/users.dart index 52bb430d..659453d1 100644 --- a/lib/ui/pages/users/users.dart +++ b/lib/ui/pages/users/users.dart @@ -24,7 +24,7 @@ import 'package:selfprivacy/ui/pages/ssh_keys/ssh_keys.dart'; import 'package:selfprivacy/utils/ui_helpers.dart'; import 'package:share_plus/share_plus.dart'; -import '../../../utils/route_transitions/basic.dart'; +import 'package:selfprivacy/utils/route_transitions/basic.dart'; part 'empty.dart'; part 'new_user.dart'; @@ -33,23 +33,19 @@ part 'user_details.dart'; part 'add_user_fab.dart'; class UsersPage extends StatelessWidget { - const UsersPage({Key? key}) : super(key: key); + const UsersPage({final super.key}); @override - Widget build(BuildContext context) { - // final usersCubitState = context.watch().state; - var isReady = context.watch().state + Widget build(final BuildContext context) { + final bool isReady = context.watch().state is ServerInstallationFinished; - // final primaryUser = usersCubitState.primaryUser; - // final users = [primaryUser, ...usersCubitState.users]; - // final isEmpty = users.isEmpty; Widget child; if (!isReady) { child = isNotReady(); } else { child = BlocBuilder( - builder: (context, state) { + builder: (final BuildContext context, final UsersState state) { print('Rebuild users page'); final primaryUser = state.primaryUser; final users = [primaryUser, ...state.users]; @@ -60,12 +56,11 @@ class UsersPage extends StatelessWidget { }, child: ListView.builder( itemCount: users.length, - itemBuilder: (BuildContext context, int index) { - return _User( - user: users[index], - isRootUser: index == 0, - ); - }, + itemBuilder: (final BuildContext context, final int index) => + _User( + user: users[index], + isRootUser: index == 0, + ), ), ); }, @@ -83,25 +78,23 @@ class UsersPage extends StatelessWidget { ); } - Widget isNotReady() { - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const Padding( - padding: EdgeInsets.symmetric(horizontal: 15), - child: NotReadyCard(), - ), - Expanded( - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 15), - child: Center( - child: _NoUsers( - text: 'users.not_ready'.tr(), + Widget isNotReady() => Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const Padding( + padding: EdgeInsets.symmetric(horizontal: 15), + child: NotReadyCard(), + ), + Expanded( + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: Center( + child: _NoUsers( + text: 'users.not_ready'.tr(), + ), ), ), - ), - ) - ], - ); - } + ) + ], + ); } diff --git a/lib/utils/color_utils.dart b/lib/utils/color_utils.dart index ac0c63ba..9fcf7397 100644 --- a/lib/utils/color_utils.dart +++ b/lib/utils/color_utils.dart @@ -1,8 +1,9 @@ import 'package:flutter/material.dart'; -Color stringToColor(String string) { - var number = string.codeUnits.reduce((a, b) => a + b); - var index = number % colorPalette.length; +Color stringToColor(final String string) { + final int number = + string.codeUnits.reduce((final int a, final int b) => a + b); + final int index = number % colorPalette.length; return colorPalette[index]; } @@ -10,7 +11,7 @@ var originalColor = const Color(0xFFDBD8BD); var count = 40; var colorPalette = List.generate( count, - (index) => HSLColor.fromColor(originalColor) - .withHue((index) * 360.0 / count) + (final int index) => HSLColor.fromColor(originalColor) + .withHue(index * 360.0 / count) .toColor(), ); diff --git a/lib/utils/extensions/duration.dart b/lib/utils/extensions/duration.dart index aac00eb2..2c302fb8 100644 --- a/lib/utils/extensions/duration.dart +++ b/lib/utils/extensions/duration.dart @@ -1,35 +1,32 @@ // ignore_for_file: unnecessary_this extension DurationFormatter on Duration { - String toDayHourMinuteSecondFormat() { - return [ - this.inHours.remainder(24), - this.inMinutes.remainder(60), - this.inSeconds.remainder(60) - ].map((seg) => seg.toString().padLeft(2, '0')).join(':'); - } + String toDayHourMinuteSecondFormat() => [ + this.inHours.remainder(24), + this.inMinutes.remainder(60), + this.inSeconds.remainder(60) + ].map((final int seg) => seg.toString().padLeft(2, '0')).join(':'); String toDayHourMinuteFormat() { - var designator = this >= Duration.zero ? '+' : '-'; + final designator = this >= Duration.zero ? '+' : '-'; - var segments = [ + final Iterable segments = [ this.inHours.remainder(24).abs(), this.inMinutes.remainder(60).abs(), - ].map((seg) => seg.toString().padLeft(2, '0')); + ].map((final int seg) => seg.toString().padLeft(2, '0')); return '$designator${segments.first}:${segments.last}'; } - String toHoursMinutesSecondsFormat() { - // WAT: https://flutterigniter.com/how-to-format-duration/ - return this.toString().split('.').first.padLeft(8, '0'); - } +// WAT: https://flutterigniter.com/how-to-format-duration/ + String toHoursMinutesSecondsFormat() => + this.toString().split('.').first.padLeft(8, '0'); String toDayHourMinuteFormat2() { - var segments = [ + final Iterable segments = [ this.inHours.remainder(24), this.inMinutes.remainder(60), - ].map((seg) => seg.toString().padLeft(2, '0')); + ].map((final int seg) => seg.toString().padLeft(2, '0')); return '${segments.first} h ${segments.last} min'; } } diff --git a/lib/utils/extensions/elevation_extension.dart b/lib/utils/extensions/elevation_extension.dart index 4b1cbf3f..9c6bbc14 100644 --- a/lib/utils/extensions/elevation_extension.dart +++ b/lib/utils/extensions/elevation_extension.dart @@ -1,5 +1,3 @@ -// ignore_for_file: always_specify_types - library elevation_extension; import 'package:flutter/material.dart'; @@ -15,16 +13,20 @@ extension ElevationExtension on BoxDecoration { final Gradient? gradient, final BlendMode? backgroundBlendMode, final BoxShape? shape, - }) => BoxDecoration( - color: color ?? this.color, - image: image ?? this.image, - border: border ?? this.border, - borderRadius: borderRadius ?? this.borderRadius, - boxShadow: this.boxShadow != null || boxShadow != null - ? [...this.boxShadow ?? [], ...boxShadow ?? []] - : null, - gradient: gradient ?? this.gradient, - backgroundBlendMode: backgroundBlendMode ?? this.backgroundBlendMode, - shape: shape ?? this.shape, - ); + }) => + BoxDecoration( + color: color ?? this.color, + image: image ?? this.image, + border: border ?? this.border, + borderRadius: borderRadius ?? this.borderRadius, + boxShadow: this.boxShadow != null || boxShadow != null + ? [ + ...this.boxShadow ?? [], + ...boxShadow ?? [] + ] + : null, + gradient: gradient ?? this.gradient, + backgroundBlendMode: backgroundBlendMode ?? this.backgroundBlendMode, + shape: shape ?? this.shape, + ); } diff --git a/lib/utils/password_generator.dart b/lib/utils/password_generator.dart index 70fd83a9..5acf3888 100644 --- a/lib/utils/password_generator.dart +++ b/lib/utils/password_generator.dart @@ -1,5 +1,3 @@ -// ignore_for_file: always_specify_types - import 'dart:math'; Random _rnd = Random(); @@ -20,10 +18,22 @@ class StringGenerators { final isStrict = false, }) { String chars = ''; - if (hasLowercaseLetters) chars += letters; - if (hasUppercaseLetters) chars += letters.toUpperCase(); - if (hasNumbers) chars += numbers; - if (hasSymbols) chars += symbols; + + if (hasLowercaseLetters) { + chars += letters; + } + + if (hasUppercaseLetters) { + chars += letters.toUpperCase(); + } + + if (hasNumbers) { + chars += numbers; + } + + if (hasSymbols) { + chars += symbols; + } assert(chars.isNotEmpty, 'chart empty'); @@ -55,14 +65,15 @@ class StringGenerators { return shuffledlist.join(); } - static String genString(final int length, final String chars) => String.fromCharCodes( - Iterable.generate( - length, - (final _) => chars.codeUnitAt( - _rnd.nextInt(chars.length), + static String genString(final int length, final String chars) => + String.fromCharCodes( + Iterable.generate( + length, + (final _) => chars.codeUnitAt( + _rnd.nextInt(chars.length), + ), ), - ), - ); + ); static StringGeneratorFunction userPassword = () => getRandomString( 8, diff --git a/lib/utils/route_transitions/basic.dart b/lib/utils/route_transitions/basic.dart index 5cd69cfd..a3148e1d 100644 --- a/lib/utils/route_transitions/basic.dart +++ b/lib/utils/route_transitions/basic.dart @@ -1,5 +1,3 @@ -// ignore_for_file: always_specify_types - import 'package:flutter/material.dart'; Route materialRoute(final Widget widget) => MaterialPageRoute( @@ -7,5 +5,10 @@ Route materialRoute(final Widget widget) => MaterialPageRoute( ); Route noAnimationRoute(final Widget widget) => PageRouteBuilder( - pageBuilder: (final BuildContext context, final Animation animation1, final Animation animation2) => widget, + pageBuilder: ( + final BuildContext context, + final Animation animation1, + final Animation animation2, + ) => + widget, ); diff --git a/pubspec.lock b/pubspec.lock index 965eb666..e3faf1b6 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -363,6 +363,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.9.2" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" flutter_localizations: dependency: transitive description: flutter @@ -519,7 +526,7 @@ packages: source: hosted version: "3.1.3" intl: - dependency: transitive + dependency: "direct main" description: name: intl url: "https://pub.dartlang.org" @@ -560,6 +567,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "6.2.0" + lints: + dependency: transitive + description: + name: lints + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" local_auth: dependency: "direct main" description: From 80e04887005b1da32095e6dc5bcca22001cf1026 Mon Sep 17 00:00:00 2001 From: NaiJi Date: Tue, 7 Jun 2022 22:59:15 +0300 Subject: [PATCH 44/52] Minor bug fixing Co-authored-by: Inex Code --- analysis_options.yaml | 1 - lib/logic/api_maps/backblaze.dart | 29 ++++--- lib/logic/api_maps/server.dart | 7 +- lib/logic/cubit/devices/devices_cubit.dart | 1 - .../recovery_key/recovery_key_state.dart | 8 ++ .../server_installation_repository.dart | 5 +- lib/ui/pages/devices/devices.dart | 81 ++++++++++++------- lib/ui/pages/recovery_key/recovery_key.dart | 13 ++- 8 files changed, 98 insertions(+), 47 deletions(-) diff --git a/analysis_options.yaml b/analysis_options.yaml index 3ab00ee3..9d16cb20 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -52,7 +52,6 @@ linter: sized_box_shrink_expand: true sort_constructors_first: true unnecessary_await_in_return: true - unnecessary_lambdas: true unnecessary_null_checks: true unnecessary_parenthesis: true use_enums: true diff --git a/lib/logic/api_maps/backblaze.dart b/lib/logic/api_maps/backblaze.dart index f6626a26..8d827e78 100644 --- a/lib/logic/api_maps/backblaze.dart +++ b/lib/logic/api_maps/backblaze.dart @@ -73,20 +73,25 @@ class BackblazeApi extends ApiMap { Future isValid(final String encodedApiKey) async { final Dio client = await getClient(); - final Response response = await client.get( - 'b2_authorize_account', - options: Options(headers: {'Authorization': 'Basic $encodedApiKey'}), - ); - close(client); - if (response.statusCode == HttpStatus.ok) { - if (response.data['allowed']['capabilities'].contains('listBuckets')) { - return true; + try { + final Response response = await client.get( + 'b2_authorize_account', + options: Options(headers: {'Authorization': 'Basic $encodedApiKey'}), + ); + if (response.statusCode == HttpStatus.ok) { + if (response.data['allowed']['capabilities'].contains('listBuckets')) { + return true; + } + return false; + } else if (response.statusCode == HttpStatus.unauthorized) { + return false; + } else { + throw Exception('code: ${response.statusCode}'); } + } on DioError { return false; - } else if (response.statusCode == HttpStatus.unauthorized) { - return false; - } else { - throw Exception('code: ${response.statusCode}'); + } finally { + close(client); } } diff --git a/lib/logic/api_maps/server.dart b/lib/logic/api_maps/server.dart index bdd544f7..67a0739c 100644 --- a/lib/logic/api_maps/server.dart +++ b/lib/logic/api_maps/server.dart @@ -447,7 +447,8 @@ class ServerApi extends ApiMap { final Dio client = await getClient(); try { response = await client.get('/services/restic/backup/list'); - backups = response.data.map(Backup.fromJson).toList(); + backups = + response.data.map((final e) => Backup.fromJson(e)).toList(); } on DioError catch (e) { print(e.message); } catch (e) { @@ -846,7 +847,9 @@ class ServerApi extends ApiMap { return ApiResponse( statusCode: code, data: (response.data != null) - ? response.data.map(ApiToken.fromJson).toList() + ? response.data + .map((final e) => ApiToken.fromJson(e)) + .toList() : [], ); } diff --git a/lib/logic/cubit/devices/devices_cubit.dart b/lib/logic/cubit/devices/devices_cubit.dart index 01f17a7d..f0380635 100644 --- a/lib/logic/cubit/devices/devices_cubit.dart +++ b/lib/logic/cubit/devices/devices_cubit.dart @@ -16,7 +16,6 @@ class ApiDevicesCubit @override void load() async { if (serverInstallationCubit.state is ServerInstallationFinished) { - emit(const ApiDevicesState([], LoadingStatus.refreshing)); final List? devices = await _getApiTokens(); if (devices != null) { emit(ApiDevicesState(devices, LoadingStatus.success)); diff --git a/lib/logic/cubit/recovery_key/recovery_key_state.dart b/lib/logic/cubit/recovery_key/recovery_key_state.dart index 1b764298..b35ae9a3 100644 --- a/lib/logic/cubit/recovery_key/recovery_key_state.dart +++ b/lib/logic/cubit/recovery_key/recovery_key_state.dart @@ -17,6 +17,14 @@ class RecoveryKeyState extends ServerInstallationDependendState { DateTime? get generatedAt => _status.date; DateTime? get expiresAt => _status.expiration; int? get usesLeft => _status.usesLeft; + + bool get isInvalidBecauseExpired => + _status.expiration != null && + _status.expiration!.isBefore(DateTime.now()); + + bool get isInvalidBecauseUsed => + _status.usesLeft != null && _status.usesLeft == 0; + @override List get props => [_status, loadingStatus]; diff --git a/lib/logic/cubit/server_installation/server_installation_repository.dart b/lib/logic/cubit/server_installation/server_installation_repository.dart index ea6948ec..5dc414f5 100644 --- a/lib/logic/cubit/server_installation/server_installation_repository.dart +++ b/lib/logic/cubit/server_installation/server_installation_repository.dart @@ -38,6 +38,7 @@ class ServerAuthorizationException implements Exception { class ServerInstallationRepository { Box box = Hive.box(BNames.serverInstallationBox); + Box usersBox = Hive.box(BNames.usersBox); Future load() async { final String? hetznerToken = getIt().hetznerKey; @@ -123,6 +124,7 @@ class ServerInstallationRepository { void clearAppConfig() { box.clear(); + usersBox.clear(); } Future startServer( @@ -526,7 +528,7 @@ class ServerInstallationRepository { ), provider: ServerProvider.unknown, id: 0, - ip4: '', + ip4: serverIp, startTime: null, createTime: null, ); @@ -668,7 +670,6 @@ class ServerInstallationRepository { BNames.isServerResetedSecondTime, BNames.hasFinalChecked, BNames.isLoading, - BNames.isRecoveringServer, ]); getIt().init(); } diff --git a/lib/ui/pages/devices/devices.dart b/lib/ui/pages/devices/devices.dart index 5883064c..ad48096e 100644 --- a/lib/ui/pages/devices/devices.dart +++ b/lib/ui/pages/devices/devices.dart @@ -1,6 +1,7 @@ import 'package:cubit_form/cubit_form.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +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'; @@ -30,6 +31,58 @@ class _DevicesScreenState extends State { heroSubtitle: 'devices.main_screen.description'.tr(), hasBackButton: true, hasFlashButton: false, + children: [ + if (devicesStatus.status == LoadingStatus.uninitialized) ...[ + const Center( + heightFactor: 8, + child: CircularProgressIndicator(), + ), + ], + if (devicesStatus.status != LoadingStatus.uninitialized) ...[ + _DevicesInfo( + devicesStatus: devicesStatus, + ), + const SizedBox(height: 16), + OutlinedButton( + onPressed: () => Navigator.of(context) + .push(materialRoute(const NewDeviceScreen())), + child: Text('devices.main_screen.authorize_new_device'.tr()), + ), + const SizedBox(height: 16), + const Divider(height: 1), + const SizedBox(height: 16), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon( + Icons.info_outline, + color: Theme.of(context).colorScheme.onBackground, + ), + const SizedBox(height: 16), + Text( + 'devices.main_screen.tip'.tr(), + style: Theme.of(context).textTheme.bodyMedium, + ), + ], + ), + ], + const SizedBox(height: 24), + ], + ), + ); + } +} + +class _DevicesInfo extends StatelessWidget { + const _DevicesInfo({ + required this.devicesStatus, + }); + + final ApiDevicesState devicesStatus; + + @override + Widget build(final BuildContext context) => Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'devices.main_screen.this_device'.tr(), @@ -49,34 +102,8 @@ class _DevicesScreenState extends State { ...devicesStatus.otherDevices .map((final device) => _DeviceTile(device: device)) .toList(), - const SizedBox(height: 16), - OutlinedButton( - onPressed: () => Navigator.of(context) - .push(materialRoute(const NewDeviceScreen())), - child: Text('devices.main_screen.authorize_new_device'.tr()), - ), - const SizedBox(height: 16), - const Divider(height: 1), - const SizedBox(height: 16), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Icon( - Icons.info_outline, - color: Theme.of(context).colorScheme.onBackground, - ), - const SizedBox(height: 16), - Text( - 'devices.main_screen.tip'.tr(), - style: Theme.of(context).textTheme.bodyMedium!, - ), - ], - ), - const SizedBox(height: 24), ], - ), - ); - } + ); } class _DeviceTile extends StatelessWidget { diff --git a/lib/ui/pages/recovery_key/recovery_key.dart b/lib/ui/pages/recovery_key/recovery_key.dart index d7167a6f..0b506d88 100644 --- a/lib/ui/pages/recovery_key/recovery_key.dart +++ b/lib/ui/pages/recovery_key/recovery_key.dart @@ -151,7 +151,7 @@ class RecoveryKeyInformation extends StatelessWidget { @override Widget build(final BuildContext context) { const EdgeInsets padding = - EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0); + EdgeInsets.symmetric(vertical: 8.0, horizontal: 8.0); return SizedBox( width: double.infinity, child: Column( @@ -164,6 +164,11 @@ class RecoveryKeyInformation extends StatelessWidget { 'recovery_key.key_valid_until'.tr( args: [DateFormat.yMMMMd().format(state.expiresAt!)], ), + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: state.isInvalidBecauseExpired + ? Theme.of(context).colorScheme.error + : Theme.of(context).colorScheme.onBackground, + ), ), ), if (state.usesLeft != null) @@ -173,6 +178,11 @@ class RecoveryKeyInformation extends StatelessWidget { 'recovery_key.key_valid_for'.tr( args: [state.usesLeft!.toString()], ), + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: state.isInvalidBecauseUsed + ? Theme.of(context).colorScheme.error + : Theme.of(context).colorScheme.onBackground, + ), ), ), if (state.generatedAt != null) @@ -182,7 +192,6 @@ class RecoveryKeyInformation extends StatelessWidget { 'recovery_key.key_creation_date'.tr( args: [DateFormat.yMMMMd().format(state.generatedAt!)], ), - textAlign: TextAlign.start, ), ), ], From 43411adf2c5685395b55d9351669c1ec42c180a4 Mon Sep 17 00:00:00 2001 From: NaiJi Date: Thu, 9 Jun 2022 07:36:22 +0300 Subject: [PATCH 45/52] Bugfix About application page for desktop --- lib/ui/pages/more/info/info.dart | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/ui/pages/more/info/info.dart b/lib/ui/pages/more/info/info.dart index 04de405e..ac4eeff2 100644 --- a/lib/ui/pages/more/info/info.dart +++ b/lib/ui/pages/more/info/info.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:selfprivacy/config/brand_theme.dart'; import 'package:selfprivacy/ui/components/brand_divider/brand_divider.dart'; import 'package:selfprivacy/ui/components/brand_header/brand_header.dart'; @@ -35,7 +36,14 @@ class InfoPage extends StatelessWidget { ); Future _version() async { - final PackageInfo packageInfo = await PackageInfo.fromPlatform(); - return packageInfo.version; + String packageVersion = 'unknown'; + try { + final PackageInfo packageInfo = await PackageInfo.fromPlatform(); + packageVersion = packageInfo.version; + } catch (e) { + print(e); + } + + return packageVersion; } } From 3fbdc05469d2d046c743eb1e9f8bacf7b68a93c7 Mon Sep 17 00:00:00 2001 From: NaiJi Date: Thu, 9 Jun 2022 09:51:29 +0300 Subject: [PATCH 46/52] Minor flow bugfixes --- lib/logic/api_maps/hetzner.dart | 63 ++++++++++++------- .../server_installation_repository.dart | 13 +++- lib/ui/pages/more/info/info.dart | 1 - 3 files changed, 50 insertions(+), 27 deletions(-) diff --git a/lib/logic/api_maps/hetzner.dart b/lib/logic/api_maps/hetzner.dart index 54d60c21..4de4f36f 100644 --- a/lib/logic/api_maps/hetzner.dart +++ b/lib/logic/api_maps/hetzner.dart @@ -76,7 +76,7 @@ class HetznerApi extends ApiMap { ); } - Future createServer({ + Future createServer({ required final String cloudFlareKey, required final User rootUser, required final String domainName, @@ -117,21 +117,32 @@ class HetznerApi extends ApiMap { }; print('Decoded data: $data'); - final Response serverCreateResponse = await client.post( - '/servers', - data: data, - ); + ServerHostingDetails? serverDetails; - print(serverCreateResponse.data); - client.close(); - return ServerHostingDetails( - id: serverCreateResponse.data['server']['id'], - ip4: serverCreateResponse.data['server']['public_net']['ipv4']['ip'], - createTime: DateTime.now(), - volume: dataBase, - apiToken: apiToken, - provider: ServerProvider.hetzner, - ); + try { + final Response serverCreateResponse = await client.post( + '/servers', + data: data, + ); + print(serverCreateResponse.data); + serverDetails = ServerHostingDetails( + id: serverCreateResponse.data['server']['id'], + ip4: serverCreateResponse.data['server']['public_net']['ipv4']['ip'], + createTime: DateTime.now(), + volume: dataBase, + apiToken: apiToken, + provider: ServerProvider.hetzner, + ); + } on DioError catch (e) { + print(e); + rethrow; + } catch (e) { + print(e); + } finally { + client.close(); + } + + return serverDetails; } static String getHostnameFromDomain(final String domain) { @@ -247,14 +258,20 @@ class HetznerApi extends ApiMap { }) async { final ServerHostingDetails? hetznerServer = getIt().serverDetails; + final Dio client = await getClient(); - await client.post( - '/servers/${hetznerServer!.id}/actions/change_dns_ptr', - data: { - 'ip': ip4, - 'dns_ptr': domainName, - }, - ); - close(client); + try { + await client.post( + '/servers/${hetznerServer!.id}/actions/change_dns_ptr', + data: { + 'ip': ip4, + 'dns_ptr': domainName, + }, + ); + } catch (e) { + print(e); + } finally { + close(client); + } } } diff --git a/lib/logic/cubit/server_installation/server_installation_repository.dart b/lib/logic/cubit/server_installation/server_installation_repository.dart index 5dc414f5..891c55bb 100644 --- a/lib/logic/cubit/server_installation/server_installation_repository.dart +++ b/lib/logic/cubit/server_installation/server_installation_repository.dart @@ -214,12 +214,16 @@ class ServerInstallationRepository { try { dataBase = await hetznerApi.createVolume(); - final ServerHostingDetails serverDetails = await hetznerApi.createServer( + final ServerHostingDetails? serverDetails = await hetznerApi.createServer( cloudFlareKey: cloudFlareKey, rootUser: rootUser, domainName: domainName, dataBase: dataBase, ); + if (serverDetails == null) { + print('Server is not initialized!'); + return; + } saveServerDetails(serverDetails); onSuccess(serverDetails); } on DioError catch (e) { @@ -238,14 +242,17 @@ class ServerInstallationRepository { domainName: domainName, ); - final ServerHostingDetails serverDetails = + final ServerHostingDetails? serverDetails = await hetznerApi.createServer( cloudFlareKey: cloudFlareKey, rootUser: rootUser, domainName: domainName, dataBase: dataBase, ); - + if (serverDetails == null) { + print('Server is not initialized!'); + return; + } await saveServerDetails(serverDetails); onSuccess(serverDetails); }, diff --git a/lib/ui/pages/more/info/info.dart b/lib/ui/pages/more/info/info.dart index ac4eeff2..d4c4863b 100644 --- a/lib/ui/pages/more/info/info.dart +++ b/lib/ui/pages/more/info/info.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:selfprivacy/config/brand_theme.dart'; import 'package:selfprivacy/ui/components/brand_divider/brand_divider.dart'; import 'package:selfprivacy/ui/components/brand_header/brand_header.dart'; From 3c3cb376e216e2ffbb9d9665b96f75118f244849 Mon Sep 17 00:00:00 2001 From: Inex Code Date: Thu, 9 Jun 2022 19:15:53 +0300 Subject: [PATCH 47/52] Fix null check on DNS check --- .../cubit/server_installation/server_installation_cubit.dart | 2 +- .../server_installation/server_installation_repository.dart | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/logic/cubit/server_installation/server_installation_cubit.dart b/lib/logic/cubit/server_installation/server_installation_cubit.dart index 3f156e4b..622abaf7 100644 --- a/lib/logic/cubit/server_installation/server_installation_cubit.dart +++ b/lib/logic/cubit/server_installation/server_installation_cubit.dart @@ -159,7 +159,7 @@ class ServerInstallationCubit extends Cubit { final Map matches = await repository.isDnsAddressesMatch( domainName, ip4, - dataState.dnsMatches, + dataState.dnsMatches ?? {}, ); if (matches.values.every((final bool value) => value)) { diff --git a/lib/logic/cubit/server_installation/server_installation_repository.dart b/lib/logic/cubit/server_installation/server_installation_repository.dart index 891c55bb..5d4db8fe 100644 --- a/lib/logic/cubit/server_installation/server_installation_repository.dart +++ b/lib/logic/cubit/server_installation/server_installation_repository.dart @@ -153,7 +153,7 @@ class ServerInstallationRepository { Future> isDnsAddressesMatch( final String? domainName, final String? ip4, - final Map? skippedMatches, + final Map skippedMatches, ) async { final List addresses = [ '$domainName', @@ -166,7 +166,7 @@ class ServerInstallationRepository { final Map matches = {}; for (final String address in addresses) { - if (skippedMatches![address] ?? false) { + if (skippedMatches[address] ?? false) { matches[address] = true; continue; } From bf03f61668b218f9870c91e34092ed06bab54b01 Mon Sep 17 00:00:00 2001 From: Inex Code Date: Thu, 9 Jun 2022 19:49:57 +0300 Subject: [PATCH 48/52] Bump version --- lib/ui/pages/recovery_key/recovery_key.dart | 4 ++-- pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/ui/pages/recovery_key/recovery_key.dart b/lib/ui/pages/recovery_key/recovery_key.dart index 0b506d88..f808f1c3 100644 --- a/lib/ui/pages/recovery_key/recovery_key.dart +++ b/lib/ui/pages/recovery_key/recovery_key.dart @@ -83,7 +83,7 @@ class _RecoveryKeyContentState extends State { if (_isConfigurationVisible || !keyStatus.exists) const RecoveryKeyConfiguration(), const SizedBox(height: 16), - if (!_isConfigurationVisible && keyStatus.isValid) + if (!_isConfigurationVisible && keyStatus.isValid && keyStatus.exists) BrandButton.text( title: 'recovery_key.key_replace_button'.tr(), onPressed: () { @@ -92,7 +92,7 @@ class _RecoveryKeyContentState extends State { }); }, ), - if (!_isConfigurationVisible && !keyStatus.isValid) + if (!_isConfigurationVisible && !keyStatus.isValid && keyStatus.exists) FilledButton( title: 'recovery_key.key_replace_button'.tr(), onPressed: () { diff --git a/pubspec.yaml b/pubspec.yaml index 519f9996..71a5f2f9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: selfprivacy description: selfprivacy.org publish_to: 'none' -version: 0.5.3+14 +version: 0.6.0+15 environment: sdk: '>=2.17.0 <3.0.0' From ad53000415a63627c4177e6f94f23c9fa39c2a9f Mon Sep 17 00:00:00 2001 From: NaiJi Date: Thu, 9 Jun 2022 23:25:42 +0300 Subject: [PATCH 49/52] Add recovery manuals Co-authored-by: Inex Code --- assets/markdown/how_fallback_old-en.md | 18 ++------ assets/markdown/how_fallback_old-ru.md | 16 ++------ assets/markdown/how_fallback_ssh-en.md | 34 ++++++++------- assets/markdown/how_fallback_ssh-ru.md | 32 +++++++++------ assets/markdown/how_fallback_terminal-en.md | 41 ++++++++++++------- assets/markdown/how_fallback_terminal-ru.md | 39 ++++++++++++------ .../android/en-US/changelogs/0.6.0.txt | 6 +++ 7 files changed, 102 insertions(+), 84 deletions(-) create mode 100644 fastlane/metadata/android/en-US/changelogs/0.6.0.txt diff --git a/assets/markdown/how_fallback_old-en.md b/assets/markdown/how_fallback_old-en.md index 368ea83a..c12504e7 100644 --- a/assets/markdown/how_fallback_old-en.md +++ b/assets/markdown/how_fallback_old-en.md @@ -1,15 +1,3 @@ -### How to get Cloudflare API Token -1. Visit the following link: https://dash.cloudflare.com/ -2. the right corner, click on the profile icon (a man in a circle). For the mobile version of the site, in the upper left corner, click the **Menu** button (three horizontal bars), in the dropdown menu, click on **My Profile** -3. There are four configuration categories to choose from: *Communication*, *Authentication*, **API Tokens**, *Session*. Choose **API Tokens**. -4. Click on **Create Token** button. -5. Go down to the bottom and see the **Create Custom Token** field and press **Get Started** button on the right side. -6. In the **Token Name** field, give your token a name. -7. Next we have Permissions. In the leftmost field, select **Zone**. In the longest field, center, select **DNS**. In the rightmost field, select **Edit**. -8. Next, right under this line, click Add More. Similar field will appear. -9. In the leftmost field of the new line, select, similar to the last line — **Zone**. In the center — a little different. Here choose the same as in the left — **Zone**. In the rightmost field, select **Read**. -10. Next look at **Zone Resources**. Under this inscription there is a line with two fields. The left must have **Include** and the right must have **Specific Zone**. Once you select Specific Zone, another field appears on the right. Choose your domain in it. -11. Flick to the bottom and press the blue **Continue to Summary** button. -12. Check if you got everything right. A similar string must be present: *Domain — DNS:Edit, Zone:Read*. -13. Click on **Create Token**. -14. We copy the created token, and save it in a reliable place (preferably in the password manager). +In the next window, enter the token obtained from the console of the previous version of the application. + +Enter it without the word *Bearer*. diff --git a/assets/markdown/how_fallback_old-ru.md b/assets/markdown/how_fallback_old-ru.md index 2c0ad22b..1d0a43f7 100644 --- a/assets/markdown/how_fallback_old-ru.md +++ b/assets/markdown/how_fallback_old-ru.md @@ -1,13 +1,3 @@ -### Как получить Cloudflare API Token -1. Переходим по [ссылке](https://dash.cloudflare.com/) и авторизуемся в ранее созданном аккаунте. https://dash.cloudflare.com/ -В правом углу кликаем на иконку профиля (человечек в кружочке). Для мобильной версии сайта, в верхнем левом углу, нажимаем кнопку **Меню** (три горизонтальных полоски), в выпавшем меню, ищем пункт **My Profile**. -3. Нам предлагается на выбор, четыре категории настройки: **Preferences**, **Authentication**, **API Tokens**, **Sessions**. Выбираем **API Tokens**. -4. Самым первым пунктом видим кнопку **Create Token**. С полной уверенностью в себе и желанием обрести приватность, нажимаем на неё. -5. Спускаемся в самый низ и видим поле **Create Custom Token** и кнопку **Get Started** с правой стороны. Нажимаем. -6. В поле **Token Name** даём своему токену имя. Можете покреативить и отнестись к этому как к наименованию домашнего зверька :) -7. Далее, у нас **Permissions**. В первом поле выбираем Zone. Во втором поле, по центру, выбираем **DNS**. В последнем поле выбираем **Edit**. -8. Далее смотрим на **Zone Resources**. Под этой надписью есть строка с двумя полями. В первом должно быть **Include**, а во втором — **Specific Zone**. Как только Вы выберите **Specific Zone**, справа появится ещё одно поле. В нём выбираем наш домен. -9. Листаем в самый низ и нажимаем на синюю кнопку **Continue to Summary**. -10. Проверяем, всё ли мы правильно выбрали. Должна присутствовать подобная строка: ваш.домен — **DNS:Edit, Zone:Read**. -11. Нажимаем **Create Token**. -12. Копируем созданный токен, и сохраняем его в надёжном месте (желательно — в менеджере паролей). \ No newline at end of file +Введите в следующем окне токен, полученный из консоли прошлой версии приложения. + +Вводить нужно без слова *Bearer*. diff --git a/assets/markdown/how_fallback_ssh-en.md b/assets/markdown/how_fallback_ssh-en.md index 368ea83a..ce90e76a 100644 --- a/assets/markdown/how_fallback_ssh-en.md +++ b/assets/markdown/how_fallback_ssh-en.md @@ -1,15 +1,19 @@ -### How to get Cloudflare API Token -1. Visit the following link: https://dash.cloudflare.com/ -2. the right corner, click on the profile icon (a man in a circle). For the mobile version of the site, in the upper left corner, click the **Menu** button (three horizontal bars), in the dropdown menu, click on **My Profile** -3. There are four configuration categories to choose from: *Communication*, *Authentication*, **API Tokens**, *Session*. Choose **API Tokens**. -4. Click on **Create Token** button. -5. Go down to the bottom and see the **Create Custom Token** field and press **Get Started** button on the right side. -6. In the **Token Name** field, give your token a name. -7. Next we have Permissions. In the leftmost field, select **Zone**. In the longest field, center, select **DNS**. In the rightmost field, select **Edit**. -8. Next, right under this line, click Add More. Similar field will appear. -9. In the leftmost field of the new line, select, similar to the last line — **Zone**. In the center — a little different. Here choose the same as in the left — **Zone**. In the rightmost field, select **Read**. -10. Next look at **Zone Resources**. Under this inscription there is a line with two fields. The left must have **Include** and the right must have **Specific Zone**. Once you select Specific Zone, another field appears on the right. Choose your domain in it. -11. Flick to the bottom and press the blue **Continue to Summary** button. -12. Check if you got everything right. A similar string must be present: *Domain — DNS:Edit, Zone:Read*. -13. Click on **Create Token**. -14. We copy the created token, and save it in a reliable place (preferably in the password manager). +Login as root user to your server and look at the contents of the file `/etc/nixos/userdata/tokens.json` + +```sh +cat /etc/nixos/userdata/tokens.json +``` + +This file will have a similar construction: + +```json +{ + "tokens": [ + { + "token": "token_to_copy", + "name": "device_name", + "date": "date" + } +``` + +Copy the token from the file and paste it in the next window. diff --git a/assets/markdown/how_fallback_ssh-ru.md b/assets/markdown/how_fallback_ssh-ru.md index 2c0ad22b..11a32875 100644 --- a/assets/markdown/how_fallback_ssh-ru.md +++ b/assets/markdown/how_fallback_ssh-ru.md @@ -1,13 +1,19 @@ -### Как получить Cloudflare API Token -1. Переходим по [ссылке](https://dash.cloudflare.com/) и авторизуемся в ранее созданном аккаунте. https://dash.cloudflare.com/ -В правом углу кликаем на иконку профиля (человечек в кружочке). Для мобильной версии сайта, в верхнем левом углу, нажимаем кнопку **Меню** (три горизонтальных полоски), в выпавшем меню, ищем пункт **My Profile**. -3. Нам предлагается на выбор, четыре категории настройки: **Preferences**, **Authentication**, **API Tokens**, **Sessions**. Выбираем **API Tokens**. -4. Самым первым пунктом видим кнопку **Create Token**. С полной уверенностью в себе и желанием обрести приватность, нажимаем на неё. -5. Спускаемся в самый низ и видим поле **Create Custom Token** и кнопку **Get Started** с правой стороны. Нажимаем. -6. В поле **Token Name** даём своему токену имя. Можете покреативить и отнестись к этому как к наименованию домашнего зверька :) -7. Далее, у нас **Permissions**. В первом поле выбираем Zone. Во втором поле, по центру, выбираем **DNS**. В последнем поле выбираем **Edit**. -8. Далее смотрим на **Zone Resources**. Под этой надписью есть строка с двумя полями. В первом должно быть **Include**, а во втором — **Specific Zone**. Как только Вы выберите **Specific Zone**, справа появится ещё одно поле. В нём выбираем наш домен. -9. Листаем в самый низ и нажимаем на синюю кнопку **Continue to Summary**. -10. Проверяем, всё ли мы правильно выбрали. Должна присутствовать подобная строка: ваш.домен — **DNS:Edit, Zone:Read**. -11. Нажимаем **Create Token**. -12. Копируем созданный токен, и сохраняем его в надёжном месте (желательно — в менеджере паролей). \ No newline at end of file +Войдите как root пользователь на свой сервер и посмотрите содерижмое файла `/etc/nixos/userdata/tokens.json` + +```sh +cat /etc/nixos/userdata/tokens.json +``` + +В этом файле будет схожая конструкция: + +```json +{ + "tokens": [ + { + "token": "токен_который_надо_скопировать", + "name": "имя_устройства", + "date": "дата" + } +``` + +Скопируйте токен из файла и вставьте в следующем окне. diff --git a/assets/markdown/how_fallback_terminal-en.md b/assets/markdown/how_fallback_terminal-en.md index 368ea83a..760b49df 100644 --- a/assets/markdown/how_fallback_terminal-en.md +++ b/assets/markdown/how_fallback_terminal-en.md @@ -1,15 +1,26 @@ -### How to get Cloudflare API Token -1. Visit the following link: https://dash.cloudflare.com/ -2. the right corner, click on the profile icon (a man in a circle). For the mobile version of the site, in the upper left corner, click the **Menu** button (three horizontal bars), in the dropdown menu, click on **My Profile** -3. There are four configuration categories to choose from: *Communication*, *Authentication*, **API Tokens**, *Session*. Choose **API Tokens**. -4. Click on **Create Token** button. -5. Go down to the bottom and see the **Create Custom Token** field and press **Get Started** button on the right side. -6. In the **Token Name** field, give your token a name. -7. Next we have Permissions. In the leftmost field, select **Zone**. In the longest field, center, select **DNS**. In the rightmost field, select **Edit**. -8. Next, right under this line, click Add More. Similar field will appear. -9. In the leftmost field of the new line, select, similar to the last line — **Zone**. In the center — a little different. Here choose the same as in the left — **Zone**. In the rightmost field, select **Read**. -10. Next look at **Zone Resources**. Under this inscription there is a line with two fields. The left must have **Include** and the right must have **Specific Zone**. Once you select Specific Zone, another field appears on the right. Choose your domain in it. -11. Flick to the bottom and press the blue **Continue to Summary** button. -12. Check if you got everything right. A similar string must be present: *Domain — DNS:Edit, Zone:Read*. -13. Click on **Create Token**. -14. We copy the created token, and save it in a reliable place (preferably in the password manager). +In the server control panel in Hetzner, go to the **Rescue** tab. Then, click on **Enable rescue & power cycle**. + +In *Choose a Recue OS* select **linux64**, and in *SSH Key* select your key if it has been added to your Hetzner account. + +Click **Enable rescue & power cycle** and wait for the server to reboot. The login and password will be displayed on the screen. Login to the root user using your login and password information. + +Mount your server file system and see the contents of the token file: + +```sh +mount /dev/sda1 /mnt +cat /mnt/etc/nixos/userdata/tokens.json +``` + +This file will have a similar construction: + +```json +{ + "tokens": [ + { + "token": "token_to_copy", + "name": "device_name", + "date": "date" + } +``` + +Copy the token from the file and paste it in the next window. diff --git a/assets/markdown/how_fallback_terminal-ru.md b/assets/markdown/how_fallback_terminal-ru.md index 2c0ad22b..94d357c7 100644 --- a/assets/markdown/how_fallback_terminal-ru.md +++ b/assets/markdown/how_fallback_terminal-ru.md @@ -1,13 +1,26 @@ -### Как получить Cloudflare API Token -1. Переходим по [ссылке](https://dash.cloudflare.com/) и авторизуемся в ранее созданном аккаунте. https://dash.cloudflare.com/ -В правом углу кликаем на иконку профиля (человечек в кружочке). Для мобильной версии сайта, в верхнем левом углу, нажимаем кнопку **Меню** (три горизонтальных полоски), в выпавшем меню, ищем пункт **My Profile**. -3. Нам предлагается на выбор, четыре категории настройки: **Preferences**, **Authentication**, **API Tokens**, **Sessions**. Выбираем **API Tokens**. -4. Самым первым пунктом видим кнопку **Create Token**. С полной уверенностью в себе и желанием обрести приватность, нажимаем на неё. -5. Спускаемся в самый низ и видим поле **Create Custom Token** и кнопку **Get Started** с правой стороны. Нажимаем. -6. В поле **Token Name** даём своему токену имя. Можете покреативить и отнестись к этому как к наименованию домашнего зверька :) -7. Далее, у нас **Permissions**. В первом поле выбираем Zone. Во втором поле, по центру, выбираем **DNS**. В последнем поле выбираем **Edit**. -8. Далее смотрим на **Zone Resources**. Под этой надписью есть строка с двумя полями. В первом должно быть **Include**, а во втором — **Specific Zone**. Как только Вы выберите **Specific Zone**, справа появится ещё одно поле. В нём выбираем наш домен. -9. Листаем в самый низ и нажимаем на синюю кнопку **Continue to Summary**. -10. Проверяем, всё ли мы правильно выбрали. Должна присутствовать подобная строка: ваш.домен — **DNS:Edit, Zone:Read**. -11. Нажимаем **Create Token**. -12. Копируем созданный токен, и сохраняем его в надёжном месте (желательно — в менеджере паролей). \ No newline at end of file +В панели управления сервером в Hetzner перейдите во вкладку **Rescue**. Затем, нажмите на кнопку **Enable rescue & power cycle**. + +В поле *Choose a Recue OS* выберите **linux64**, а в *SSH Key* свой ключ, если он был добавлен в ваш аккаунт Hetzner. + +Нажмите **Enable rescue & power cycle** и подождите перезагрузки сервера. На экране будет отображён пароль для входа. Войдите в root пользователя используя данные логин и пароль. + +Примонтируйте файловую систему вашего сервера и посмотрите содерижмое файла с токенами: + +```sh +mount /dev/sda1 /mnt +cat /mnt/etc/nixos/userdata/tokens.json +``` + +В этом файле будет схожая конструкция: + +```json +{ + "tokens": [ + { + "token": "токен_который_надо_скопировать", + "name": "имя_устройства", + "date": "дата" + } +``` + +Скопируйте токен из файла и вставьте в следующем окне. diff --git a/fastlane/metadata/android/en-US/changelogs/0.6.0.txt b/fastlane/metadata/android/en-US/changelogs/0.6.0.txt new file mode 100644 index 00000000..b8e98f1c --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/0.6.0.txt @@ -0,0 +1,6 @@ +- Added support for multi-device server access from SelfPrivacy app. +- You can now create recovery token to regain the access to the server if you lose your device or the app's data. +- You can now connect to an existing server, instead of creating a new one. +- Initial support for Material Design 3 (Material You). +- App now uses your system colors on Android 12 (Material You), Windows 10 (accent color) and Linux (GTK colors). +- Minor bug fixes. From 18d0c2c40fda899f587039609dc511359565e18c Mon Sep 17 00:00:00 2001 From: Inex Code Date: Fri, 10 Jun 2022 00:13:06 +0300 Subject: [PATCH 50/52] Bug fixes and linting --- lib/config/brand_theme.dart | 3 +- lib/config/hive_config.dart | 3 +- lib/config/text_themes.dart | 6 ++- .../setup/initializing/domain_cloudflare.dart | 1 - .../cubit/providers/providers_cubit.dart | 3 +- .../recovery_key/recovery_key_cubit.dart | 12 +++-- .../server_detailed_info_cubit.dart | 14 ++--- .../server_detailed_info_repository.dart | 1 - .../server_installation_cubit.dart | 15 ++++-- lib/logic/models/hive/backblaze_bucket.dart | 11 ++-- lib/logic/models/hive/server_details.dart | 19 +++---- lib/logic/models/json/api_token.dart | 1 - lib/logic/models/json/backup.dart | 14 ++--- lib/logic/models/json/device_token.dart | 1 - lib/logic/models/server_status.dart | 9 ++-- lib/ui/components/brand_md/brand_md.dart | 2 +- lib/ui/components/error/error.dart | 24 ++++----- lib/ui/pages/devices/new_device.dart | 2 +- lib/ui/pages/recovery_key/recovery_key.dart | 4 +- lib/ui/pages/server_details/cpu_chart.dart | 4 +- .../pages/server_details/network_charts.dart | 4 +- lib/ui/pages/setup/initializing.dart | 9 +++- .../recovering/recover_by_new_device_key.dart | 4 +- .../recovering/recover_by_old_token.dart | 4 +- .../recovering/recover_by_recovery_key.dart | 4 +- .../recovery_confirm_backblaze.dart | 14 +++-- .../recovery_confirm_cloudflare.dart | 12 +++-- .../recovery_hentzner_connected.dart | 12 +++-- .../recovering/recovery_method_select.dart | 2 + .../setup/recovering/recovery_routing.dart | 4 -- lib/ui/pages/users/user_details.dart | 3 +- lib/utils/extensions/text_extensions.dart | 7 ++- lib/utils/route_transitions/slide_bottom.dart | 39 ++++++++------ lib/utils/route_transitions/slide_right.dart | 39 ++++++++------ test/widget_test.dart | 54 +++++++++++-------- 35 files changed, 208 insertions(+), 152 deletions(-) diff --git a/lib/config/brand_theme.dart b/lib/config/brand_theme.dart index 9487fe5c..3ad0623c 100644 --- a/lib/config/brand_theme.dart +++ b/lib/config/brand_theme.dart @@ -82,6 +82,7 @@ ThemeData darkTheme = lightTheme.copyWith( ), ); -const EdgeInsets paddingH15V30 = EdgeInsets.symmetric(horizontal: 15, vertical: 30); +const EdgeInsets paddingH15V30 = + EdgeInsets.symmetric(horizontal: 15, vertical: 30); const EdgeInsets paddingH15V0 = EdgeInsets.symmetric(horizontal: 15); diff --git a/lib/config/hive_config.dart b/lib/config/hive_config.dart index f4d67c7c..03355311 100644 --- a/lib/config/hive_config.dart +++ b/lib/config/hive_config.dart @@ -25,7 +25,8 @@ class HiveConfig { await Hive.openBox(BNames.appSettingsBox); final HiveAesCipher cipher = HiveAesCipher( - await getEncryptedKey(BNames.serverInstallationEncryptionKey),); + await getEncryptedKey(BNames.serverInstallationEncryptionKey), + ); await Hive.openBox(BNames.usersDeprecated); await Hive.openBox(BNames.usersBox, encryptionCipher: cipher); diff --git a/lib/config/text_themes.dart b/lib/config/text_themes.dart index b7224622..63b4b99c 100644 --- a/lib/config/text_themes.dart +++ b/lib/config/text_themes.dart @@ -63,9 +63,11 @@ final TextStyle buttonTitleText = defaultTextStyle.copyWith( height: 1, ); -final TextStyle mediumStyle = defaultTextStyle.copyWith(fontSize: 13, height: 1.53); +final TextStyle mediumStyle = + defaultTextStyle.copyWith(fontSize: 13, height: 1.53); -final TextStyle smallStyle = defaultTextStyle.copyWith(fontSize: 11, height: 1.45); +final TextStyle smallStyle = + defaultTextStyle.copyWith(fontSize: 11, height: 1.45); const TextStyle progressTextStyleLight = TextStyle( fontSize: 11, diff --git a/lib/logic/cubit/forms/setup/initializing/domain_cloudflare.dart b/lib/logic/cubit/forms/setup/initializing/domain_cloudflare.dart index db7044f4..89b50a62 100644 --- a/lib/logic/cubit/forms/setup/initializing/domain_cloudflare.dart +++ b/lib/logic/cubit/forms/setup/initializing/domain_cloudflare.dart @@ -61,7 +61,6 @@ class Loading extends DomainSetupState { enum LoadingTypes { loadingDomain, saving } class Loaded extends DomainSetupState { - Loaded(this.domain); final String domain; } diff --git a/lib/logic/cubit/providers/providers_cubit.dart b/lib/logic/cubit/providers/providers_cubit.dart index eb33046f..d3ce60b9 100644 --- a/lib/logic/cubit/providers/providers_cubit.dart +++ b/lib/logic/cubit/providers/providers_cubit.dart @@ -12,7 +12,8 @@ class ProvidersCubit extends Cubit { ProvidersCubit() : super(InitialProviderState()); void connect(final ProviderModel provider) { - final ProvidersState newState = state.updateElement(provider, StateType.stable); + final ProvidersState newState = + state.updateElement(provider, StateType.stable); emit(newState); } } diff --git a/lib/logic/cubit/recovery_key/recovery_key_cubit.dart b/lib/logic/cubit/recovery_key/recovery_key_cubit.dart index 032fd31b..abd7b2fa 100644 --- a/lib/logic/cubit/recovery_key/recovery_key_cubit.dart +++ b/lib/logic/cubit/recovery_key/recovery_key_cubit.dart @@ -19,8 +19,12 @@ class RecoveryKeyCubit if (status == null) { emit(state.copyWith(loadingStatus: LoadingStatus.error)); } else { - emit(state.copyWith( - status: status, loadingStatus: LoadingStatus.success,),); + emit( + state.copyWith( + status: status, + loadingStatus: LoadingStatus.success, + ), + ); } } else { emit(state.copyWith(loadingStatus: LoadingStatus.uninitialized)); @@ -44,7 +48,8 @@ class RecoveryKeyCubit emit(state.copyWith(loadingStatus: LoadingStatus.error)); } else { emit( - state.copyWith(status: status, loadingStatus: LoadingStatus.success),); + state.copyWith(status: status, loadingStatus: LoadingStatus.success), + ); } } @@ -69,7 +74,6 @@ class RecoveryKeyCubit } class GenerationError extends Error { - GenerationError(this.message); final String message; } diff --git a/lib/logic/cubit/server_detailed_info/server_detailed_info_cubit.dart b/lib/logic/cubit/server_detailed_info/server_detailed_info_cubit.dart index ac5cbec5..613069b0 100644 --- a/lib/logic/cubit/server_detailed_info/server_detailed_info_cubit.dart +++ b/lib/logic/cubit/server_detailed_info/server_detailed_info_cubit.dart @@ -18,12 +18,14 @@ class ServerDetailsCubit extends Cubit { if (isReadyToCheck) { emit(ServerDetailsLoading()); final ServerDetailsRepositoryDto data = await repository.load(); - emit(Loaded( - serverInfo: data.hetznerServerInfo, - autoUpgradeSettings: data.autoUpgradeSettings, - serverTimezone: data.serverTimezone, - checkTime: DateTime.now(), - ),); + emit( + Loaded( + serverInfo: data.hetznerServerInfo, + autoUpgradeSettings: data.autoUpgradeSettings, + serverTimezone: data.serverTimezone, + checkTime: DateTime.now(), + ), + ); } else { emit(ServerDetailsNotReady()); } diff --git a/lib/logic/cubit/server_detailed_info/server_detailed_info_repository.dart b/lib/logic/cubit/server_detailed_info/server_detailed_info_repository.dart index 00bd9ec4..97dc6292 100644 --- a/lib/logic/cubit/server_detailed_info/server_detailed_info_repository.dart +++ b/lib/logic/cubit/server_detailed_info/server_detailed_info_repository.dart @@ -19,7 +19,6 @@ class ServerDetailsRepository { } class ServerDetailsRepositoryDto { - ServerDetailsRepositoryDto({ required this.hetznerServerInfo, required this.serverTimezone, diff --git a/lib/logic/cubit/server_installation/server_installation_cubit.dart b/lib/logic/cubit/server_installation/server_installation_cubit.dart index 622abaf7..ef83104d 100644 --- a/lib/logic/cubit/server_installation/server_installation_cubit.dart +++ b/lib/logic/cubit/server_installation/server_installation_cubit.dart @@ -146,8 +146,9 @@ class ServerInstallationCubit extends Cubit { } } - void startServerIfDnsIsOkay( - {final ServerInstallationNotFinished? state,}) async { + void startServerIfDnsIsOkay({ + final ServerInstallationNotFinished? state, + }) async { final ServerInstallationNotFinished dataState = state ?? this.state as ServerInstallationNotFinished; @@ -345,7 +346,9 @@ class ServerInstallationCubit extends Cubit { } void tryToRecover( - final String token, final ServerRecoveryMethods method,) async { + final String token, + final ServerRecoveryMethods method, + ) async { final ServerInstallationRecovery dataState = state as ServerInstallationRecovery; final ServerDomain? serverDomain = dataState.serverDomain; @@ -395,6 +398,9 @@ class ServerInstallationCubit extends Cubit { } void revertRecoveryStep() { + if (state is ServerInstallationEmpty) { + return; + } final ServerInstallationRecovery dataState = state as ServerInstallationRecovery; switch (dataState.currentStep) { @@ -535,7 +541,8 @@ class ServerInstallationCubit extends Cubit { } void finishRecoveryProcess( - final BackblazeCredential backblazeCredential,) async { + final BackblazeCredential backblazeCredential, + ) async { await repository.saveIsServerStarted(true); await repository.saveIsServerResetedFirstTime(true); await repository.saveIsServerResetedSecondTime(true); diff --git a/lib/logic/models/hive/backblaze_bucket.dart b/lib/logic/models/hive/backblaze_bucket.dart index ded04b65..39b98cf5 100644 --- a/lib/logic/models/hive/backblaze_bucket.dart +++ b/lib/logic/models/hive/backblaze_bucket.dart @@ -4,11 +4,12 @@ part 'backblaze_bucket.g.dart'; @HiveType(typeId: 6) class BackblazeBucket { - BackblazeBucket( - {required this.bucketId, - required this.bucketName, - required this.applicationKeyId, - required this.applicationKey,}); + BackblazeBucket({ + required this.bucketId, + required this.bucketName, + required this.applicationKeyId, + required this.applicationKey, + }); @HiveField(0) final String bucketId; diff --git a/lib/logic/models/hive/server_details.dart b/lib/logic/models/hive/server_details.dart index 1924aab7..5188e62e 100644 --- a/lib/logic/models/hive/server_details.dart +++ b/lib/logic/models/hive/server_details.dart @@ -35,15 +35,16 @@ class ServerHostingDetails { @HiveField(6, defaultValue: ServerProvider.hetzner) final ServerProvider provider; - ServerHostingDetails copyWith({final DateTime? startTime}) => ServerHostingDetails( - startTime: startTime ?? this.startTime, - createTime: createTime, - id: id, - ip4: ip4, - volume: volume, - apiToken: apiToken, - provider: provider, - ); + ServerHostingDetails copyWith({final DateTime? startTime}) => + ServerHostingDetails( + startTime: startTime ?? this.startTime, + createTime: createTime, + id: id, + ip4: ip4, + volume: volume, + apiToken: apiToken, + provider: provider, + ); @override String toString() => id.toString(); diff --git a/lib/logic/models/json/api_token.dart b/lib/logic/models/json/api_token.dart index 5e33fd4f..980d5132 100644 --- a/lib/logic/models/json/api_token.dart +++ b/lib/logic/models/json/api_token.dart @@ -4,7 +4,6 @@ part 'api_token.g.dart'; @JsonSerializable() class ApiToken { - factory ApiToken.fromJson(final Map json) => _$ApiTokenFromJson(json); ApiToken({ diff --git a/lib/logic/models/json/backup.dart b/lib/logic/models/json/backup.dart index 7a5ad963..2e1215db 100644 --- a/lib/logic/models/json/backup.dart +++ b/lib/logic/models/json/backup.dart @@ -4,8 +4,8 @@ part 'backup.g.dart'; @JsonSerializable() class Backup { - - factory Backup.fromJson(final Map json) => _$BackupFromJson(json); + factory Backup.fromJson(final Map json) => + _$BackupFromJson(json); Backup({required this.time, required this.id}); // Time of the backup @@ -33,13 +33,13 @@ enum BackupStatusEnum { @JsonSerializable() class BackupStatus { - factory BackupStatus.fromJson(final Map json) => _$BackupStatusFromJson(json); - BackupStatus( - {required this.status, - required this.progress, - required this.errorMessage,}); + BackupStatus({ + required this.status, + required this.progress, + required this.errorMessage, + }); final BackupStatusEnum status; final double progress; diff --git a/lib/logic/models/json/device_token.dart b/lib/logic/models/json/device_token.dart index 53eac22f..2ec23012 100644 --- a/lib/logic/models/json/device_token.dart +++ b/lib/logic/models/json/device_token.dart @@ -4,7 +4,6 @@ part 'device_token.g.dart'; @JsonSerializable() class DeviceToken { - factory DeviceToken.fromJson(final Map json) => _$DeviceTokenFromJson(json); DeviceToken({ diff --git a/lib/logic/models/server_status.dart b/lib/logic/models/server_status.dart index b191ee16..e6b15f25 100644 --- a/lib/logic/models/server_status.dart +++ b/lib/logic/models/server_status.dart @@ -1,5 +1,4 @@ class ServerStatus { - ServerStatus({ required this.http, this.imap = StatusTypes.nodata, @@ -10,10 +9,10 @@ class ServerStatus { final StatusTypes smtp; ServerStatus fromJson(final Map json) => ServerStatus( - http: statusTypeFromNumber(json['http']), - imap: statusTypeFromNumber(json['imap']), - smtp: statusTypeFromNumber(json['smtp']), - ); + http: statusTypeFromNumber(json['http']), + imap: statusTypeFromNumber(json['imap']), + smtp: statusTypeFromNumber(json['smtp']), + ); } StatusTypes statusTypeFromNumber(final int? number) { diff --git a/lib/ui/components/brand_md/brand_md.dart b/lib/ui/components/brand_md/brand_md.dart index 3de9f86a..249895a9 100644 --- a/lib/ui/components/brand_md/brand_md.dart +++ b/lib/ui/components/brand_md/brand_md.dart @@ -55,7 +55,7 @@ class _BrandMarkdownState extends State { color: isDark ? BrandColors.white : null, ), ); - return Markdown( + return MarkdownBody( shrinkWrap: true, styleSheet: markdown, onTapLink: (final String text, final String? href, final String title) { diff --git a/lib/ui/components/error/error.dart b/lib/ui/components/error/error.dart index 9fe651cc..d12af1a3 100644 --- a/lib/ui/components/error/error.dart +++ b/lib/ui/components/error/error.dart @@ -8,19 +8,19 @@ class BrandError extends StatelessWidget { @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()), - ], + 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/pages/devices/new_device.dart b/lib/ui/pages/devices/new_device.dart index 56f3d47f..e8173db0 100644 --- a/lib/ui/pages/devices/new_device.dart +++ b/lib/ui/pages/devices/new_device.dart @@ -66,7 +66,7 @@ class _KeyDisplay extends StatelessWidget { const SizedBox(height: 16), Text( 'devices.add_new_device_screen.tip'.tr(), - style: Theme.of(context).textTheme.bodyMedium!, + style: Theme.of(context).textTheme.bodyMedium, ), ], ), diff --git a/lib/ui/pages/recovery_key/recovery_key.dart b/lib/ui/pages/recovery_key/recovery_key.dart index f808f1c3..44147f57 100644 --- a/lib/ui/pages/recovery_key/recovery_key.dart +++ b/lib/ui/pages/recovery_key/recovery_key.dart @@ -234,7 +234,9 @@ class _RecoveryKeyConfigurationState extends State { : null, expirationDate: _isExpirationToggled ? _selectedDate : null, ); - if (!mounted) return; + if (!mounted) { + return; + } setState(() { _isLoading = false; }); diff --git a/lib/ui/pages/server_details/cpu_chart.dart b/lib/ui/pages/server_details/cpu_chart.dart index 35d6fff4..11f1eaef 100644 --- a/lib/ui/pages/server_details/cpu_chart.dart +++ b/lib/ui/pages/server_details/cpu_chart.dart @@ -8,11 +8,11 @@ import 'package:intl/intl.dart'; class CpuChart extends StatelessWidget { const CpuChart({ - final Key? key, required this.data, required this.period, required this.start, - }) : super(key: key); + final super.key, + }); final List data; final Period period; diff --git a/lib/ui/pages/server_details/network_charts.dart b/lib/ui/pages/server_details/network_charts.dart index 838b4ebb..d1375ae6 100644 --- a/lib/ui/pages/server_details/network_charts.dart +++ b/lib/ui/pages/server_details/network_charts.dart @@ -10,11 +10,11 @@ import 'package:intl/intl.dart'; class NetworkChart extends StatelessWidget { const NetworkChart({ - final Key? key, required this.listData, required this.period, required this.start, - }) : super(key: key); + final super.key, + }); final List> listData; final Period period; diff --git a/lib/ui/pages/setup/initializing.dart b/lib/ui/pages/setup/initializing.dart index 1710113b..9c92f161 100644 --- a/lib/ui/pages/setup/initializing.dart +++ b/lib/ui/pages/setup/initializing.dart @@ -567,8 +567,13 @@ class _HowTo extends StatelessWidget { isExpended: true, child: Padding( padding: paddingH15V0, - child: BrandMarkdown( - fileName: fileName, + child: ListView( + padding: const EdgeInsets.symmetric(vertical: 16), + children: [ + BrandMarkdown( + fileName: fileName, + ), + ], ), ), ); 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 d9fb3952..a9f37b19 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 @@ -17,8 +17,8 @@ class RecoverByNewDeviceKeyInstruction extends StatelessWidget { heroSubtitle: 'recovering.method_device_description'.tr(), hasBackButton: true, hasFlashButton: false, - onBackButtonPressed: () => - context.read().revertRecoveryStep(), + onBackButtonPressed: + context.read().revertRecoveryStep, children: [ FilledButton( title: 'recovering.method_device_button'.tr(), 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 6d3831f9..e3507a0e 100644 --- a/lib/ui/pages/setup/recovering/recover_by_old_token.dart +++ b/lib/ui/pages/setup/recovering/recover_by_old_token.dart @@ -28,8 +28,8 @@ class RecoverByOldTokenInstruction extends StatelessWidget { heroTitle: 'recovering.recovery_main_header'.tr(), hasBackButton: true, hasFlashButton: false, - onBackButtonPressed: () => - context.read().revertRecoveryStep(), + onBackButtonPressed: + context.read().revertRecoveryStep, children: [ BrandMarkdown( fileName: instructionFilename, 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 a6cc44cd..f729524e 100644 --- a/lib/ui/pages/setup/recovering/recover_by_recovery_key.dart +++ b/lib/ui/pages/setup/recovering/recover_by_recovery_key.dart @@ -31,8 +31,8 @@ class RecoverByRecoveryKey extends StatelessWidget { heroSubtitle: 'recovering.method_recovery_input_description'.tr(), hasBackButton: true, hasFlashButton: false, - onBackButtonPressed: () => - context.read().revertRecoveryStep(), + onBackButtonPressed: + context.read().revertRecoveryStep, children: [ CubitFormTextField( formFieldCubit: diff --git a/lib/ui/pages/setup/recovering/recovery_confirm_backblaze.dart b/lib/ui/pages/setup/recovering/recovery_confirm_backblaze.dart index 2513b054..2b558727 100644 --- a/lib/ui/pages/setup/recovering/recovery_confirm_backblaze.dart +++ b/lib/ui/pages/setup/recovering/recovery_confirm_backblaze.dart @@ -10,7 +10,7 @@ import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.da import 'package:selfprivacy/ui/components/brand_md/brand_md.dart'; class RecoveryConfirmBackblaze extends StatelessWidget { - const RecoveryConfirmBackblaze({final Key? key}) : super(key: key); + const RecoveryConfirmBackblaze({final super.key}); @override Widget build(final BuildContext context) { @@ -59,13 +59,17 @@ class RecoveryConfirmBackblaze extends StatelessWidget { context: context, isScrollControlled: true, backgroundColor: Colors.transparent, - builder: (final BuildContext context) => - const BrandBottomSheet( + builder: (final BuildContext context) => BrandBottomSheet( isExpended: true, child: Padding( padding: paddingH15V0, - child: BrandMarkdown( - fileName: 'how_backblaze', + child: ListView( + padding: const EdgeInsets.symmetric(vertical: 16), + children: const [ + BrandMarkdown( + fileName: 'how_backblaze', + ), + ], ), ), ), diff --git a/lib/ui/pages/setup/recovering/recovery_confirm_cloudflare.dart b/lib/ui/pages/setup/recovering/recovery_confirm_cloudflare.dart index b64ca6ae..8cbdbe6c 100644 --- a/lib/ui/pages/setup/recovering/recovery_confirm_cloudflare.dart +++ b/lib/ui/pages/setup/recovering/recovery_confirm_cloudflare.dart @@ -52,13 +52,17 @@ class RecoveryConfirmCloudflare extends StatelessWidget { context: context, isScrollControlled: true, backgroundColor: Colors.transparent, - builder: (final BuildContext context) => - const BrandBottomSheet( + builder: (final BuildContext context) => BrandBottomSheet( isExpended: true, child: Padding( padding: paddingH15V0, - child: BrandMarkdown( - fileName: 'how_cloudflare', + child: ListView( + padding: const EdgeInsets.symmetric(vertical: 16), + children: const [ + BrandMarkdown( + fileName: 'how_cloudflare', + ), + ], ), ), ), diff --git a/lib/ui/pages/setup/recovering/recovery_hentzner_connected.dart b/lib/ui/pages/setup/recovering/recovery_hentzner_connected.dart index 29506e8b..e1812b32 100644 --- a/lib/ui/pages/setup/recovering/recovery_hentzner_connected.dart +++ b/lib/ui/pages/setup/recovering/recovery_hentzner_connected.dart @@ -54,13 +54,17 @@ class RecoveryHetznerConnected extends StatelessWidget { context: context, isScrollControlled: true, backgroundColor: Colors.transparent, - builder: (final BuildContext context) => - const BrandBottomSheet( + builder: (final BuildContext context) => BrandBottomSheet( isExpended: true, child: Padding( padding: paddingH15V0, - child: BrandMarkdown( - fileName: 'how_hetzner', + child: ListView( + padding: const EdgeInsets.symmetric(vertical: 16), + children: const [ + BrandMarkdown( + fileName: 'how_hetzner', + ), + ], ), ), ), diff --git a/lib/ui/pages/setup/recovering/recovery_method_select.dart b/lib/ui/pages/setup/recovering/recovery_method_select.dart index 8abe3a27..fe622acb 100644 --- a/lib/ui/pages/setup/recovering/recovery_method_select.dart +++ b/lib/ui/pages/setup/recovering/recovery_method_select.dart @@ -17,6 +17,8 @@ class RecoveryMethodSelect extends StatelessWidget { heroSubtitle: 'recovering.method_select_description'.tr(), hasBackButton: true, hasFlashButton: false, + onBackButtonPressed: + context.read().revertRecoveryStep, children: [ BrandCards.outlined( child: ListTile( diff --git a/lib/ui/pages/setup/recovering/recovery_routing.dart b/lib/ui/pages/setup/recovering/recovery_routing.dart index e5a86074..17c5963f 100644 --- a/lib/ui/pages/setup/recovering/recovery_routing.dart +++ b/lib/ui/pages/setup/recovering/recovery_routing.dart @@ -108,10 +108,6 @@ class SelectDomainToRecover extends StatelessWidget { heroSubtitle: 'recovering.domain_recovery_description'.tr(), hasBackButton: true, hasFlashButton: false, - onBackButtonPressed: - serverInstallation is ServerInstallationRecovery - ? serverInstallation.clearAppConfig - : null, children: [ CubitFormTextField( formFieldCubit: diff --git a/lib/ui/pages/users/user_details.dart b/lib/ui/pages/users/user_details.dart index f7e212c8..d758c1f4 100644 --- a/lib/ui/pages/users/user_details.dart +++ b/lib/ui/pages/users/user_details.dart @@ -2,10 +2,9 @@ part of 'users.dart'; class _UserDetails extends StatelessWidget { const _UserDetails({ - final Key? key, required this.user, required this.isRootUser, - }) : super(key: key); + }); final User user; final bool isRootUser; diff --git a/lib/utils/extensions/text_extensions.dart b/lib/utils/extensions/text_extensions.dart index 6afaee08..bfacc600 100644 --- a/lib/utils/extensions/text_extensions.dart +++ b/lib/utils/extensions/text_extensions.dart @@ -32,7 +32,9 @@ extension TextExtension on Text { final String? semanticsLabel, final TextWidthBasis? textWidthBasis, final TextStyle? style, - }) => Text(data!, + }) => + Text( + data!, key: key ?? this.key, strutStyle: strutStyle ?? this.strutStyle, textAlign: textAlign ?? this.textAlign, @@ -44,5 +46,6 @@ extension TextExtension on Text { maxLines: maxLines ?? this.maxLines, semanticsLabel: semanticsLabel ?? this.semanticsLabel, textWidthBasis: textWidthBasis ?? this.textWidthBasis, - style: style != null ? this.style?.merge(style) ?? style : this.style,); + style: style != null ? this.style?.merge(style) ?? style : this.style, + ); } diff --git a/lib/utils/route_transitions/slide_bottom.dart b/lib/utils/route_transitions/slide_bottom.dart index 374a5e90..e187b4d7 100644 --- a/lib/utils/route_transitions/slide_bottom.dart +++ b/lib/utils/route_transitions/slide_bottom.dart @@ -12,24 +12,25 @@ Function transitionsBuilder = ( final Animation animation, final Animation secondaryAnimation, final Widget child, -) => SlideTransition( - position: Tween( - begin: const Offset(0, 1), - end: Offset.zero, - ).animate(animation), - child: Container( - decoration: animation.isCompleted - ? null - : const BoxDecoration( - border: Border( - bottom: BorderSide( - color: Colors.black, +) => + SlideTransition( + position: Tween( + begin: const Offset(0, 1), + end: Offset.zero, + ).animate(animation), + child: Container( + decoration: animation.isCompleted + ? null + : const BoxDecoration( + border: Border( + bottom: BorderSide( + color: Colors.black, + ), ), ), - ), - child: child, - ), - ); + child: child, + ), + ); class SlideBottomRoute extends PageRouteBuilder { SlideBottomRoute(this.widget) @@ -37,7 +38,11 @@ class SlideBottomRoute extends PageRouteBuilder { transitionDuration: const Duration(milliseconds: 150), pageBuilder: pageBuilder(widget), transitionsBuilder: transitionsBuilder as Widget Function( - BuildContext, Animation, Animation, Widget,), + BuildContext, + Animation, + Animation, + Widget, + ), ); final Widget widget; diff --git a/lib/utils/route_transitions/slide_right.dart b/lib/utils/route_transitions/slide_right.dart index 774dcaff..eae4414d 100644 --- a/lib/utils/route_transitions/slide_right.dart +++ b/lib/utils/route_transitions/slide_right.dart @@ -12,31 +12,36 @@ Function transitionsBuilder = ( final Animation animation, final Animation secondaryAnimation, final Widget child, -) => SlideTransition( - position: Tween( - begin: const Offset(-1, 0), - end: Offset.zero, - ).animate(animation), - child: Container( - decoration: animation.isCompleted - ? null - : const BoxDecoration( - border: Border( - right: BorderSide( - color: Colors.black, +) => + SlideTransition( + position: Tween( + begin: const Offset(-1, 0), + end: Offset.zero, + ).animate(animation), + child: Container( + decoration: animation.isCompleted + ? null + : const BoxDecoration( + border: Border( + right: BorderSide( + color: Colors.black, + ), ), ), - ), - child: child, - ), - ); + child: child, + ), + ); class SlideRightRoute extends PageRouteBuilder { SlideRightRoute(this.widget) : super( pageBuilder: pageBuilder(widget), transitionsBuilder: transitionsBuilder as Widget Function( - BuildContext, Animation, Animation, Widget,), + BuildContext, + Animation, + Animation, + Widget, + ), ); final Widget widget; diff --git a/test/widget_test.dart b/test/widget_test.dart index 72a8e56e..b8799e1f 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -6,9 +6,12 @@ void main() { group('StringGenerators', () { group('Basic', () { test('assert chart empty', () { - expect(() { - StringGenerators.getRandomString(8); - }, throwsAssertionError,); + expect( + () { + StringGenerators.getRandomString(8); + }, + throwsAssertionError, + ); }); test('only lowercase string', () { @@ -27,8 +30,11 @@ void main() { test('only uppercase string', () { const int length = 8; - final String generatedString = StringGenerators.getRandomString(length, - hasLowercaseLetters: false, hasUppercaseLetters: true,); + final String generatedString = StringGenerators.getRandomString( + length, + hasLowercaseLetters: false, + hasUppercaseLetters: true, + ); expect(generatedString, isNot(matches(regExpNewLines))); expect(generatedString, isNot(matches(regExpWhiteSpaces))); @@ -41,10 +47,12 @@ void main() { test('only numbers string', () { const int length = 8; - final String generatedString = StringGenerators.getRandomString(length, - hasLowercaseLetters: false, - hasUppercaseLetters: false, - hasNumbers: true,); + final String generatedString = StringGenerators.getRandomString( + length, + hasLowercaseLetters: false, + hasUppercaseLetters: false, + hasNumbers: true, + ); expect(generatedString, isNot(matches(regExpNewLines))); expect(generatedString, isNot(matches(regExpWhiteSpaces))); @@ -78,12 +86,14 @@ void main() { group('Strict mode', () { test('All', () { const int length = 5; - final String generatedString = StringGenerators.getRandomString(length, - hasLowercaseLetters: true, - hasUppercaseLetters: true, - hasNumbers: true, - hasSymbols: true, - isStrict: true,); + final String generatedString = StringGenerators.getRandomString( + length, + hasLowercaseLetters: true, + hasUppercaseLetters: true, + hasNumbers: true, + hasSymbols: true, + isStrict: true, + ); expect(generatedString, isNot(matches(regExpNewLines))); expect(generatedString, isNot(matches(regExpWhiteSpaces))); @@ -95,12 +105,14 @@ void main() { }); test('Lowercase letters and numbers', () { const int length = 3; - final String generatedString = StringGenerators.getRandomString(length, - hasLowercaseLetters: true, - hasUppercaseLetters: false, - hasNumbers: true, - hasSymbols: false, - isStrict: true,); + final String generatedString = StringGenerators.getRandomString( + length, + hasLowercaseLetters: true, + hasUppercaseLetters: false, + hasNumbers: true, + hasSymbols: false, + isStrict: true, + ); expect(generatedString, isNot(matches(regExpNewLines))); expect(generatedString, isNot(matches(regExpWhiteSpaces))); From ed4234ee63836be8cf9a3eef8d9708b3b80d5b6d Mon Sep 17 00:00:00 2001 From: NaiJi Date: Fri, 10 Jun 2022 17:57:48 +0300 Subject: [PATCH 51/52] Fix assets typos --- assets/markdown/how_fallback_ssh-ru.md | 2 +- assets/markdown/how_fallback_terminal-en.md | 2 +- assets/markdown/how_fallback_terminal-ru.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/assets/markdown/how_fallback_ssh-ru.md b/assets/markdown/how_fallback_ssh-ru.md index 11a32875..a1737f08 100644 --- a/assets/markdown/how_fallback_ssh-ru.md +++ b/assets/markdown/how_fallback_ssh-ru.md @@ -1,4 +1,4 @@ -Войдите как root пользователь на свой сервер и посмотрите содерижмое файла `/etc/nixos/userdata/tokens.json` +Войдите как root пользователь на свой сервер и посмотрите содержимое файла `/etc/nixos/userdata/tokens.json` ```sh cat /etc/nixos/userdata/tokens.json diff --git a/assets/markdown/how_fallback_terminal-en.md b/assets/markdown/how_fallback_terminal-en.md index 760b49df..77c97efa 100644 --- a/assets/markdown/how_fallback_terminal-en.md +++ b/assets/markdown/how_fallback_terminal-en.md @@ -1,4 +1,4 @@ -In the server control panel in Hetzner, go to the **Rescue** tab. Then, click on **Enable rescue & power cycle**. +In the Hetzner server control panel, go to the **Rescue** tab. Then, click on **Enable rescue & power cycle**. In *Choose a Recue OS* select **linux64**, and in *SSH Key* select your key if it has been added to your Hetzner account. diff --git a/assets/markdown/how_fallback_terminal-ru.md b/assets/markdown/how_fallback_terminal-ru.md index 94d357c7..6681191e 100644 --- a/assets/markdown/how_fallback_terminal-ru.md +++ b/assets/markdown/how_fallback_terminal-ru.md @@ -4,7 +4,7 @@ Нажмите **Enable rescue & power cycle** и подождите перезагрузки сервера. На экране будет отображён пароль для входа. Войдите в root пользователя используя данные логин и пароль. -Примонтируйте файловую систему вашего сервера и посмотрите содерижмое файла с токенами: +Примонтируйте файловую систему вашего сервера и посмотрите содержимое файла с токенами: ```sh mount /dev/sda1 /mnt From f370a7fc91b75c5243baf143c2874b4220d18449 Mon Sep 17 00:00:00 2001 From: NaiJi Date: Fri, 10 Jun 2022 18:15:43 +0300 Subject: [PATCH 52/52] Fix minor recovery routing problem --- lib/ui/pages/setup/recovering/recovery_routing.dart | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/ui/pages/setup/recovering/recovery_routing.dart b/lib/ui/pages/setup/recovering/recovery_routing.dart index 17c5963f..3c375ef0 100644 --- a/lib/ui/pages/setup/recovering/recovery_routing.dart +++ b/lib/ui/pages/setup/recovering/recovery_routing.dart @@ -6,6 +6,7 @@ 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/filled_button.dart'; import 'package:selfprivacy/ui/components/brand_hero_screen/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'; import 'package:selfprivacy/ui/pages/setup/recovering/recover_by_new_device_key.dart'; @@ -14,6 +15,7 @@ import 'package:selfprivacy/ui/pages/setup/recovering/recovery_confirm_cloudflar import 'package:selfprivacy/ui/pages/setup/recovering/recovery_confirm_server.dart'; import 'package:selfprivacy/ui/pages/setup/recovering/recovery_hentzner_connected.dart'; import 'package:selfprivacy/ui/pages/setup/recovering/recovery_method_select.dart'; +import 'package:selfprivacy/utils/route_transitions/basic.dart'; class RecoveryRouting extends StatelessWidget { const RecoveryRouting({final super.key}); @@ -108,6 +110,12 @@ class SelectDomainToRecover extends StatelessWidget { heroSubtitle: 'recovering.domain_recovery_description'.tr(), hasBackButton: true, hasFlashButton: false, + onBackButtonPressed: () { + Navigator.of(context).pushAndRemoveUntil( + materialRoute(const RootPage()), + (final predicate) => false, + ); + }, children: [ CubitFormTextField( formFieldCubit: