diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..2b83ac77 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,53 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "debug", + "request": "launch", + "type": "dart" + }, + { + "name": "profile mode", + "request": "launch", + "type": "dart", + "flutterMode": "profile" + }, + { + "name": "release mode", + "request": "launch", + "type": "dart", + "flutterMode": "release" + }, + { + "name": "debug (fdroid)", + "request": "launch", + "type": "dart", + "args": [ + "--flavor", + "fdroid" + ] + }, + { + "name": "debug (production)", + "request": "launch", + "type": "dart", + "args": [ + "--flavor", + "production" + ] + }, + + { + "name": "debug (nightly)", + "request": "launch", + "type": "dart", + "args": [ + "--flavor", + "nightly" + ] + } + ] +} \ No newline at end of file diff --git a/android/app/build.gradle b/android/app/build.gradle index 4c8300ca..a5fc42bb 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -1,3 +1,10 @@ +plugins { + id "com.android.application" + id "kotlin-android" + id "dev.flutter.flutter-gradle-plugin" +} + + def localProperties = new Properties() def localPropertiesFile = rootProject.file('local.properties') if (localPropertiesFile.exists()) { @@ -6,10 +13,6 @@ if (localPropertiesFile.exists()) { } } -def flutterRoot = localProperties.getProperty('flutter.sdk') -if (flutterRoot == null) { - throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") -} def flutterVersionCode = localProperties.getProperty('flutter.versionCode') if (flutterVersionCode == null) { @@ -21,14 +24,9 @@ if (flutterVersionName == null) { flutterVersionName = '1.0' } -apply plugin: 'com.android.application' -apply plugin: 'kotlin-android' -apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" - android { - namespace 'org.selfprivacy.app' - - compileSdkVersion flutter.compileSdkVersion + namespace 'org.selfprivacy.app' + compileSdkVersion flutter.compileSdkVersion ndkVersion flutter.ndkVersion sourceSets { @@ -43,13 +41,16 @@ android { kotlinOptions { jvmTarget = '1.8' } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } lintOptions { disable 'InvalidPackage' } defaultConfig { - // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "org.selfprivacy.app" minSdkVersion 21 targetSdkVersion 34 @@ -57,31 +58,33 @@ android { versionCode flutterVersionCode.toInteger() versionName flutterVersionName } + + buildTypes { + debug { - flavorDimensions "default" - productFlavors { - fdroid { - applicationId "pro.kherel.selfprivacy" } - production { - applicationIdSuffix "" + profile { + } - nightly { - applicationIdSuffix ".nightly" - versionCode project.getVersionCode() - versionName "nightly-" + project.getVersionCode() + release { + } } + buildFeatures { + flavorDimensions = ["default"] + } + - flavorDimensions "default" productFlavors { fdroid { + dimension 'default' applicationId "pro.kherel.selfprivacy" } production { - applicationIdSuffix "" + dimension 'default' } nightly { + dimension 'default' applicationIdSuffix ".nightly" versionCode project.getVersionCode() versionName "nightly-" + project.getVersionCode() @@ -93,6 +96,5 @@ flutter { source '../..' } -dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" -} +dependencies {} + diff --git a/android/build.gradle b/android/build.gradle index b1ba7da3..98a94fa2 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,6 +1,5 @@ buildscript { - ext.kotlin_version = '1.9.21' - ext.getVersionCode = { -> + ext.getVersionCode = { -> try { def stdout = new ByteArrayOutputStream() exec { @@ -13,15 +12,6 @@ buildscript { return -1 } } - repositories { - google() - jcenter() - } - - dependencies { - classpath 'com.android.tools.build:gradle:7.1.2' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - } } allprojects { diff --git a/android/gradle.properties b/android/gradle.properties index c396be2a..85faa95d 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,4 +1,4 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.useAndroidX=true android.enableJetifier=true android.bundle.enableUncompressedNativeLibs=false diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 02e5f581..aeaff6f8 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-7.6-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-all.zip diff --git a/android/settings.gradle b/android/settings.gradle index 44e62bcf..14d2478f 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -1,11 +1,25 @@ -include ':app' +pluginManagement { + def flutterSdkPath = { + def properties = new Properties() + file("local.properties").withInputStream { properties.load(it) } + def flutterSdkPath = properties.getProperty("flutter.sdk") + assert flutterSdkPath != null, "flutter.sdk not set in local.properties" + return flutterSdkPath + }() -def localPropertiesFile = new File(rootProject.projectDir, "local.properties") -def properties = new Properties() + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") -assert localPropertiesFile.exists() -localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} -def flutterSdkPath = properties.getProperty("flutter.sdk") -assert flutterSdkPath != null, "flutter.sdk not set in local.properties" -apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" +plugins { + id "dev.flutter.flutter-plugin-loader" version "1.0.0" + id "com.android.application" version "7.3.0" apply false + id "org.jetbrains.kotlin.android" version "1.9.21" apply false +} + +include ":app" \ No newline at end of file diff --git a/assets/markdown/how_fallback_ssh-en.md b/assets/markdown/how_fallback_ssh-en.md index ce90e76a..e5bcab69 100644 --- a/assets/markdown/how_fallback_ssh-en.md +++ b/assets/markdown/how_fallback_ssh-en.md @@ -1,19 +1,7 @@ -Login as root user to your server and look at the contents of the file `/etc/nixos/userdata/tokens.json` +[Login as root user to your server](https://selfprivacy.org/docs/how-to-guides/root_ssh/) and enter this command: ```sh -cat /etc/nixos/userdata/tokens.json +sp-print-api-token ``` -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. +Copy the token from the terminal and paste it in the next window. diff --git a/assets/markdown/how_fallback_ssh-es.md b/assets/markdown/how_fallback_ssh-es.md index ce90e76a..e5bcab69 100644 --- a/assets/markdown/how_fallback_ssh-es.md +++ b/assets/markdown/how_fallback_ssh-es.md @@ -1,19 +1,7 @@ -Login as root user to your server and look at the contents of the file `/etc/nixos/userdata/tokens.json` +[Login as root user to your server](https://selfprivacy.org/docs/how-to-guides/root_ssh/) and enter this command: ```sh -cat /etc/nixos/userdata/tokens.json +sp-print-api-token ``` -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. +Copy the token from the terminal and paste it in the next window. diff --git a/assets/markdown/how_fallback_ssh-fr.md b/assets/markdown/how_fallback_ssh-fr.md index ce90e76a..e5bcab69 100644 --- a/assets/markdown/how_fallback_ssh-fr.md +++ b/assets/markdown/how_fallback_ssh-fr.md @@ -1,19 +1,7 @@ -Login as root user to your server and look at the contents of the file `/etc/nixos/userdata/tokens.json` +[Login as root user to your server](https://selfprivacy.org/docs/how-to-guides/root_ssh/) and enter this command: ```sh -cat /etc/nixos/userdata/tokens.json +sp-print-api-token ``` -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. +Copy the token from the terminal and paste it in the next window. diff --git a/assets/markdown/how_fallback_ssh-it.md b/assets/markdown/how_fallback_ssh-it.md index ce90e76a..e5bcab69 100644 --- a/assets/markdown/how_fallback_ssh-it.md +++ b/assets/markdown/how_fallback_ssh-it.md @@ -1,19 +1,7 @@ -Login as root user to your server and look at the contents of the file `/etc/nixos/userdata/tokens.json` +[Login as root user to your server](https://selfprivacy.org/docs/how-to-guides/root_ssh/) and enter this command: ```sh -cat /etc/nixos/userdata/tokens.json +sp-print-api-token ``` -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. +Copy the token from the terminal and paste it in the next window. diff --git a/assets/markdown/how_fallback_ssh-ka.md b/assets/markdown/how_fallback_ssh-ka.md index ce90e76a..e5bcab69 100644 --- a/assets/markdown/how_fallback_ssh-ka.md +++ b/assets/markdown/how_fallback_ssh-ka.md @@ -1,19 +1,7 @@ -Login as root user to your server and look at the contents of the file `/etc/nixos/userdata/tokens.json` +[Login as root user to your server](https://selfprivacy.org/docs/how-to-guides/root_ssh/) and enter this command: ```sh -cat /etc/nixos/userdata/tokens.json +sp-print-api-token ``` -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. +Copy the token from the terminal and paste it in the next window. diff --git a/assets/markdown/how_fallback_ssh-nl.md b/assets/markdown/how_fallback_ssh-nl.md index ce90e76a..e5bcab69 100644 --- a/assets/markdown/how_fallback_ssh-nl.md +++ b/assets/markdown/how_fallback_ssh-nl.md @@ -1,19 +1,7 @@ -Login as root user to your server and look at the contents of the file `/etc/nixos/userdata/tokens.json` +[Login as root user to your server](https://selfprivacy.org/docs/how-to-guides/root_ssh/) and enter this command: ```sh -cat /etc/nixos/userdata/tokens.json +sp-print-api-token ``` -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. +Copy the token from the terminal and paste it in the next window. diff --git a/assets/markdown/how_fallback_ssh-th.md b/assets/markdown/how_fallback_ssh-th.md index ce90e76a..e5bcab69 100644 --- a/assets/markdown/how_fallback_ssh-th.md +++ b/assets/markdown/how_fallback_ssh-th.md @@ -1,19 +1,7 @@ -Login as root user to your server and look at the contents of the file `/etc/nixos/userdata/tokens.json` +[Login as root user to your server](https://selfprivacy.org/docs/how-to-guides/root_ssh/) and enter this command: ```sh -cat /etc/nixos/userdata/tokens.json +sp-print-api-token ``` -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. +Copy the token from the terminal and paste it in the next window. diff --git a/assets/translations/ar.json b/assets/translations/ar.json index c5228ce3..d77b1ba4 100644 --- a/assets/translations/ar.json +++ b/assets/translations/ar.json @@ -173,6 +173,7 @@ "destroy_server": "هل تريد إنهاء هذا الخادم وإنشاء واحد جديد؟", "try_again": "هل تريد المحاولة مرة أخرى؟", "purge_all_keys": "هل تريد محو جميع مفاتيح الّتي مُنحت الموافقة؟", + "delete_server_volume": "هل تريد حذف الخادم ووحدة التخزين؟", "reboot": "قم بإعادة التشغيل", "yes": "نعم", "no": "لا" @@ -332,12 +333,12 @@ }, "application_settings": { "title": "إعدادات التطبيق", - "system_dark_theme_title": "الوضع الافتراضي للنظام", - "system_dark_theme_description": "قم بتطبيق الوضع الفاتح أو الداكن حسب إعدادات النظام", + "system_theme_mode_title": "الوضع الافتراضي للنظام", + "system_theme_mode_description": "قم بتطبيق الوضع الفاتح أو الداكن حسب إعدادات النظام", "dark_theme_title": "الوضع الداكن", + "change_application_theme": "قم بتبديل وضع التطبيق", "dangerous_settings": "إعدادات خطرة", "reset_config_title": "قم بإعادة ضبط إعدادات التطبيق", - "dark_theme_description": "قم بتبديل وضع التطبيق", "reset_config_description": "قم بإعادة ضبط مفاتيح API والمستخدم المميز." }, "ssh": { diff --git a/assets/translations/az.json b/assets/translations/az.json index 83647f46..2b4de98c 100644 --- a/assets/translations/az.json +++ b/assets/translations/az.json @@ -54,13 +54,15 @@ }, "application_settings": { "title": "Tətbiq parametrləri", + "system_theme_mode_title": "Defolt sistem mövzusu", + "system_theme_mode_description": "Sistem parametrlərindən asılı olaraq açıq və ya qaranlıq mövzudan istifadə edin", "dark_theme_title": "Qaranlıq mövzu", + "change_application_theme": "Rəng mövzusunu dəyişdirin", + "dangerous_settings": "Təhlükəli Parametrlər", "reset_config_title": "Tətbiq Sıfırlayın", - "reset_config_description": "API və Super İstifadəçi Açarlarını sıfırlayın.", - "dark_theme_description": "Rəng mövzusunu dəyişdirin", - "system_dark_theme_title": "Defolt sistem mövzusu", - "system_dark_theme_description": "Sistem parametrlərindən asılı olaraq açıq və ya qaranlıq mövzudan istifadə edin", - "dangerous_settings": "Təhlükəli Parametrlər" + "reset_config_description": "API və Super İstifadəçi Açarlarını sıfırlayın." + + }, "ssh": { "title": "SSH açarları", @@ -379,6 +381,7 @@ "are_you_sure": "Sən əminsən?", "purge_all_keys": "Bütün avtorizasiya açarları silinsin?", "purge_all_keys_confirm": "Bəli, bütün düymələri silin", + "delete_server_volume": "Server və yaddaş silinsin?", "reboot": "Yenidən yükləyin", "yes": "Bəli", "no": "Yox" diff --git a/assets/translations/be.json b/assets/translations/be.json index 69498b0a..551cead5 100644 --- a/assets/translations/be.json +++ b/assets/translations/be.json @@ -51,7 +51,7 @@ "connect_to_server_provider": "Аўтарызавацца ў ", "connect_to_server_provider_text": "З дапамогай API токена праграма SelfPrivacy зможа ад вашага імя замовіць і наладзіць сервер", "steps": { - "nixos_installation": "Ўстаноўка NixOS", + "nixos_installation": "Ўсталёўка NixOS", "hosting": "Хостынг", "server_type": "Тып сервера", "dns_provider": "DNS правайдэр", @@ -59,7 +59,7 @@ "domain": "Дамен", "master_account": "Майстар акаўнт", "server": "Сервер", - "dns_setup": "Устаноўка DNS", + "dns_setup": "Усталёўка DNS", "server_reboot": "Перазагрузка сервера", "final_checks": "Фінальныя праверкі" }, @@ -100,7 +100,7 @@ "modal_confirmation_dns_invalid": "Зваротны DNS паказвае на іншы дамен", "modal_confirmation_ip_invalid": "IP не супадае з паказаным у DNS запісу", "fallback_select_provider_console": "Доступ да кансолі хостынгу.", - "provider_connected_description": "Сувязь устаноўлена. Увядзіце свой токен з доступам да {}:", + "provider_connected_description": "Сувязь наладжана. Увядзіце свой токен з доступам да {}:", "choose_server": "Выберыце сервер", "no_servers": "На вашым акаўнце няма даступных сэрвэраў.", "modal_confirmation_description": "Падлучэнне да няправільнага сервера можа прывесці да дэструктыўных наступстваў.", @@ -114,7 +114,7 @@ "authorize_new_device": "Аўтарызаваць новую прыладу", "access_granted_on": "Доступ выдадзены {}", "tip": "Націсніце на прыладу, каб адклікаць доступ.", - "description": "Гэтыя прылады маюць поўны доступ да кіравання серверам праз прыкладанне SelfPrivacy." + "description": "Гэтыя прылады маюць поўны доступ да кіравання серверам праз прыладу SelfPrivacy." }, "add_new_device_screen": { "description": "Увядзіце гэты ключ на новай прыладзе:", @@ -127,7 +127,7 @@ "revoke_device_alert": { "header": "Адклікаць доступ?", "yes": "Адклікаць", - "no": "Адменіць", + "no": "Адхіліць", "description": "Прылада {} больш не зможа кіраваць серверам." } }, @@ -143,7 +143,7 @@ "later": "Прапусціць і наладзіць потым", "no_data": "Няма дадзеных", "services": "Сэрвісы", - "users": "Ужыткоўнікі", + "users": "Карыстальнікі", "more": "Дадаткова", "got_it": "Зразумеў", "settings": "Налады", @@ -205,6 +205,7 @@ "dns_removal_error": "Немагчыма выдаліць запісы DNS.", "server_deletion_error": "Немагчыма выдаліць сервер.", "unexpected_error": "Непрадбачаная памылка з боку правайдэра.", + "delete_server_volume": "Выдаліць сервер і сховішча?", "volume_creation_error": "Не ўдалося стварыць том." }, "timer": { @@ -235,7 +236,7 @@ }, "more_page": { "configuration_wizard": "Майстар наладкі", - "onboarding": "Прівітанне", + "onboarding": "Прывітанне", "create_ssh_key": "SSH ключы адміністратара" }, "about_application_page": { @@ -265,12 +266,15 @@ "application_settings": { "reset_config_description": "Скінуць API ключы i суперкарыстальніка.", "title": "Налады праграмы", + "system_theme_mode_title": "Сістэмная тэма па-змаўчанні", + "system_theme_mode_description": "Выкарыстоўвайце светлую ці цёмную тэмы ў залежнасці ад сістэмных налад", "dark_theme_title": "Цёмная тэма", - "dark_theme_description": "Змяніць каляровую тэму", + "change_application_theme": "Змяніць каляровую тэму", + "language": "Мова", + "click_to_change_locale": "Націсніце, каб адчыніць меню выбару мовы", + "dangerous_settings": "Небяспечныя налады", "reset_config_title": "Скід налад", - "system_dark_theme_title": "Сістэмная тэма па-змаўчанні", - "system_dark_theme_description": "Выкарыстоўвайце светлую ці цёмную тэмы ў залежнасці ад сістэмных налад", - "dangerous_settings": "Небяспечныя наладкі" + "reset_config_description": "Скінуць API ключы i суперкарыстальніка." }, "ssh": { "root_subtitle": "Уладальнікі паказаных тут ключоў атрымліваюць поўны доступ да дадзеных і налад сервера. Дадавайце выключна свае ключы.", diff --git a/assets/translations/cs.json b/assets/translations/cs.json index 0ba9d484..1033a37b 100644 --- a/assets/translations/cs.json +++ b/assets/translations/cs.json @@ -54,13 +54,13 @@ }, "application_settings": { "title": "Nastavení aplikace", + "system_theme_mode_title": "Výchozí téma systému", + "system_theme_mode_description": "Použití světlého nebo tmavého motivu v závislosti na nastavení systému", "dark_theme_title": "Tmavé téma", + "change_application_theme": "Přepnutí tématu aplikace", + "dangerous_settings": "Nebezpečná nastavení", "reset_config_title": "Obnovení konfigurace aplikace", - "reset_config_description": "Obnovení klíčů API a uživatele root.", - "dark_theme_description": "Přepnutí tématu aplikace", - "system_dark_theme_title": "Výchozí téma systému", - "system_dark_theme_description": "Použití světlého nebo tmavého motivu v závislosti na nastavení systému", - "dangerous_settings": "Nebezpečná nastavení" + "reset_config_description": "Obnovení klíčů API a uživatele root." }, "ssh": { "title": "Klíče SSH", diff --git a/assets/translations/de.json b/assets/translations/de.json index 7ebad6b0..b0c59bb1 100644 --- a/assets/translations/de.json +++ b/assets/translations/de.json @@ -57,13 +57,13 @@ }, "application_settings": { "title": "Anwendungseinstellungen", + "system_theme_mode_title": "Standard-Systemthema", + "system_theme_mode_description": "Verwenden Sie je nach Systemeinstellungen ein helles oder dunkles Thema", "dark_theme_title": "Dunkles Thema", - "dark_theme_description": "Ihr Anwendungsdesign wechseln", + "change_application_theme": "Ihr Anwendungsdesign wechseln", + "dangerous_settings": "Gefährliche Einstellungen", "reset_config_title": "Anwendungseinstellungen zurücksetzen", - "reset_config_description": "API Sclüssel und root Benutzer zurücksetzen.", - "system_dark_theme_title": "Standard-Systemthema", - "system_dark_theme_description": "Verwenden Sie je nach Systemeinstellungen ein helles oder dunkles Thema", - "dangerous_settings": "Gefährliche Einstellungen" + "reset_config_description": "API Sclüssel und root Benutzer zurücksetzen." }, "ssh": { "title": "SSH Schlüssel", diff --git a/assets/translations/en.json b/assets/translations/en.json index 263c50b1..9f672acd 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -47,7 +47,29 @@ "console_page": { "title": "Console", "waiting": "Waiting for initialization…", - "copy": "Copy" + "copy": "Copy", + "copy_raw": "Raw response", + "history_empty": "No data yet", + "error": "Error", + "log": "Log", + "rest_api_request": "Rest API Request", + "rest_api_response": "Rest API Response", + "graphql_request": "GraphQL Request", + "graphql_response": "GraphQL Response", + "logged_at": "Logged at", + "data": "Data", + "errors": "Errors", + "error_path": "Path", + "error_locations": "Locations", + "error_extensions": "Extensions", + "request_data": "Request data", + "headers": "Headers", + "response_data": "Response data", + "context": "Context", + "operation": "Operation", + "operation_type": "Operation type", + "operation_name": "Operation name", + "variables": "Variables" }, "about_application_page": { "title": "About & support", @@ -75,10 +97,12 @@ }, "application_settings": { "title": "Application settings", - "system_dark_theme_title": "System default theme", - "system_dark_theme_description": "Use light or dark theme depending on system settings", + "system_theme_mode_title": "System default theme", + "system_theme_mode_description": "Use light or dark theme depending on system settings", "dark_theme_title": "Dark theme", - "dark_theme_description": "Switch your application theme", + "change_application_theme": "Switch your application theme", + "language": "Language", + "click_to_change_locale": "Click to open language list", "dangerous_settings": "Dangerous settings", "reset_config_title": "Reset application config", "reset_config_description": "Resets API keys and root user." diff --git a/assets/translations/es.json b/assets/translations/es.json index ff3578dc..0f154fb1 100644 --- a/assets/translations/es.json +++ b/assets/translations/es.json @@ -39,14 +39,14 @@ "test": "es-test", "locale": "es", "application_settings": { - "reset_config_title": "Restablecer la configuración de la aplicación", - "dark_theme_description": "Cambia el tema de tu aplicación", - "reset_config_description": "Restablecer claves API y usuario root.", "title": "Ajustes de la aplicación", + "system_theme_mode_title": "Tema del sistema", + "system_theme_mode_description": "Utiliza un tema claro u oscuro de la configuración del sistema", "dark_theme_title": "Tema oscuro", - "system_dark_theme_title": "Tema del sistema", - "system_dark_theme_description": "Utiliza un tema claro u oscuro de la configuración del sistema", - "dangerous_settings": "Configuraciones peligrosas" + "change_application_theme": "Cambia el tema de tu aplicación", + "dangerous_settings": "Configuraciones peligrosas", + "reset_config_title": "Restablecer la configuración de la aplicación", + "reset_config_description": "Restablecer claves API y usuario root." }, "ssh": { "delete_confirm_question": "¿Está seguro de que desea eliminar la clave SSH?", diff --git a/assets/translations/et.json b/assets/translations/et.json index 0742a205..4355ff29 100644 --- a/assets/translations/et.json +++ b/assets/translations/et.json @@ -1,10 +1,10 @@ { "application_settings": { - "system_dark_theme_description": "Kasutage valgus- või tumeteemat sõltuvalt süsteemi seadetest", "title": "Rakenduse seaded", - "system_dark_theme_title": "Süsteemi vaiketeema", + "system_theme_mode_title": "Süsteemi vaiketeema", + "system_theme_mode_description": "Kasutage valgus- või tumeteemat sõltuvalt süsteemi seadetest", "dark_theme_title": "Tume teema", - "dark_theme_description": "Vaheta oma rakenduse teemat", + "change_application_theme": "Vaheta oma rakenduse teemat", "dangerous_settings": "Ohtlikud seaded", "reset_config_title": "Lähtesta rakenduse konfiguratsioon", "reset_config_description": "Lähtestab API võtmed ja juurkasutaja." diff --git a/assets/translations/fr.json b/assets/translations/fr.json index 21704c64..2a756d7d 100644 --- a/assets/translations/fr.json +++ b/assets/translations/fr.json @@ -56,13 +56,13 @@ }, "application_settings": { "title": "Paramètres de l'application", - "dark_theme_description": "Changer le thème de l'application", - "reset_config_title": "Réinitialiser la configuration de l'application", + "system_theme_mode_title": "Thème par défaut du système", + "system_theme_mode_description": "Affichage de jour ou de nuit en fonction du paramétrage système", "dark_theme_title": "Thème sombre", - "reset_config_description": "Réinitialiser les clés API et l'utilisateur root.", - "system_dark_theme_title": "Thème par défaut du système", - "system_dark_theme_description": "Affichage de jour ou de nuit en fonction du paramétrage système", - "dangerous_settings": "Paramètres dangereux" + "change_application_theme": "Changer le thème de l'application", + "dangerous_settings": "Paramètres dangereux", + "reset_config_title": "Réinitialiser la configuration de l'application", + "reset_config_description": "Réinitialiser les clés API et l'utilisateur root." }, "ssh": { "title": "Clés SSH", diff --git a/assets/translations/he.json b/assets/translations/he.json index 5f668cd4..942135ed 100644 --- a/assets/translations/he.json +++ b/assets/translations/he.json @@ -81,10 +81,10 @@ }, "application_settings": { "title": "הגדרות יישום", - "system_dark_theme_title": "ערכת העיצוב כברירת המחדל של המערכת", - "system_dark_theme_description": "להשתמש בערכות עיצוב בהירה או כהה בהתאם להגדרות המערכת שלך", + "system_theme_mode_title": "ערכת העיצוב כברירת המחדל של המערכת", + "system_theme_mode_description": "להשתמש בערכות עיצוב בהירה או כהה בהתאם להגדרות המערכת שלך", "dark_theme_title": "ערכת עיצוב כהה", - "dark_theme_description": "החלפת ערכת העיצוב של המערכת שלך", + "change_application_theme": "החלפת ערכת העיצוב של המערכת שלך", "dangerous_settings": "הגדרות מסוכנות", "reset_config_title": "איפוס הגדרות היישומון", "reset_config_description": "איפוס מפתחות ה־API ומשתמש העל." diff --git a/assets/translations/kk.json b/assets/translations/kk.json index 6647d1aa..1a80c8dc 100644 --- a/assets/translations/kk.json +++ b/assets/translations/kk.json @@ -92,13 +92,13 @@ "bug_report_subtitle": "Спамға байланысты есептік жазбаны қолмен растау қажет. Тіркелгіні белсендіру үшін Қолдау чатында бізге хабарласыңыз." }, "application_settings": { + "title": "Қосымша параметрлері", + "system_theme_mode_title": "Системалық қараңғы тақырып", + "system_theme_mode_description": "Системалық қараңғы тақырып сипаттамасы", + "dark_theme_title": "Қараңғы тақырып", + "change_application_theme": "Қараңғы тақырып сипаттамасы", "dangerous_settings": "Қауіпті параметрлер", "reset_config_title": "Конфигурацияны қалпына келтіру", - "title": "Қосымша параметрлері", - "system_dark_theme_title": "Системалық қараңғы тақырып", - "system_dark_theme_description": "Системалық қараңғы тақырып сипаттамасы", - "dark_theme_title": "Қараңғы тақырып", - "dark_theme_description": "Қараңғы тақырып сипаттамасы", "reset_config_description": "Конфигурацияны қалпына келтіру сипаттамасы." }, "resource_chart": { diff --git a/assets/translations/lv.json b/assets/translations/lv.json index ef95183b..47a09b86 100644 --- a/assets/translations/lv.json +++ b/assets/translations/lv.json @@ -52,11 +52,11 @@ "privacy_policy": "Privātuma politika" }, "application_settings": { - "system_dark_theme_title": "Sistēmas noklusējuma dizains", - "dark_theme_title": "Tumšs dizains", "title": "Aplikācijas iestatījumi", - "system_dark_theme_description": "Izmantojiet gaišu vai tumšu dizainu atkarībā no sistēmas iestatījumiem", - "dark_theme_description": "Lietojumprogrammas dizaina pārslēgšana", + "system_theme_mode_title": "Sistēmas noklusējuma dizains", + "system_theme_mode_description": "Izmantojiet gaišu vai tumšu dizainu atkarībā no sistēmas iestatījumiem", + "dark_theme_title": "Tumšs dizains", + "change_application_theme": "Lietojumprogrammas dizaina pārslēgšana", "dangerous_settings": "Bīstamie iestatījumi", "reset_config_title": "Atiestatīt lietojumprogrammas konfigurāciju", "reset_config_description": "Atiestatīt API atslēgas un saknes lietotāju." diff --git a/assets/translations/pl.json b/assets/translations/pl.json index 77913061..2c7468b5 100644 --- a/assets/translations/pl.json +++ b/assets/translations/pl.json @@ -56,13 +56,13 @@ }, "application_settings": { "title": "Ustawienia aplikacji", + "system_theme_mode_description": "Użyj jasnego lub ciemnego motywu w zależności od ustawień systemu", + "system_theme_mode_title": "Domyślny motyw systemowy", "dark_theme_title": "Ciemny motyw aplikacji", - "dark_theme_description": "Zmień kolor motywu aplikacji", + "change_application_theme": "Zmień kolor motywu aplikacji", + "dangerous_settings": "Niebezpieczne ustawienia", "reset_config_title": "Resetowanie", - "reset_config_description": "Zresetuj klucze API i użytkownika root.", - "system_dark_theme_description": "Użyj jasnego lub ciemnego motywu w zależności od ustawień systemu", - "system_dark_theme_title": "Domyślny motyw systemowy", - "dangerous_settings": "Niebezpieczne ustawienia" + "reset_config_description": "Zresetuj klucze API i użytkownika root." }, "ssh": { "title": "klucze SSH", diff --git a/assets/translations/ru.json b/assets/translations/ru.json index 2fdd8ca7..d13fea15 100644 --- a/assets/translations/ru.json +++ b/assets/translations/ru.json @@ -75,13 +75,15 @@ }, "application_settings": { "title": "Настройки приложения", + "system_theme_mode_title": "Системная тема", + "system_theme_mode_description": "Будет использована светлая или тёмная тема в зависимости от системных настроек", "dark_theme_title": "Тёмная тема", - "dark_theme_description": "Сменить цветовую тему", + "change_application_theme": "Сменить цветовую тему", + "language": "Язык", + "click_to_change_locale": "Нажмите, чтобы открыть список языков", + "dangerous_settings": "Опасные настройки", "reset_config_title": "Сброс настроек", - "reset_config_description": "Сбросить API ключи и root пользователя.", - "system_dark_theme_title": "Системная тема", - "system_dark_theme_description": "Будет использована светлая или тёмная тема в зависимости от системных настроек", - "dangerous_settings": "Опасные настройки" + "reset_config_description": "Сбросить API ключи и root пользователя." }, "ssh": { "title": "SSH ключи", diff --git a/assets/translations/sk.json b/assets/translations/sk.json index 1f86430a..abebe5aa 100644 --- a/assets/translations/sk.json +++ b/assets/translations/sk.json @@ -102,13 +102,13 @@ }, "application_settings": { "title": "Nastavenia aplikácie", + "system_theme_mode_description": "Použitie svetlej alebo tmavej témy v závislosti od nastavení systému", + "system_theme_mode_title": "Systémová predvolená téma", "dark_theme_title": "Temná téma", - "dark_theme_description": "Zmeniť tému aplikácie", + "change_application_theme": "Zmeniť tému aplikácie", + "dangerous_settings": "Nebezpečné nastavenia", "reset_config_title": "Resetovať nastavenia aplikácie", - "reset_config_description": "Resetovať kľúče API a užívateľa root.", - "system_dark_theme_description": "Použitie svetlej alebo tmavej témy v závislosti od nastavení systému", - "system_dark_theme_title": "Systémová predvolená téma", - "dangerous_settings": "Nebezpečné nastavenia" + "reset_config_description": "Resetovať kľúče API a užívateľa root." }, "ssh": { "title": "Kľúče SSH", diff --git a/assets/translations/sl.json b/assets/translations/sl.json index f8fa3ffa..5715cb43 100644 --- a/assets/translations/sl.json +++ b/assets/translations/sl.json @@ -53,11 +53,11 @@ "application_version_text": "Različica aplikacije" }, "application_settings": { - "dark_theme_title": "Temna tema", "title": "Nastavitve aplikacije", - "system_dark_theme_title": "Privzeta tema sistema", - "system_dark_theme_description": "Uporaba svetle ali temne teme glede na sistemske nastavitve", - "dark_theme_description": "Spreminjanje barvne teme", + "system_theme_mode_title": "Privzeta tema sistema", + "system_theme_mode_description": "Uporaba svetle ali temne teme glede na sistemske nastavitve", + "dark_theme_title": "Temna tema", + "change_application_theme": "Spreminjanje barvne teme", "dangerous_settings": "Nevarne nastavitve", "reset_config_title": "Ponastavitev konfiguracije aplikacije" }, diff --git a/assets/translations/th.json b/assets/translations/th.json index 5fdf9f82..462a495e 100644 --- a/assets/translations/th.json +++ b/assets/translations/th.json @@ -47,9 +47,9 @@ "privacy_policy": "นโยบายความเป็นส่วนตัว" }, "application_settings": { - "dark_theme_description": "สลับธีมแอปพลิเคชั่นของคุณ", "title": "การตั้งค่าแอปพลิเคชัน", "dark_theme_title": "ธีมมืด", + "change_application_theme": "สลับธีมแอปพลิเคชั่นของคุณ", "reset_config_title": "รีเซ็ตค่าดั้งเดิมการตั้งค่าของแอปพลิเคชั่น", "reset_config_description": "รีเซ็ต API key และผู้ใช้งาน root" }, diff --git a/assets/translations/uk.json b/assets/translations/uk.json index 576ad37a..de954c70 100644 --- a/assets/translations/uk.json +++ b/assets/translations/uk.json @@ -41,13 +41,14 @@ "locale": "ua", "application_settings": { "title": "Налаштування додатка", - "reset_config_title": "Скинути налаштування", + "system_theme_mode_title": "Системна тема за замовчуванням", + "system_theme_mode_description": "Використовуйте світлу або темну теми залежно від системних налаштувань", "dark_theme_title": "Темна тема", - "dark_theme_description": "Змінити тему додатка", - "reset_config_description": "Скинути API ключі та root користувача.", - "system_dark_theme_title": "Системна тема за замовчуванням", - "system_dark_theme_description": "Використовуйте світлу або темну теми залежно від системних налаштувань", - "dangerous_settings": "Небезпечні налаштування" + "change_application_theme": "Змінити тему додатка", + "language": "Мова", + "dangerous_settings": "Небезпечні налаштування", + "reset_config_title": "Скинути налаштування", + "reset_config_description": "Скинути API ключі та root користувача." }, "ssh": { "delete_confirm_question": "Ви впевнені, що хочете видалити SSH-ключ?", diff --git a/assets/translations/zh-Hans.json b/assets/translations/zh-Hans.json index 75e9dac3..76870828 100644 --- a/assets/translations/zh-Hans.json +++ b/assets/translations/zh-Hans.json @@ -475,12 +475,13 @@ }, "application_settings": { "title": "应用设置", - "system_dark_theme_title": "系统默认主题", + "system_theme_mode_title": "系统默认主题", + "system_theme_mode_description": "根据系统设置自动使用明亮或暗色主题", "dark_theme_title": "暗色主题", - "system_dark_theme_description": "根据系统设置自动使用明亮或暗色主题", - "dark_theme_description": "切换应用主题", + "change_application_theme": "切换应用主题", "dangerous_settings": "危险设置", "reset_config_title": "重置应用配置", + "delete_server_title": "删除服务器", "reset_config_description": "重置API密钥和root用户。" }, "ssh": { diff --git a/lib/config/app_controller/app_controller.dart b/lib/config/app_controller/app_controller.dart new file mode 100644 index 00000000..f78bae72 --- /dev/null +++ b/lib/config/app_controller/app_controller.dart @@ -0,0 +1,175 @@ +import 'package:flutter/material.dart'; +import 'package:material_color_utilities/material_color_utilities.dart' + as color_utils; +import 'package:selfprivacy/config/get_it_config.dart'; +import 'package:selfprivacy/config/localization.dart'; +import 'package:selfprivacy/config/preferences_repository/preferences_repository.dart'; + +/// A class that many Widgets can interact with to read current app +/// configuration, update it, or listen to its changes. +/// +/// AppController uses repo to change persistent data. +class AppController with ChangeNotifier { + AppController(this._repo); + + /// repo encapsulates retrieval and storage of preferences + final PreferencesRepository _repo; + + /// TODO: to be removed or changed + late final ApiConfigModel _apiConfigModel = getIt.get(); + + bool _loaded = false; + bool get loaded => _loaded; + + // localization + late Locale _locale; + Locale get locale => _locale; + late List _supportedLocales; + List get supportedLocales => _supportedLocales; + + // theme + late ThemeData _lightTheme; + ThemeData get lightTheme => _lightTheme; + late ThemeData _darkTheme; + ThemeData get darkTheme => _darkTheme; + late color_utils.CorePalette _corePalette; + color_utils.CorePalette get corePalette => _corePalette; + + late bool _systemThemeModeActive; + bool get systemThemeModeActive => _systemThemeModeActive; + + late bool _darkThemeModeActive; + bool get darkThemeModeActive => _darkThemeModeActive; + + ThemeMode get themeMode => systemThemeModeActive + ? ThemeMode.system + : darkThemeModeActive + ? ThemeMode.dark + : ThemeMode.light; + + late bool _shouldShowOnboarding; + bool get shouldShowOnboarding => _shouldShowOnboarding; + + Future init({ + // required final AppPreferencesRepository repo, + required final ThemeData lightThemeData, + required final ThemeData darkThemeData, + required final color_utils.CorePalette colorPalette, + }) async { + // _repo = repo; + + await Future.wait([ + // load locale + () async { + _supportedLocales = [ + Localization.systemLocale, + ...await _repo.getSupportedLocales(), + ]; + + _locale = await _repo.getActiveLocale(); + if (_locale != Localization.systemLocale) { + // preset value to other state holders + await _apiConfigModel.setLocaleCode(_locale.languageCode); + await _repo.setDelegateLocale(_locale); + } + }(), + + // load theme mode && initialize theme + () async { + _lightTheme = lightThemeData; + _darkTheme = darkThemeData; + _corePalette = colorPalette; + _darkThemeModeActive = await _repo.getDarkThemeModeFlag(); + _systemThemeModeActive = await _repo.getSystemThemeModeFlag(); + }(), + + // load onboarding flag + () async { + _shouldShowOnboarding = await _repo.getShouldShowOnboarding(); + }(), + ]); + + _loaded = true; + // Important! Inform listeners a change has occurred. + notifyListeners(); + } + + // updateRepoReference + Future setShouldShowOnboarding(final bool newValue) async { + // Do not perform any work if new and old flag values are identical + if (newValue == shouldShowOnboarding) { + return; + } + + // Store the flag in memory + _shouldShowOnboarding = newValue; + notifyListeners(); + + // Persist the change + await _repo.setShouldShowOnboarding(newValue); + } + + Future setSystemThemeModeFlag(final bool newValue) async { + // Do not perform any work if new and old ThemeMode are identical + if (systemThemeModeActive == newValue) { + return; + } + + // Store the new ThemeMode in memory + _systemThemeModeActive = newValue; + + // Inform listeners a change has occurred. + notifyListeners(); + + // Persist the change + await _repo.setSystemModeFlag(newValue); + } + + Future setDarkThemeModeFlag(final bool newValue) async { + // Do not perform any work if new and old ThemeMode are identical + if (darkThemeModeActive == newValue) { + return; + } + + // Store the new ThemeMode in memory + _darkThemeModeActive = newValue; + + // Inform listeners a change has occurred. + notifyListeners(); + + // Persist the change + await _repo.setDarkThemeModeFlag(newValue); + } + + Future setLocale(final Locale newLocale) async { + // Do not perform any work if new and old Locales are identical + if (newLocale == _locale) { + return; + } + + // Store the new Locale in memory + _locale = newLocale; + + if (newLocale == Localization.systemLocale) { + return resetLocale(); + } + + /// update locale delegate, which in turn should update deps + await _repo.setDelegateLocale(newLocale); + + // Persist the change + await _repo.setActiveLocale(newLocale); + // Update other locale holders + await _apiConfigModel.setLocaleCode(newLocale.languageCode); + } + + Future resetLocale() async { + /// update locale delegate, which in turn should update deps + await _repo.resetDelegateLocale(); + + // Persist the change + await _repo.resetActiveLocale(); + // Update other locale holders + await _apiConfigModel.resetLocaleCode(); + } +} diff --git a/lib/config/app_controller/inherited_app_controller.dart b/lib/config/app_controller/inherited_app_controller.dart new file mode 100644 index 00000000..e7eeac7b --- /dev/null +++ b/lib/config/app_controller/inherited_app_controller.dart @@ -0,0 +1,106 @@ +import 'package:flutter/material.dart'; +import 'package:material_color_utilities/material_color_utilities.dart' + as color_utils; +import 'package:selfprivacy/config/app_controller/app_controller.dart'; +import 'package:selfprivacy/config/brand_colors.dart'; +import 'package:selfprivacy/config/preferences_repository/inherited_preferences_repository.dart'; +import 'package:selfprivacy/config/preferences_repository/preferences_repository.dart'; +import 'package:selfprivacy/theming/factory/app_theme_factory.dart'; + +class _AppControllerInjector extends InheritedNotifier { + const _AppControllerInjector({ + required super.child, + required super.notifier, + }); +} + +class InheritedAppController extends StatefulWidget { + const InheritedAppController({ + required this.child, + super.key, + }); + + final Widget child; + + @override + State createState() => _InheritedAppControllerState(); + + static AppController of(final BuildContext context) => context + .dependOnInheritedWidgetOfExactType<_AppControllerInjector>()! + .notifier!; +} + +class _InheritedAppControllerState extends State { + // actual state provider + late AppController controller; + // hold local reference to active repo + late PreferencesRepository _repo; + + bool initTriggerred = false; + + @override + void didChangeDependencies() { + /// update reference on dependency change + _repo = InheritedPreferencesRepository.of(context)!; + + if (!initTriggerred) { + /// hook controller repo to local reference + controller = AppController(_repo); + initialize(); + initTriggerred = true; + } + + super.didChangeDependencies(); + } + + Future initialize() async { + late final ThemeData lightThemeData; + late final ThemeData darkThemeData; + late final color_utils.CorePalette colorPalette; + + await Future.wait( + >[ + () async { + lightThemeData = await AppThemeFactory.create( + isDark: false, + fallbackColor: BrandColors.primary, + ); + }(), + () async { + darkThemeData = await AppThemeFactory.create( + isDark: true, + fallbackColor: BrandColors.primary, + ); + }(), + () async { + colorPalette = (await AppThemeFactory.getCorePalette()) ?? + color_utils.CorePalette.of(BrandColors.primary.value); + }(), + ], + ); + + await controller.init( + colorPalette: colorPalette, + lightThemeData: lightThemeData, + darkThemeData: darkThemeData, + ); + + WidgetsBinding.instance.addPostFrameCallback((final _) { + if (mounted) { + setState(() {}); + } + }); + } + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + + @override + Widget build(final BuildContext context) => _AppControllerInjector( + notifier: controller, + child: widget.child, + ); +} diff --git a/lib/config/bloc_config.dart b/lib/config/bloc_config.dart index 06cb7244..83027d7b 100644 --- a/lib/config/bloc_config.dart +++ b/lib/config/bloc_config.dart @@ -1,14 +1,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:selfprivacy/logic/bloc/backups/backups_bloc.dart'; -import 'package:selfprivacy/logic/bloc/connection_status/connection_status_bloc.dart'; +import 'package:selfprivacy/logic/bloc/connection_status_bloc.dart'; import 'package:selfprivacy/logic/bloc/devices/devices_bloc.dart'; import 'package:selfprivacy/logic/bloc/recovery_key/recovery_key_bloc.dart'; import 'package:selfprivacy/logic/bloc/server_jobs/server_jobs_bloc.dart'; import 'package:selfprivacy/logic/bloc/services/services_bloc.dart'; import 'package:selfprivacy/logic/bloc/users/users_bloc.dart'; import 'package:selfprivacy/logic/bloc/volumes/volumes_bloc.dart'; -import 'package:selfprivacy/logic/cubit/app_settings/app_settings_cubit.dart'; import 'package:selfprivacy/logic/cubit/client_jobs/client_jobs_cubit.dart'; import 'package:selfprivacy/logic/cubit/dns_records/dns_records_cubit.dart'; import 'package:selfprivacy/logic/cubit/server_detailed_info/server_detailed_info_cubit.dart'; @@ -56,58 +55,46 @@ class BlocAndProviderConfigState extends State { } @override - Widget build(final BuildContext context) { - const isDark = false; - const isAutoDark = true; - - return MultiProvider( - providers: [ - BlocProvider( - create: (final _) => AppSettingsCubit( - isDarkModeOn: isDark, - isAutoDarkModeOn: isAutoDark, - isOnboardingShowing: true, - )..load(), - ), - BlocProvider( - create: (final _) => supportSystemCubit, - ), - BlocProvider( - create: (final _) => serverInstallationCubit, - lazy: false, - ), - BlocProvider( - create: (final _) => usersBloc, - lazy: false, - ), - BlocProvider( - create: (final _) => servicesBloc, - ), - BlocProvider( - create: (final _) => backupsBloc, - ), - BlocProvider( - create: (final _) => dnsRecordsCubit, - ), - BlocProvider( - create: (final _) => recoveryKeyBloc, - ), - BlocProvider( - create: (final _) => devicesBloc, - ), - BlocProvider( - create: (final _) => serverJobsBloc, - ), - BlocProvider(create: (final _) => connectionStatusBloc), - BlocProvider( - create: (final _) => serverDetailsCubit, - ), - BlocProvider(create: (final _) => volumesBloc), - BlocProvider( - create: (final _) => JobsCubit(), - ), - ], - child: widget.child, - ); - } + Widget build(final BuildContext context) => MultiProvider( + providers: [ + BlocProvider( + create: (final _) => supportSystemCubit, + ), + BlocProvider( + create: (final _) => serverInstallationCubit, + lazy: false, + ), + BlocProvider( + create: (final _) => usersBloc, + lazy: false, + ), + BlocProvider( + create: (final _) => servicesBloc, + ), + BlocProvider( + create: (final _) => backupsBloc, + ), + BlocProvider( + create: (final _) => dnsRecordsCubit, + ), + BlocProvider( + create: (final _) => recoveryKeyBloc, + ), + BlocProvider( + create: (final _) => devicesBloc, + ), + BlocProvider( + create: (final _) => serverJobsBloc, + ), + BlocProvider(create: (final _) => connectionStatusBloc), + BlocProvider( + create: (final _) => serverDetailsCubit, + ), + BlocProvider(create: (final _) => volumesBloc), + BlocProvider( + create: (final _) => JobsCubit(), + ), + ], + child: widget.child, + ); } diff --git a/lib/config/get_it_config.dart b/lib/config/get_it_config.dart index 78e40261..5f6d55cb 100644 --- a/lib/config/get_it_config.dart +++ b/lib/config/get_it_config.dart @@ -1,21 +1,23 @@ import 'package:get_it/get_it.dart'; import 'package:selfprivacy/logic/get_it/api_config.dart'; import 'package:selfprivacy/logic/get_it/api_connection_repository.dart'; -import 'package:selfprivacy/logic/get_it/console.dart'; +import 'package:selfprivacy/logic/get_it/console_model.dart'; import 'package:selfprivacy/logic/get_it/navigation.dart'; export 'package:selfprivacy/logic/get_it/api_config.dart'; export 'package:selfprivacy/logic/get_it/api_connection_repository.dart'; -export 'package:selfprivacy/logic/get_it/console.dart'; +export 'package:selfprivacy/logic/get_it/console_model.dart'; export 'package:selfprivacy/logic/get_it/navigation.dart'; final GetIt getIt = GetIt.instance; Future getItSetup() async { getIt.registerSingleton(NavigationService()); - getIt.registerSingleton(ConsoleModel()); - getIt.registerSingleton(ApiConfigModel()..init()); + + final apiConfigModel = ApiConfigModel(); + await apiConfigModel.init(); + getIt.registerSingleton(apiConfigModel); getIt.registerSingleton( ApiConnectionRepository()..init(), diff --git a/lib/config/hive_config.dart b/lib/config/hive_config.dart index ea6cca9c..85a86583 100644 --- a/lib/config/hive_config.dart +++ b/lib/config/hive_config.dart @@ -74,17 +74,20 @@ class HiveConfig { /// Mappings for the different boxes and their keys class BNames { - /// App settings box. Contains app settings like [isDarkModeOn], [isOnboardingShowing] + /// App settings box. Contains app settings like [darkThemeModeOn], [shouldShowOnboarding] static String appSettingsBox = 'appSettings'; /// A boolean field of [appSettingsBox] box. - static String isDarkModeOn = 'isDarkModeOn'; + static String darkThemeModeOn = 'isDarkModeOn'; /// A boolean field of [appSettingsBox] box. - static String isAutoDarkModeOn = 'isAutoDarkModeOn'; + static String systemThemeModeOn = 'isAutoDarkModeOn'; /// A boolean field of [appSettingsBox] box. - static String isOnboardingShowing = 'isOnboardingShowing'; + static String shouldShowOnboarding = 'isOnboardingShowing'; + + /// A string field + static String appLocale = 'appLocale'; /// Encryption key to decrypt [serverInstallationBox] and [usersBox] box. static String serverInstallationEncryptionKey = 'key'; diff --git a/lib/config/localization.dart b/lib/config/localization.dart index 3f55fae2..e5da63ad 100644 --- a/lib/config/localization.dart +++ b/lib/config/localization.dart @@ -3,40 +3,76 @@ import 'package:flutter/material.dart'; class Localization extends StatelessWidget { const Localization({ + required this.child, super.key, - this.child, }); - final Widget? child; + /// value for resetting locale in settings to system default + static const systemLocale = Locale('system'); + + // when adding new locale, add corresponding native language name to mapper + // below + static const supportedLocales = [ + Locale('ar'), + Locale('az'), + Locale('be'), + Locale('cs'), + Locale('de'), + Locale('en'), + Locale('es'), + Locale('et'), + Locale('fr'), + Locale('he'), + Locale('kk'), + Locale('lv'), + Locale('mk'), + Locale('pl'), + Locale('ru'), + Locale('sk'), + Locale('sl'), + Locale('th'), + Locale('uk'), + Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hans'), + ]; + + // https://en.wikipedia.org/wiki/IETF_language_tag#List_of_common_primary_language_subtags + static final _languageNames = { + systemLocale: 'System default', + const Locale('ar'): 'العربية', + const Locale('az'): 'Azərbaycan', + const Locale('be'): 'беларуская', + const Locale('cs'): 'čeština', + const Locale('de'): 'Deutsch', + const Locale('en'): 'English', + const Locale('es'): 'español', + const Locale('et'): 'eesti', + const Locale('fr'): 'français', + const Locale('he'): 'עברית', + const Locale('kk'): 'Қазақша', + const Locale('lv'): 'latviešu', + const Locale('mk'): 'македонски јазик', + const Locale('pl'): 'polski', + const Locale('ru'): 'русский', + const Locale('sk'): 'slovenčina', + const Locale('sl'): 'slovenski', + const Locale('th'): 'ไทย', + const Locale('uk'): 'Українська', + const Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hans'): '中文', + }; + + static String getLanguageName(final Locale locale) => + _languageNames[locale] ?? locale.languageCode; + + final Widget child; + @override Widget build(final BuildContext context) => EasyLocalization( - supportedLocales: const [ - Locale('ar'), - Locale('az'), - Locale('be'), - Locale('cs'), - Locale('de'), - Locale('en'), - Locale('es'), - Locale('et'), - Locale('fr'), - Locale('he'), - Locale('kk'), - Locale('lv'), - Locale('mk'), - Locale('pl'), - Locale('ru'), - Locale('sk'), - Locale('sl'), - Locale('th'), - Locale('uk'), - Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hans'), - ], + supportedLocales: supportedLocales, path: 'assets/translations', fallbackLocale: const Locale('en'), useFallbackTranslations: true, saveLocale: false, useOnlyLangCode: false, - child: child!, + child: child, ); } diff --git a/lib/config/preferences_repository/datasources/preferences_datasource.dart b/lib/config/preferences_repository/datasources/preferences_datasource.dart new file mode 100644 index 00000000..8c8498dd --- /dev/null +++ b/lib/config/preferences_repository/datasources/preferences_datasource.dart @@ -0,0 +1,33 @@ +/// abstraction for manipulation of stored app preferences +abstract class PreferencesDataSource { + /// should onboarding be shown + Future getOnboardingFlag(); + + /// should onboarding be shown + Future setOnboardingFlag(final bool newValue); + + // TODO: should probably deprecate the following, instead add the + // getThemeMode and setThemeMode methods, which store one value instead of + // flags. + + /// should system theme mode be enabled + Future getSystemThemeModeFlag(); + + /// should system theme mode be enabled + Future setSystemThemeModeFlag(final bool newValue); + + /// should dark theme be enabled + Future getDarkThemeModeFlag(); + + /// should dark theme be enabled + Future setDarkThemeModeFlag(final bool newValue); + + /// locale, as set by user + /// + /// + /// when null, app takes system locale + Future getLocale(); + + /// locale, as set by user + Future setLocale(final String? newLocale); +} diff --git a/lib/config/preferences_repository/datasources/preferences_hive_datasource.dart b/lib/config/preferences_repository/datasources/preferences_hive_datasource.dart new file mode 100644 index 00000000..f4e30130 --- /dev/null +++ b/lib/config/preferences_repository/datasources/preferences_hive_datasource.dart @@ -0,0 +1,40 @@ +import 'package:hive/hive.dart'; +import 'package:selfprivacy/config/hive_config.dart'; +import 'package:selfprivacy/config/preferences_repository/datasources/preferences_datasource.dart'; + +/// app preferences data source hive implementation +class PreferencesHiveDataSource implements PreferencesDataSource { + final Box _appSettingsBox = Hive.box(BNames.appSettingsBox); + + @override + Future getOnboardingFlag() async => + _appSettingsBox.get(BNames.shouldShowOnboarding, defaultValue: true); + + @override + Future setOnboardingFlag(final bool newValue) async => + _appSettingsBox.put(BNames.shouldShowOnboarding, newValue); + + @override + Future getSystemThemeModeFlag() async => + _appSettingsBox.get(BNames.systemThemeModeOn); + + @override + Future setSystemThemeModeFlag(final bool newValue) async => + _appSettingsBox.put(BNames.systemThemeModeOn, newValue); + + @override + Future getDarkThemeModeFlag() async => + _appSettingsBox.get(BNames.darkThemeModeOn); + + @override + Future setDarkThemeModeFlag(final bool newValue) async => + _appSettingsBox.put(BNames.darkThemeModeOn, newValue); + + @override + Future getLocale() async => _appSettingsBox.get(BNames.appLocale); + + @override + Future setLocale(final String? newLocale) async => newLocale == null + ? _appSettingsBox.delete(BNames.appLocale) + : _appSettingsBox.put(BNames.appLocale, newLocale); +} diff --git a/lib/config/preferences_repository/inherited_preferences_repository.dart b/lib/config/preferences_repository/inherited_preferences_repository.dart new file mode 100644 index 00000000..c90dc91b --- /dev/null +++ b/lib/config/preferences_repository/inherited_preferences_repository.dart @@ -0,0 +1,64 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:selfprivacy/config/preferences_repository/datasources/preferences_datasource.dart'; +import 'package:selfprivacy/config/preferences_repository/preferences_repository.dart'; + +class _PreferencesRepositoryInjector extends InheritedWidget { + const _PreferencesRepositoryInjector({ + required this.settingsRepository, + required super.child, + }); + + final PreferencesRepository settingsRepository; + + @override + bool updateShouldNotify( + covariant final _PreferencesRepositoryInjector oldWidget, + ) => + oldWidget.settingsRepository != settingsRepository; +} + +/// Creates and injects app preferences repository inside widget tree. +class InheritedPreferencesRepository extends StatefulWidget { + const InheritedPreferencesRepository({ + required this.child, + required this.dataSource, + super.key, + }); + + final PreferencesDataSource dataSource; + final Widget child; + + @override + State createState() => + _InheritedPreferencesRepositoryState(); + + static PreferencesRepository? of(final BuildContext context) => context + .dependOnInheritedWidgetOfExactType<_PreferencesRepositoryInjector>() + ?.settingsRepository; +} + +class _InheritedPreferencesRepositoryState + extends State { + late PreferencesRepository repo; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + + /// recreate repo each time dependencies change + repo = PreferencesRepository( + dataSource: widget.dataSource, + setDelegateLocale: EasyLocalization.of(context)!.setLocale, + resetDelegateLocale: EasyLocalization.of(context)!.resetLocale, + getDelegateLocale: () => EasyLocalization.of(context)!.locale, + getSupportedLocales: () => EasyLocalization.of(context)!.supportedLocales, + ); + } + + @override + Widget build(final BuildContext context) => _PreferencesRepositoryInjector( + settingsRepository: repo, + child: widget.child, + ); +} diff --git a/lib/config/preferences_repository/preferences_repository.dart b/lib/config/preferences_repository/preferences_repository.dart new file mode 100644 index 00000000..086156e5 --- /dev/null +++ b/lib/config/preferences_repository/preferences_repository.dart @@ -0,0 +1,73 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:selfprivacy/config/localization.dart'; +import 'package:selfprivacy/config/preferences_repository/datasources/preferences_datasource.dart'; + +class PreferencesRepository { + const PreferencesRepository({ + required this.dataSource, + required this.getSupportedLocales, + required this.getDelegateLocale, + required this.setDelegateLocale, + required this.resetDelegateLocale, + }); + + final PreferencesDataSource dataSource; + + /// easy localizations don't expose type of localization provider, + /// so it needs to be this crutchy (I could've created one more class-wrapper, + /// containing needed functions, but perceive it as boilerplate, because we + /// don't need additional encapsulation level here) + final FutureOr Function(Locale) setDelegateLocale; + final FutureOr Function() resetDelegateLocale; + final FutureOr> Function() getSupportedLocales; + final FutureOr Function() getDelegateLocale; + + Future getSystemThemeModeFlag() async => + (await dataSource.getSystemThemeModeFlag()) ?? true; + + Future setSystemThemeModeFlag(final bool newValue) async => + dataSource.setSystemThemeModeFlag(newValue); + + Future getDarkThemeModeFlag() async => + (await dataSource.getDarkThemeModeFlag()) ?? false; + + Future setDarkThemeModeFlag(final bool newValue) async => + dataSource.setDarkThemeModeFlag(newValue); + + Future setSystemModeFlag(final bool newValue) async => + dataSource.setSystemThemeModeFlag(newValue); + + Future> supportedLocales() async => getSupportedLocales(); + + Future getActiveLocale() async { + Locale? chosenLocale; + + final String? storedLocaleCode = await dataSource.getLocale(); + if (storedLocaleCode != null) { + chosenLocale = Locale(storedLocaleCode); + } + + // when it's null fallback on delegate locale + chosenLocale ??= Localization.systemLocale; + + return chosenLocale; + } + + Future setActiveLocale(final Locale newLocale) async { + await dataSource.setLocale(newLocale.toString()); + } + + Future resetActiveLocale() async { + await dataSource.setLocale(null); + } + + /// true when we need to show onboarding + Future getShouldShowOnboarding() async => + dataSource.getOnboardingFlag(); + + /// true when we need to show onboarding + Future setShouldShowOnboarding(final bool newValue) => + dataSource.setOnboardingFlag(newValue); +} diff --git a/lib/logic/api_maps/graphql_maps/graphql_api_map.dart b/lib/logic/api_maps/graphql_maps/graphql_api_map.dart index 6a00f5b6..d2823a56 100644 --- a/lib/logic/api_maps/graphql_maps/graphql_api_map.dart +++ b/lib/logic/api_maps/graphql_maps/graphql_api_map.dart @@ -1,18 +1,14 @@ +import 'dart:convert'; import 'dart:io'; import 'package:graphql_flutter/graphql_flutter.dart'; import 'package:http/io_client.dart'; import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/logic/api_maps/tls_options.dart'; -import 'package:selfprivacy/logic/models/message.dart'; +import 'package:selfprivacy/logic/models/console_log.dart'; -void _logToAppConsole(final T objectToLog) { - getIt.get().addMessage( - Message( - text: objectToLog.toString(), - ), - ); -} +void _addConsoleLog(final ConsoleLog message) => + getIt.get().log(message); class RequestLoggingLink extends Link { @override @@ -20,13 +16,14 @@ class RequestLoggingLink extends Link { final Request request, [ final NextLink? forward, ]) async* { - getIt.get().addMessage( - GraphQlRequestMessage( - operation: request.operation, - variables: request.variables, - context: request.context, - ), - ); + _addConsoleLog( + GraphQlRequestConsoleLog( + // context: request.context, + operationType: request.type.name, + operation: request.operation, + variables: request.variables, + ), + ); yield* forward!(request); } } @@ -35,20 +32,26 @@ class ResponseLoggingParser extends ResponseParser { @override Response parseResponse(final Map body) { final response = super.parseResponse(body); - getIt.get().addMessage( - GraphQlResponseMessage( - data: response.data, - errors: response.errors, - context: response.context, - ), - ); + _addConsoleLog( + GraphQlResponseConsoleLog( + // context: response.context, + data: response.data, + errors: response.errors, + rawResponse: jsonEncode(response.response), + ), + ); return response; } @override GraphQLError parseError(final Map error) { final graphQlError = super.parseError(error); - _logToAppConsole(graphQlError); + _addConsoleLog( + ManualConsoleLog.warning( + customTitle: 'GraphQL Error', + content: graphQlError.toString(), + ), + ); return graphQlError; } } @@ -113,14 +116,15 @@ abstract class GraphQLApiMap { ); } - String get _locale => getIt.get().localeCode ?? 'en'; + String get _locale => getIt.get().localeCode; String get _token { String token = ''; final serverDetails = getIt().serverDetails; if (serverDetails != null) { - token = getIt().serverDetails!.apiToken; + token = serverDetails.apiToken; } + return token; } diff --git a/lib/logic/api_maps/rest_maps/rest_api_map.dart b/lib/logic/api_maps/rest_maps/rest_api_map.dart index 3a8d0571..5426248f 100644 --- a/lib/logic/api_maps/rest_maps/rest_api_map.dart +++ b/lib/logic/api_maps/rest_maps/rest_api_map.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:convert'; import 'dart:developer'; import 'dart:io'; @@ -6,7 +7,7 @@ import 'package:dio/dio.dart'; import 'package:dio/io.dart'; import 'package:pretty_dio_logger/pretty_dio_logger.dart'; import 'package:selfprivacy/config/get_it_config.dart'; -import 'package:selfprivacy/logic/models/message.dart'; +import 'package:selfprivacy/logic/models/console_log.dart'; abstract class RestApiMap { Future getClient({final BaseOptions? customOptions}) async { @@ -57,8 +58,8 @@ abstract class RestApiMap { } class ConsoleInterceptor extends InterceptorsWrapper { - void addMessage(final Message message) { - getIt.get().addMessage(message); + void addConsoleLog(final ConsoleLog message) { + getIt.get().log(message); } @override @@ -66,12 +67,12 @@ class ConsoleInterceptor extends InterceptorsWrapper { final RequestOptions options, final RequestInterceptorHandler handler, ) async { - addMessage( - RestApiRequestMessage( - method: options.method, - data: options.data.toString(), - headers: options.headers, + addConsoleLog( + RestApiRequestConsoleLog( uri: options.uri, + method: options.method, + headers: options.headers, + data: jsonEncode(options.data), ), ); return super.onRequest(options, handler); @@ -82,12 +83,12 @@ class ConsoleInterceptor extends InterceptorsWrapper { final Response response, final ResponseInterceptorHandler handler, ) async { - addMessage( - RestApiResponseMessage( + addConsoleLog( + RestApiResponseConsoleLog( + uri: response.realUri, method: response.requestOptions.method, statusCode: response.statusCode, - data: response.data.toString(), - uri: response.realUri, + data: jsonEncode(response.data), ), ); return super.onResponse( @@ -103,10 +104,13 @@ class ConsoleInterceptor extends InterceptorsWrapper { ) async { final Response? response = err.response; log(err.toString()); - addMessage( - Message.warn( - text: - 'response-uri: ${response?.realUri}\ncode: ${response?.statusCode}\ndata: ${response?.toString()}\n', + + addConsoleLog( + ManualConsoleLog.warning( + customTitle: 'RestAPI error', + content: '"uri": "${response?.realUri}",\n' + '"status_code": ${response?.statusCode},\n' + '"response": ${jsonEncode(response)}', ), ); return super.onError(err, handler); diff --git a/lib/logic/bloc/connection_status/connection_status_bloc.dart b/lib/logic/bloc/connection_status/connection_status_bloc.dart deleted file mode 100644 index 868f05d0..00000000 --- a/lib/logic/bloc/connection_status/connection_status_bloc.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'dart:async'; - -import 'package:equatable/equatable.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:selfprivacy/config/get_it_config.dart'; - -part 'connection_status_event.dart'; -part 'connection_status_state.dart'; - -class ConnectionStatusBloc - extends Bloc { - ConnectionStatusBloc() - : super( - const ConnectionStatusState( - connectionStatus: ConnectionStatus.nonexistent, - ), - ) { - on((final event, final emit) { - emit(ConnectionStatusState(connectionStatus: event.connectionStatus)); - }); - final apiConnectionRepository = getIt(); - _apiConnectionStatusSubscription = - apiConnectionRepository.connectionStatusStream.listen( - (final ConnectionStatus connectionStatus) { - add( - ConnectionStatusChanged(connectionStatus), - ); - }, - ); - } - - StreamSubscription? _apiConnectionStatusSubscription; - - @override - Future close() { - _apiConnectionStatusSubscription?.cancel(); - return super.close(); - } -} diff --git a/lib/logic/bloc/connection_status/connection_status_event.dart b/lib/logic/bloc/connection_status/connection_status_event.dart deleted file mode 100644 index 0fc6e72f..00000000 --- a/lib/logic/bloc/connection_status/connection_status_event.dart +++ /dev/null @@ -1,14 +0,0 @@ -part of 'connection_status_bloc.dart'; - -sealed class ConnectionStatusEvent extends Equatable { - const ConnectionStatusEvent(); -} - -class ConnectionStatusChanged extends ConnectionStatusEvent { - const ConnectionStatusChanged(this.connectionStatus); - - final ConnectionStatus connectionStatus; - - @override - List get props => [connectionStatus]; -} diff --git a/lib/logic/bloc/connection_status/connection_status_state.dart b/lib/logic/bloc/connection_status/connection_status_state.dart deleted file mode 100644 index 258765c1..00000000 --- a/lib/logic/bloc/connection_status/connection_status_state.dart +++ /dev/null @@ -1,12 +0,0 @@ -part of 'connection_status_bloc.dart'; - -class ConnectionStatusState extends Equatable { - const ConnectionStatusState({ - required this.connectionStatus, - }); - - final ConnectionStatus connectionStatus; - - @override - List get props => [connectionStatus]; -} diff --git a/lib/logic/bloc/connection_status_bloc.dart b/lib/logic/bloc/connection_status_bloc.dart new file mode 100644 index 00000000..f5315f46 --- /dev/null +++ b/lib/logic/bloc/connection_status_bloc.dart @@ -0,0 +1,27 @@ +import 'dart:async'; + +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:selfprivacy/config/get_it_config.dart'; + +/// basically, a bus for other blocs to listen to server status updates +class ConnectionStatusBloc extends Bloc { + ConnectionStatusBloc() : super(ConnectionStatus.nonexistent) { + on( + (final newStatus, final emit) => emit(newStatus), + ); + + final apiConnectionRepository = getIt(); + _apiConnectionStatusSubscription = + apiConnectionRepository.connectionStatusStream.listen( + (final ConnectionStatus newStatus) => add(newStatus), + ); + } + + StreamSubscription? _apiConnectionStatusSubscription; + + @override + Future close() { + _apiConnectionStatusSubscription?.cancel(); + return super.close(); + } +} diff --git a/lib/logic/cubit/app_settings/app_settings_cubit.dart b/lib/logic/cubit/app_settings/app_settings_cubit.dart deleted file mode 100644 index 548ed812..00000000 --- a/lib/logic/cubit/app_settings/app_settings_cubit.dart +++ /dev/null @@ -1,66 +0,0 @@ -import 'package:equatable/equatable.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:hive/hive.dart'; -import 'package:material_color_utilities/material_color_utilities.dart' - as color_utils; -import 'package:selfprivacy/config/brand_colors.dart'; -import 'package:selfprivacy/config/hive_config.dart'; -import 'package:selfprivacy/theming/factory/app_theme_factory.dart'; - -export 'package:provider/provider.dart'; - -part 'app_settings_state.dart'; - -class AppSettingsCubit extends Cubit { - AppSettingsCubit({ - required final bool isDarkModeOn, - required final bool isAutoDarkModeOn, - required final bool isOnboardingShowing, - }) : super( - AppSettingsState( - isDarkModeOn: isDarkModeOn, - isAutoDarkModeOn: isAutoDarkModeOn, - isOnboardingShowing: isOnboardingShowing, - ), - ); - - Box box = Hive.box(BNames.appSettingsBox); - - void load() async { - final bool? isDarkModeOn = box.get(BNames.isDarkModeOn); - final bool? isAutoDarkModeOn = box.get(BNames.isAutoDarkModeOn); - final bool? isOnboardingShowing = box.get(BNames.isOnboardingShowing); - emit( - state.copyWith( - isDarkModeOn: isDarkModeOn, - isAutoDarkModeOn: isAutoDarkModeOn, - isOnboardingShowing: isOnboardingShowing, - ), - ); - WidgetsFlutterBinding.ensureInitialized(); - final color_utils.CorePalette? colorPalette = - await AppThemeFactory.getCorePalette(); - emit( - state.copyWith( - corePalette: colorPalette, - ), - ); - } - - void updateDarkMode({required final bool isDarkModeOn}) { - box.put(BNames.isDarkModeOn, isDarkModeOn); - emit(state.copyWith(isDarkModeOn: isDarkModeOn)); - } - - void updateAutoDarkMode({required final bool isAutoDarkModeOn}) { - box.put(BNames.isAutoDarkModeOn, isAutoDarkModeOn); - emit(state.copyWith(isAutoDarkModeOn: isAutoDarkModeOn)); - } - - void turnOffOnboarding({final bool isOnboardingShowing = false}) { - box.put(BNames.isOnboardingShowing, isOnboardingShowing); - - emit(state.copyWith(isOnboardingShowing: isOnboardingShowing)); - } -} diff --git a/lib/logic/cubit/app_settings/app_settings_state.dart b/lib/logic/cubit/app_settings/app_settings_state.dart deleted file mode 100644 index ad364d66..00000000 --- a/lib/logic/cubit/app_settings/app_settings_state.dart +++ /dev/null @@ -1,35 +0,0 @@ -part of 'app_settings_cubit.dart'; - -class AppSettingsState extends Equatable { - const AppSettingsState({ - required this.isDarkModeOn, - required this.isAutoDarkModeOn, - required this.isOnboardingShowing, - this.corePalette, - }); - - final bool isDarkModeOn; - final bool isAutoDarkModeOn; - final bool isOnboardingShowing; - final color_utils.CorePalette? corePalette; - - AppSettingsState copyWith({ - final bool? isDarkModeOn, - final bool? isAutoDarkModeOn, - final bool? isOnboardingShowing, - final color_utils.CorePalette? corePalette, - }) => - AppSettingsState( - isDarkModeOn: isDarkModeOn ?? this.isDarkModeOn, - isAutoDarkModeOn: isAutoDarkModeOn ?? this.isAutoDarkModeOn, - isOnboardingShowing: isOnboardingShowing ?? this.isOnboardingShowing, - corePalette: corePalette ?? this.corePalette, - ); - - color_utils.CorePalette get corePaletteOrDefault => - corePalette ?? color_utils.CorePalette.of(BrandColors.primary.value); - - @override - List get props => - [isDarkModeOn, isAutoDarkModeOn, isOnboardingShowing, corePalette]; -} diff --git a/lib/logic/cubit/server_installation/server_installation_repository.dart b/lib/logic/cubit/server_installation/server_installation_repository.dart index 90035fea..de8a426d 100644 --- a/lib/logic/cubit/server_installation/server_installation_repository.dart +++ b/lib/logic/cubit/server_installation/server_installation_repository.dart @@ -475,7 +475,7 @@ class ServerInstallationRepository { Future deleteServerDetails() async { await box.delete(BNames.serverDetails); - getIt().init(); + await getIt().init(); } Future saveServerProviderType(final ServerProviderType type) async { @@ -501,7 +501,7 @@ class ServerInstallationRepository { Future deleteServerProviderKey() async { await box.delete(BNames.hetznerKey); - getIt().init(); + await getIt().init(); } Future saveBackblazeKey( @@ -512,7 +512,7 @@ class ServerInstallationRepository { Future deleteBackblazeKey() async { await box.delete(BNames.backblazeCredential); - getIt().init(); + await getIt().init(); } Future setDnsApiToken(final String key) async { @@ -521,7 +521,7 @@ class ServerInstallationRepository { Future deleteDnsProviderKey() async { await box.delete(BNames.cloudFlareKey); - getIt().init(); + await getIt().init(); } Future saveDomain(final ServerDomain serverDomain) async { @@ -530,7 +530,7 @@ class ServerInstallationRepository { Future deleteDomain() async { await box.delete(BNames.serverDomain); - getIt().init(); + await getIt().init(); } Future saveIsServerStarted(final bool value) async { @@ -604,6 +604,6 @@ class ServerInstallationRepository { BNames.hasFinalChecked, BNames.isLoading, ]); - getIt().init(); + await getIt().init(); } } diff --git a/lib/logic/cubit/server_installation/server_installation_state.dart b/lib/logic/cubit/server_installation/server_installation_state.dart index 3f3ea267..8636ffee 100644 --- a/lib/logic/cubit/server_installation/server_installation_state.dart +++ b/lib/logic/cubit/server_installation/server_installation_state.dart @@ -49,9 +49,10 @@ abstract class ServerInstallationState extends Equatable { bool get isPrimaryUserFilled => rootUser != null; bool get isServerCreated => serverDetails != null; - bool get isFullyInitilized => _fulfilementList.every((final el) => el!); + bool get isFullyInitialized => + _fulfillmentList.every((final el) => el ?? false); ServerSetupProgress get progress => ServerSetupProgress - .values[_fulfilementList.where((final el) => el!).length]; + .values[_fulfillmentList.where((final el) => el!).length]; int get porgressBar { if (progress.index < 6) { @@ -63,7 +64,7 @@ abstract class ServerInstallationState extends Equatable { } } - List get _fulfilementList { + List get _fulfillmentList { final List res = [ isServerProviderApiKeyFilled, isServerTypeFilled, diff --git a/lib/logic/get_it/api_config.dart b/lib/logic/get_it/api_config.dart index b32cd995..33632f38 100644 --- a/lib/logic/get_it/api_config.dart +++ b/lib/logic/get_it/api_config.dart @@ -9,7 +9,6 @@ class ApiConfigModel { final Box _box = Hive.box(BNames.serverInstallationBox); ServerHostingDetails? get serverDetails => _serverDetails; - String? get localeCode => _localeCode; String? get serverProviderKey => _serverProviderKey; String? get serverLocation => _serverLocation; String? get serverType => _serverType; @@ -21,7 +20,12 @@ class ApiConfigModel { ServerDomain? get serverDomain => _serverDomain; BackblazeBucket? get backblazeBucket => _backblazeBucket; + static const localeCodeFallback = 'en'; String? _localeCode; + String get localeCode => _localeCode ?? localeCodeFallback; + Future setLocaleCode(final String value) async => _localeCode = value; + Future resetLocaleCode() async => _localeCode = null; + String? _serverProviderKey; String? _serverLocation; String? _dnsProviderKey; @@ -33,10 +37,6 @@ class ApiConfigModel { ServerDomain? _serverDomain; BackblazeBucket? _backblazeBucket; - Future setLocaleCode(final String value) async { - _localeCode = value; - } - Future storeServerProviderType(final ServerProviderType value) async { await _box.put(BNames.serverProvider, value); _serverProvider = value; @@ -88,7 +88,6 @@ class ApiConfigModel { } void clear() { - _localeCode = null; _serverProviderKey = null; _dnsProvider = null; _serverLocation = null; @@ -101,8 +100,7 @@ class ApiConfigModel { _serverProvider = null; } - void init() { - _localeCode = 'en'; + Future init() async { _serverProviderKey = _box.get(BNames.hetznerKey); _serverLocation = _box.get(BNames.serverLocation); _dnsProviderKey = _box.get(BNames.cloudFlareKey); diff --git a/lib/logic/get_it/console.dart b/lib/logic/get_it/console.dart deleted file mode 100644 index a523c5e8..00000000 --- a/lib/logic/get_it/console.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:selfprivacy/logic/models/message.dart'; - -class ConsoleModel extends ChangeNotifier { - final List _messages = []; - - List get messages => _messages; - - void addMessage(final Message message) { - messages.add(message); - notifyListeners(); - // Make sure we don't have too many messages - if (messages.length > 500) { - messages.removeAt(0); - } - } -} diff --git a/lib/logic/get_it/console_model.dart b/lib/logic/get_it/console_model.dart new file mode 100644 index 00000000..6ec13264 --- /dev/null +++ b/lib/logic/get_it/console_model.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; +import 'package:selfprivacy/logic/models/console_log.dart'; + +class ConsoleModel extends ChangeNotifier { + /// limit for history, so logs won't affect memory and overflow + static const logBufferLimit = 500; + + /// differs from log buffer limit so as to not rearrange memory each time + /// we add incoming log + static const incomingBufferBreakpoint = 750; + + final List _logs = []; + final List _incomingQueue = []; + + bool _paused = false; + bool get paused => _paused; + List get logs => _logs; + + void log(final ConsoleLog newLog) { + if (paused) { + _incomingQueue.add(newLog); + if (_incomingQueue.length > incomingBufferBreakpoint) { + logs.removeRange(0, _incomingQueue.length - logBufferLimit); + } + } else { + logs.add(newLog); + _updateQueue(); + } + } + + void play() { + _logs.addAll(_incomingQueue); + _paused = false; + _updateQueue(); + _incomingQueue.clear(); + } + + void pause() { + _paused = true; + notifyListeners(); + } + + /// drop logs over the limit and + void _updateQueue() { + // Make sure we don't have too many + if (logs.length > logBufferLimit) { + logs.removeRange(0, logs.length - logBufferLimit); + } + notifyListeners(); + } +} diff --git a/lib/logic/models/console_log.dart b/lib/logic/models/console_log.dart new file mode 100644 index 00000000..bd3eda8c --- /dev/null +++ b/lib/logic/models/console_log.dart @@ -0,0 +1,184 @@ +import 'dart:convert'; + +import 'package:gql/language.dart' as gql; +import 'package:graphql/client.dart' as gql_client; +import 'package:intl/intl.dart'; + +enum ConsoleLogSeverity { + normal, + warning, +} + +/// Base entity for console logs. +sealed class ConsoleLog { + ConsoleLog({ + final String? customTitle, + this.severity = ConsoleLogSeverity.normal, + }) : title = customTitle ?? + (severity == ConsoleLogSeverity.warning ? 'Error' : 'Log'), + time = DateTime.now(); + + final DateTime time; + final ConsoleLogSeverity severity; + bool get isError => severity == ConsoleLogSeverity.warning; + + /// title for both in listing and in dialog + final String title; + + /// formatted data to be shown in listing + String get content; + + /// data available for copy in dialog + String? get shareableData => '{"title": "$title",\n' + '"timestamp": "$fullUTCString",\n' + '"data":{\n$content\n}' + '\n}'; + + static final DateFormat _formatter = DateFormat('hh:mm:ss'); + String get timeString => _formatter.format(time); + + String get fullUTCString => time.toUtc().toIso8601String(); +} + +abstract class LogWithRawResponse { + String get rawResponse; +} + +/// entity for manually created logs, as opposed to automated ones coming +/// from requests / responses +class ManualConsoleLog extends ConsoleLog { + ManualConsoleLog({ + required this.content, + super.customTitle, + super.severity, + }); + + ManualConsoleLog.warning({ + required this.content, + super.customTitle, + }) : super(severity: ConsoleLogSeverity.warning); + + @override + String content; +} + +class RestApiRequestConsoleLog extends ConsoleLog { + RestApiRequestConsoleLog({ + this.method, + this.uri, + this.headers, + this.data, + super.severity, + }); + + /// headers thath should not be included into clipboard buffer, as opposed to + /// `[[ConsoleLogItemDialog]]` `_KeyValueRow.hideList` which filters values, + /// that should be accessible from UI, but hidden in screenshots + static const blacklistedHeaders = ['Authorization']; + + final String? method; + final Uri? uri; + final Map? headers; + final String? data; + + @override + String get title => 'Rest API Request'; + + Map get filteredHeaders => Map.fromEntries( + headers?.entries.where( + (final entry) => !blacklistedHeaders.contains(entry.key), + ) ?? + const [], + ); + + @override + String get content => '"method": "$method",\n' + '"uri": "$uri",\n' + '"headers": ${jsonEncode(filteredHeaders)},\n' // censor header to not expose API keys + '"data": $data'; +} + +class RestApiResponseConsoleLog extends ConsoleLog { + RestApiResponseConsoleLog({ + this.method, + this.uri, + this.statusCode, + this.data, + super.severity, + }); + + final String? method; + final Uri? uri; + final int? statusCode; + final String? data; + + @override + String get title => 'Rest API Response'; + @override + String get content => '"method": "$method",\n' + '"status_code": $statusCode,\n' + '"uri": "$uri",\n' + '"data": $data'; +} + +/// there is no actual getter for context fields outside of its class +/// one can extract unique entries by their type, which implements +/// `ContextEntry` class, I'll leave the code here if in the future +/// some entries will actually be needed. +// extension ContextEncoder on gql_client.Context { +// String get encode { +// return '""'; +// } +// } + +class GraphQlRequestConsoleLog extends ConsoleLog { + GraphQlRequestConsoleLog({ + required this.operationType, + required this.operation, + required this.variables, + // this.context, + super.severity, + }); + + // final gql_client.Context? context; + final String operationType; + final gql_client.Operation? operation; + String get operationDocument => + operation != null ? gql.printNode(operation!.document) : 'null'; + final Map? variables; + + @override + String get title => 'GraphQL Request'; + @override + String get content => + // '"context": ${context?.encode},\n' + '"variables": ${jsonEncode(variables)},\n' + '"type": "$operationType",\n' + '"name": "${operation?.operationName}",\n' + '"document": ${jsonEncode(operationDocument)}'; +} + +class GraphQlResponseConsoleLog extends ConsoleLog + implements LogWithRawResponse { + GraphQlResponseConsoleLog({ + required this.rawResponse, + // this.context, + this.data, + this.errors, + super.severity, + }); + + @override + final String rawResponse; + // final gql_client.Context? context; + final Map? data; + final List? errors; + + @override + String get title => 'GraphQL Response'; + @override + String get content => + // '"context": ${context?.encode},\n' + '"data": ${jsonEncode(data)},\n' + '"errors": $errors'; +} diff --git a/lib/logic/models/json/digital_ocean_server_info.dart b/lib/logic/models/json/digital_ocean_server_info.dart index dbe41f66..5e92de29 100644 --- a/lib/logic/models/json/digital_ocean_server_info.dart +++ b/lib/logic/models/json/digital_ocean_server_info.dart @@ -77,46 +77,21 @@ class DigitalOceanLocation { return emoji; } + static const _townPrefixToCountryMap = { + 'fra': 'germany', + 'ams': 'netherlands', + 'sgp': 'singapore', + 'lon': 'united_kingdom', + 'tor': 'canada', + 'blr': 'india', + 'syd': 'australia', + 'nyc': 'united_states', + 'sfo': 'united_states', + }; + String get countryDisplayKey { - String displayKey = 'countries.'; - switch (slug.substring(0, 3)) { - case 'fra': - displayKey += 'germany'; - break; - - case 'ams': - displayKey += 'netherlands'; - break; - - case 'sgp': - displayKey += 'singapore'; - break; - - case 'lon': - displayKey += 'united_kingdom'; - break; - - case 'tor': - displayKey += 'canada'; - break; - - case 'blr': - displayKey += 'india'; - break; - - case 'syd': - displayKey += 'australia'; - break; - - case 'nyc': - case 'sfo': - displayKey += 'united_states'; - break; - - default: - displayKey = slug; - } - return displayKey; + final countryName = _townPrefixToCountryMap[slug.substring(0, 3)] ?? slug; + return 'countries.$countryName'; } } diff --git a/lib/logic/models/json/digital_ocean_server_info.g.dart b/lib/logic/models/json/digital_ocean_server_info.g.dart index 9610dbce..ea3d73d9 100644 --- a/lib/logic/models/json/digital_ocean_server_info.g.dart +++ b/lib/logic/models/json/digital_ocean_server_info.g.dart @@ -10,8 +10,10 @@ DigitalOceanVolume _$DigitalOceanVolumeFromJson(Map json) => DigitalOceanVolume( json['id'] as String, json['name'] as String, - json['size_gigabytes'] as int, - (json['droplet_ids'] as List?)?.map((e) => e as int).toList(), + (json['size_gigabytes'] as num).toInt(), + (json['droplet_ids'] as List?) + ?.map((e) => (e as num).toInt()) + .toList(), ); Map _$DigitalOceanVolumeToJson(DigitalOceanVolume instance) => @@ -42,10 +44,10 @@ DigitalOceanServerType _$DigitalOceanServerTypeFromJson( (json['regions'] as List).map((e) => e as String).toList(), (json['memory'] as num).toDouble(), json['description'] as String, - json['disk'] as int, + (json['disk'] as num).toInt(), (json['price_monthly'] as num).toDouble(), json['slug'] as String, - json['vcpus'] as int, + (json['vcpus'] as num).toInt(), ); Map _$DigitalOceanServerTypeToJson( diff --git a/lib/logic/models/json/dns_providers/cloudflare_dns_info.g.dart b/lib/logic/models/json/dns_providers/cloudflare_dns_info.g.dart index d02fc2d2..03b871dc 100644 --- a/lib/logic/models/json/dns_providers/cloudflare_dns_info.g.dart +++ b/lib/logic/models/json/dns_providers/cloudflare_dns_info.g.dart @@ -24,8 +24,8 @@ CloudflareDnsRecord _$CloudflareDnsRecordFromJson(Map json) => name: json['name'] as String?, content: json['content'] as String?, zoneName: json['zone_name'] as String, - ttl: json['ttl'] as int? ?? 3600, - priority: json['priority'] as int? ?? 10, + ttl: (json['ttl'] as num?)?.toInt() ?? 3600, + priority: (json['priority'] as num?)?.toInt() ?? 10, id: json['id'] as String?, ); diff --git a/lib/logic/models/json/dns_providers/desec_dns_info.g.dart b/lib/logic/models/json/dns_providers/desec_dns_info.g.dart index bfb9126e..5c2415ee 100644 --- a/lib/logic/models/json/dns_providers/desec_dns_info.g.dart +++ b/lib/logic/models/json/dns_providers/desec_dns_info.g.dart @@ -8,7 +8,7 @@ part of 'desec_dns_info.dart'; DesecDomain _$DesecDomainFromJson(Map json) => DesecDomain( name: json['name'] as String, - minimumTtl: json['minimum_ttl'] as int?, + minimumTtl: (json['minimum_ttl'] as num?)?.toInt(), ); Map _$DesecDomainToJson(DesecDomain instance) => @@ -21,7 +21,7 @@ DesecDnsRecord _$DesecDnsRecordFromJson(Map json) => DesecDnsRecord( subname: json['subname'] as String, type: json['type'] as String, - ttl: json['ttl'] as int, + ttl: (json['ttl'] as num).toInt(), records: (json['records'] as List).map((e) => e as String).toList(), ); diff --git a/lib/logic/models/json/dns_providers/digital_ocean_dns_info.g.dart b/lib/logic/models/json/dns_providers/digital_ocean_dns_info.g.dart index d66c0352..df419647 100644 --- a/lib/logic/models/json/dns_providers/digital_ocean_dns_info.g.dart +++ b/lib/logic/models/json/dns_providers/digital_ocean_dns_info.g.dart @@ -9,7 +9,7 @@ part of 'digital_ocean_dns_info.dart'; DigitalOceanDomain _$DigitalOceanDomainFromJson(Map json) => DigitalOceanDomain( name: json['name'] as String, - ttl: json['ttl'] as int?, + ttl: (json['ttl'] as num?)?.toInt(), ); Map _$DigitalOceanDomainToJson(DigitalOceanDomain instance) => @@ -21,12 +21,12 @@ Map _$DigitalOceanDomainToJson(DigitalOceanDomain instance) => DigitalOceanDnsRecord _$DigitalOceanDnsRecordFromJson( Map json) => DigitalOceanDnsRecord( - id: json['id'] as int?, + id: (json['id'] as num?)?.toInt(), name: json['name'] as String, type: json['type'] as String, - ttl: json['ttl'] as int, + ttl: (json['ttl'] as num).toInt(), data: json['data'] as String, - priority: json['priority'] as int?, + priority: (json['priority'] as num?)?.toInt(), ); Map _$DigitalOceanDnsRecordToJson( diff --git a/lib/logic/models/json/hetzner_server_info.g.dart b/lib/logic/models/json/hetzner_server_info.g.dart index 27b94deb..fb877295 100644 --- a/lib/logic/models/json/hetzner_server_info.g.dart +++ b/lib/logic/models/json/hetzner_server_info.g.dart @@ -8,7 +8,7 @@ part of 'hetzner_server_info.dart'; HetznerServerInfo _$HetznerServerInfoFromJson(Map json) => HetznerServerInfo( - json['id'] as int, + (json['id'] as num).toInt(), json['name'] as String, $enumDecode(_$ServerStatusEnumMap, json['status']), DateTime.parse(json['created'] as String), @@ -16,7 +16,9 @@ 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(), + (json['volumes'] as List) + .map((e) => (e as num).toInt()) + .toList(), ); Map _$HetznerServerInfoToJson(HetznerServerInfo instance) => @@ -58,7 +60,7 @@ Map _$HetznerPublicNetInfoToJson( }; HetznerIp4 _$HetznerIp4FromJson(Map json) => HetznerIp4( - json['id'] as int, + (json['id'] as num).toInt(), json['ip'] as String, json['blocked'] as bool, json['dns_ptr'] as String, @@ -75,9 +77,9 @@ Map _$HetznerIp4ToJson(HetznerIp4 instance) => HetznerServerTypeInfo _$HetznerServerTypeInfoFromJson( Map json) => HetznerServerTypeInfo( - json['cores'] as int, + (json['cores'] as num).toInt(), json['memory'] as num, - json['disk'] as int, + (json['disk'] as num).toInt(), (json['prices'] as List) .map((e) => HetznerPriceInfo.fromJson(e as Map)) .toList(), @@ -132,9 +134,9 @@ Map _$HetznerLocationToJson(HetznerLocation instance) => HetznerVolume _$HetznerVolumeFromJson(Map json) => HetznerVolume( - json['id'] as int, - json['size'] as int, - json['serverId'] as int?, + (json['id'] as num).toInt(), + (json['size'] as num).toInt(), + (json['serverId'] as num?)?.toInt(), json['name'] as String, json['linux_device'] as String?, ); diff --git a/lib/logic/models/json/recovery_token_status.g.dart b/lib/logic/models/json/recovery_token_status.g.dart index 9fb6d6cd..eda00a4a 100644 --- a/lib/logic/models/json/recovery_token_status.g.dart +++ b/lib/logic/models/json/recovery_token_status.g.dart @@ -15,7 +15,7 @@ RecoveryKeyStatus _$RecoveryKeyStatusFromJson(Map json) => expiration: json['expiration'] == null ? null : DateTime.parse(json['expiration'] as String), - usesLeft: json['uses_left'] as int?, + usesLeft: (json['uses_left'] as num?)?.toInt(), ); Map _$RecoveryKeyStatusToJson(RecoveryKeyStatus instance) => diff --git a/lib/logic/models/json/server_job.g.dart b/lib/logic/models/json/server_job.g.dart index 712c086f..b2136abb 100644 --- a/lib/logic/models/json/server_job.g.dart +++ b/lib/logic/models/json/server_job.g.dart @@ -15,7 +15,7 @@ ServerJob _$ServerJobFromJson(Map json) => ServerJob( updatedAt: DateTime.parse(json['updatedAt'] as String), createdAt: DateTime.parse(json['createdAt'] as String), error: json['error'] as String?, - progress: json['progress'] as int?, + progress: (json['progress'] as num?)?.toInt(), result: json['result'] as String?, statusText: json['statusText'] as String?, finishedAt: json['finishedAt'] == null diff --git a/lib/logic/models/message.dart b/lib/logic/models/message.dart deleted file mode 100644 index b722d464..00000000 --- a/lib/logic/models/message.dart +++ /dev/null @@ -1,75 +0,0 @@ -import 'package:graphql/client.dart'; -import 'package:intl/intl.dart'; - -/// TODO(misterfourtytwo): add equality override -class Message { - Message({this.text, this.severity = MessageSeverity.normal}) - : time = DateTime.now(); - Message.warn({this.text}) - : severity = MessageSeverity.warning, - time = DateTime.now(); - - final String? text; - final DateTime time; - final MessageSeverity severity; - - static final DateFormat _formatter = DateFormat('hh:mm'); - String get timeString => _formatter.format(time); -} - -enum MessageSeverity { - normal, - warning, -} - -class RestApiRequestMessage extends Message { - RestApiRequestMessage({ - this.method, - this.uri, - this.data, - this.headers, - }) : super(text: 'request-uri: $uri\nheaders: $headers\ndata: $data'); - - final String? method; - final Uri? uri; - final String? data; - final Map? headers; -} - -class RestApiResponseMessage extends Message { - RestApiResponseMessage({ - this.method, - this.uri, - this.statusCode, - this.data, - }) : super(text: 'response-uri: $uri\ncode: $statusCode\ndata: $data'); - - final String? method; - final Uri? uri; - final int? statusCode; - final String? data; -} - -class GraphQlResponseMessage extends Message { - GraphQlResponseMessage({ - this.data, - this.errors, - this.context, - }) : super(text: 'GraphQL Response\ndata: $data'); - - final Map? data; - final List? errors; - final Context? context; -} - -class GraphQlRequestMessage extends Message { - GraphQlRequestMessage({ - this.operation, - this.variables, - this.context, - }) : super(text: 'GraphQL Request\noperation: $operation'); - - final Operation? operation; - final Map? variables; - final Context? context; -} diff --git a/lib/logic/providers/dns_providers/cloudflare.dart b/lib/logic/providers/dns_providers/cloudflare.dart index e40292a9..eb99db1d 100644 --- a/lib/logic/providers/dns_providers/cloudflare.dart +++ b/lib/logic/providers/dns_providers/cloudflare.dart @@ -27,9 +27,9 @@ class ApiAdapter { class CloudflareDnsProvider extends DnsProvider { CloudflareDnsProvider() : _adapter = ApiAdapter(); CloudflareDnsProvider.load( - final bool isAuthotized, + final bool isAuthorized, ) : _adapter = ApiAdapter( - isWithToken: isAuthotized, + isWithToken: isAuthorized, ); ApiAdapter _adapter; diff --git a/lib/logic/providers/dns_providers/desec.dart b/lib/logic/providers/dns_providers/desec.dart index 188045bf..eaab3aaf 100644 --- a/lib/logic/providers/dns_providers/desec.dart +++ b/lib/logic/providers/dns_providers/desec.dart @@ -22,9 +22,9 @@ class ApiAdapter { class DesecDnsProvider extends DnsProvider { DesecDnsProvider() : _adapter = ApiAdapter(); DesecDnsProvider.load( - final bool isAuthotized, + final bool isAuthorized, ) : _adapter = ApiAdapter( - isWithToken: isAuthotized, + isWithToken: isAuthorized, ); ApiAdapter _adapter; diff --git a/lib/logic/providers/dns_providers/digital_ocean_dns.dart b/lib/logic/providers/dns_providers/digital_ocean_dns.dart index f111c5f3..093123b8 100644 --- a/lib/logic/providers/dns_providers/digital_ocean_dns.dart +++ b/lib/logic/providers/dns_providers/digital_ocean_dns.dart @@ -22,9 +22,9 @@ class ApiAdapter { class DigitalOceanDnsProvider extends DnsProvider { DigitalOceanDnsProvider() : _adapter = ApiAdapter(); DigitalOceanDnsProvider.load( - final bool isAuthotized, + final bool isAuthorized, ) : _adapter = ApiAdapter( - isWithToken: isAuthotized, + isWithToken: isAuthorized, ); ApiAdapter _adapter; diff --git a/lib/logic/providers/server_providers/digital_ocean.dart b/lib/logic/providers/server_providers/digital_ocean.dart index bd61c4e1..bc372ebc 100644 --- a/lib/logic/providers/server_providers/digital_ocean.dart +++ b/lib/logic/providers/server_providers/digital_ocean.dart @@ -38,9 +38,9 @@ class DigitalOceanServerProvider extends ServerProvider { DigitalOceanServerProvider() : _adapter = ApiAdapter(); DigitalOceanServerProvider.load( final String? location, - final bool isAuthotized, + final bool isAuthorized, ) : _adapter = ApiAdapter( - isWithToken: isAuthotized, + isWithToken: isAuthorized, region: location, ); diff --git a/lib/logic/providers/server_providers/hetzner.dart b/lib/logic/providers/server_providers/hetzner.dart index 36d0ed6d..67037610 100644 --- a/lib/logic/providers/server_providers/hetzner.dart +++ b/lib/logic/providers/server_providers/hetzner.dart @@ -38,9 +38,9 @@ class HetznerServerProvider extends ServerProvider { HetznerServerProvider() : _adapter = ApiAdapter(); HetznerServerProvider.load( final String? location, - final bool isAuthotized, + final bool isAuthorized, ) : _adapter = ApiAdapter( - isWithToken: isAuthotized, + isWithToken: isAuthorized, region: location, ); diff --git a/lib/main.dart b/lib/main.dart index af680a1a..5c2580fa 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,28 +2,20 @@ 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/app_controller/inherited_app_controller.dart'; import 'package:selfprivacy/config/bloc_config.dart'; import 'package:selfprivacy/config/bloc_observer.dart'; import 'package:selfprivacy/config/brand_colors.dart'; import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/config/hive_config.dart'; import 'package:selfprivacy/config/localization.dart'; -import 'package:selfprivacy/logic/cubit/app_settings/app_settings_cubit.dart'; -import 'package:selfprivacy/theming/factory/app_theme_factory.dart'; +import 'package:selfprivacy/config/preferences_repository/datasources/preferences_hive_datasource.dart'; +import 'package:selfprivacy/config/preferences_repository/inherited_preferences_repository.dart'; import 'package:selfprivacy/ui/pages/errors/failed_to_init_secure_storage.dart'; import 'package:selfprivacy/ui/router/router.dart'; -// import 'package:wakelock/wakelock.dart'; import 'package:timezone/data/latest.dart' as tz; void main() async { - WidgetsFlutterBinding.ensureInitialized(); - try { - await HiveConfig.init(); - } on PlatformException catch (e) { - runApp( - FailedToInitSecureStorageScreen(e: e), - ); - } // await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); // try { @@ -34,85 +26,117 @@ void main() async { // print(e); // } - await getItSetup(); - await EasyLocalization.ensureInitialized(); - tz.initializeTimeZones(); + try { + await Future.wait( + >[ + HiveConfig.init(), + EasyLocalization.ensureInitialized(), + ], + ); + await getItSetup(); + } on PlatformException catch (e) { + runApp( + FailedToInitSecureStorageScreen(e: e), + ); + } - final ThemeData lightThemeData = await AppThemeFactory.create( - isDark: false, - fallbackColor: BrandColors.primary, - ); - final ThemeData darkThemeData = await AppThemeFactory.create( - isDark: true, - fallbackColor: BrandColors.primary, - ); + tz.initializeTimeZones(); Bloc.observer = SimpleBlocObserver(); runApp( Localization( - child: SelfprivacyApp( - lightThemeData: lightThemeData, - darkThemeData: darkThemeData, + child: InheritedPreferencesRepository( + dataSource: PreferencesHiveDataSource(), + child: const InheritedAppController( + child: AppBuilder(), + ), ), ), ); } -class SelfprivacyApp extends StatelessWidget { - SelfprivacyApp({ - required this.lightThemeData, - required this.darkThemeData, - super.key, - }); - - final ThemeData lightThemeData; - final ThemeData darkThemeData; - - final _appRouter = RootRouter(getIt.get().navigatorKey); +class AppBuilder extends StatelessWidget { + const AppBuilder({super.key}); @override - Widget build(final BuildContext context) => Localization( - child: BlocAndProviderConfig( - child: BlocBuilder( - builder: ( - final BuildContext context, - final AppSettingsState appSettings, - ) { - getIt.get().setLocaleCode( - context.locale.languageCode, - ); - return MaterialApp.router( - routeInformationParser: _appRouter.defaultRouteParser(), - routerDelegate: _appRouter.delegate(), - scaffoldMessengerKey: - getIt.get().scaffoldMessengerKey, - localizationsDelegates: context.localizationDelegates, - supportedLocales: context.supportedLocales, - locale: context.locale, - debugShowCheckedModeBanner: false, - title: 'SelfPrivacy', - theme: lightThemeData, - darkTheme: darkThemeData, - themeMode: appSettings.isAutoDarkModeOn - ? ThemeMode.system - : appSettings.isDarkModeOn - ? ThemeMode.dark - : ThemeMode.light, - builder: (final BuildContext context, final Widget? widget) { - Widget error = - const Center(child: Text('...rendering error...')); - if (widget is Scaffold || widget is Navigator) { - error = Scaffold(body: error); - } - ErrorWidget.builder = - (final FlutterErrorDetails errorDetails) => error; + Widget build(final BuildContext context) { + final appController = InheritedAppController.of(context); - return widget ?? error; - }, - ); - }, + if (appController.loaded) { + return const SelfprivacyApp(); + } + + return const SplashScreen(); + } +} + +/// Widget to be shown +/// until essential app initialization is completed +class SplashScreen extends StatelessWidget { + const SplashScreen({super.key}); + + @override + Widget build(final BuildContext context) => const ColoredBox( + color: Colors.white, + child: Center( + child: CircularProgressIndicator.adaptive( + valueColor: AlwaysStoppedAnimation(BrandColors.primary), ), ), ); } + +class SelfprivacyApp extends StatefulWidget { + const SelfprivacyApp({ + super.key, + }); + + @override + State createState() => _SelfprivacyAppState(); +} + +class _SelfprivacyAppState extends State { + final appKey = UniqueKey(); + final _appRouter = RootRouter(getIt.get().navigatorKey); + + @override + Widget build(final BuildContext context) { + final appController = InheritedAppController.of(context); + + return BlocAndProviderConfig( + child: MaterialApp.router( + key: appKey, + title: 'SelfPrivacy', + // routing + routeInformationParser: _appRouter.defaultRouteParser(), + routerDelegate: _appRouter.delegate(), + scaffoldMessengerKey: + getIt.get().scaffoldMessengerKey, + // localization settings + locale: context.locale, + supportedLocales: context.supportedLocales, + localizationsDelegates: context.localizationDelegates, + // theme settings + themeMode: appController.themeMode, + theme: appController.lightTheme, + darkTheme: appController.darkTheme, + // other preferences + debugShowCheckedModeBanner: false, + scrollBehavior: + const MaterialScrollBehavior().copyWith(scrollbars: false), + builder: _builder, + ), + ); + } + + Widget _builder(final BuildContext context, final Widget? widget) { + Widget error = const Center(child: Text('...rendering error...')); + if (widget is Scaffold || widget is Navigator) { + error = Scaffold(body: error); + } + ErrorWidget.builder = (final FlutterErrorDetails errorDetails) => error; + + return widget ?? error; + } +} diff --git a/lib/theming/factory/app_theme_factory.dart b/lib/theming/factory/app_theme_factory.dart index 4bb3f31b..096dd35a 100644 --- a/lib/theming/factory/app_theme_factory.dart +++ b/lib/theming/factory/app_theme_factory.dart @@ -42,6 +42,11 @@ abstract class AppThemeFactory { typography: appTypography, useMaterial3: true, scaffoldBackgroundColor: colorScheme.background, + listTileTheme: ListTileThemeData( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), ); return materialThemeData; @@ -50,7 +55,8 @@ abstract class AppThemeFactory { static Future _getDynamicColors(final Brightness brightness) { try { return DynamicColorPlugin.getCorePalette().then( - (final corePallet) => corePallet?.toColorScheme(brightness: brightness), + (final corePallete) => + corePallete?.toColorScheme(brightness: brightness), ); } on PlatformException { return Future.value(null); diff --git a/lib/ui/components/brand_header/brand_header.dart b/lib/ui/components/brand_header/brand_header.dart index 56be04df..f2ba145f 100644 --- a/lib/ui/components/brand_header/brand_header.dart +++ b/lib/ui/components/brand_header/brand_header.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -class BrandHeader extends StatelessWidget { +class BrandHeader extends StatelessWidget implements PreferredSizeWidget { const BrandHeader({ super.key, this.title = '', @@ -8,6 +8,9 @@ class BrandHeader extends StatelessWidget { this.onBackButtonPressed, }); + @override + Size get preferredSize => const Size.fromHeight(52.0); + final String title; final bool hasBackButton; final VoidCallback? onBackButtonPressed; diff --git a/lib/ui/components/brand_md/brand_md.dart b/lib/ui/components/brand_md/brand_md.dart index 55c58fec..61a8e4c5 100644 --- a/lib/ui/components/brand_md/brand_md.dart +++ b/lib/ui/components/brand_md/brand_md.dart @@ -39,6 +39,7 @@ class _BrandMarkdownState extends State { return MarkdownBody( shrinkWrap: true, styleSheet: markdown, + selectable: true, onTapLink: (final String text, final String? href, final String title) { if (href != null) { canLaunchUrlString(href).then((final bool canLaunchURL) { diff --git a/lib/ui/components/info_box/info_box.dart b/lib/ui/components/info_box/info_box.dart index c2e67def..6b77de9d 100644 --- a/lib/ui/components/info_box/info_box.dart +++ b/lib/ui/components/info_box/info_box.dart @@ -11,15 +11,16 @@ class InfoBox extends StatelessWidget { final bool isWarning; @override - Widget build(final BuildContext context) => Column( - crossAxisAlignment: CrossAxisAlignment.start, + Widget build(final BuildContext context) => Wrap( + spacing: 8.0, + runSpacing: 16.0, + crossAxisAlignment: WrapCrossAlignment.center, children: [ Icon( isWarning ? Icons.warning_amber_outlined : Icons.info_outline, size: 24, color: Theme.of(context).colorScheme.onBackground, ), - const SizedBox(height: 16), Text( text, style: Theme.of(context).textTheme.bodyMedium!.copyWith( diff --git a/lib/ui/components/jobs_content/jobs_content.dart b/lib/ui/components/jobs_content/jobs_content.dart index d982ba72..d24e1c27 100644 --- a/lib/ui/components/jobs_content/jobs_content.dart +++ b/lib/ui/components/jobs_content/jobs_content.dart @@ -283,7 +283,7 @@ class JobsContent extends StatelessWidget { (final job) => job.uid == state.rebuildJobUid, ); if (rebuildJob == null) { - return const Gap(0); + return const SizedBox.shrink(); } return Row( children: [ diff --git a/lib/ui/components/list_tiles/log_list_tile.dart b/lib/ui/components/list_tiles/log_list_tile.dart deleted file mode 100644 index e83765e9..00000000 --- a/lib/ui/components/list_tiles/log_list_tile.dart +++ /dev/null @@ -1,304 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:selfprivacy/logic/models/message.dart'; -import 'package:selfprivacy/utils/platform_adapter.dart'; - -class LogListItem extends StatelessWidget { - const LogListItem({ - required this.message, - super.key, - }); - - final Message message; - - @override - Widget build(final BuildContext context) { - final messageItem = message; - if (messageItem is RestApiRequestMessage) { - return _RestApiRequestMessageItem(message: messageItem); - } else if (messageItem is RestApiResponseMessage) { - return _RestApiResponseMessageItem(message: messageItem); - } else if (messageItem is GraphQlResponseMessage) { - return _GraphQlResponseMessageItem(message: messageItem); - } else if (messageItem is GraphQlRequestMessage) { - return _GraphQlRequestMessageItem(message: messageItem); - } else { - return _DefaultMessageItem(message: messageItem); - } - } -} - -class _RestApiRequestMessageItem extends StatelessWidget { - const _RestApiRequestMessageItem({required this.message}); - - final RestApiRequestMessage message; - - @override - Widget build(final BuildContext context) => ListTile( - title: Text( - '${message.method}\n${message.uri}', - ), - subtitle: Text(message.timeString), - leading: const Icon(Icons.upload_outlined), - iconColor: Theme.of(context).colorScheme.secondary, - onTap: () => showDialog( - context: context, - builder: (final BuildContext context) => AlertDialog( - scrollable: true, - title: Text( - '${message.method}\n${message.uri}', - ), - content: Column( - children: [ - Text(message.timeString), - const SizedBox(height: 16), - // Headers is a map of key-value pairs - if (message.headers != null) const Text('Headers'), - if (message.headers != null) - Text( - message.headers!.entries - .map((final entry) => '${entry.key}: ${entry.value}') - .join('\n'), - ), - if (message.data != null && message.data != 'null') - const Text('Data'), - if (message.data != null && message.data != 'null') - Text(message.data!), - ], - ), - actions: [ - // A button to copy the request to the clipboard - if (message.text != null) - TextButton( - onPressed: () { - PlatformAdapter.setClipboard(message.text ?? ''); - }, - child: Text('console_page.copy'.tr()), - ), - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: Text('basis.close'.tr()), - ), - ], - ), - ), - ); -} - -class _RestApiResponseMessageItem extends StatelessWidget { - const _RestApiResponseMessageItem({required this.message}); - - final RestApiResponseMessage message; - - @override - Widget build(final BuildContext context) => ListTile( - title: Text( - '${message.statusCode} ${message.method}\n${message.uri}', - ), - subtitle: Text(message.timeString), - leading: const Icon(Icons.download_outlined), - iconColor: Theme.of(context).colorScheme.primary, - onTap: () => showDialog( - context: context, - builder: (final BuildContext context) => AlertDialog( - scrollable: true, - title: Text( - '${message.statusCode} ${message.method}\n${message.uri}', - ), - content: Column( - children: [ - Text(message.timeString), - const SizedBox(height: 16), - // Headers is a map of key-value pairs - if (message.data != null && message.data != 'null') - const Text('Data'), - if (message.data != null && message.data != 'null') - Text(message.data!), - ], - ), - actions: [ - // A button to copy the request to the clipboard - if (message.text != null) - TextButton( - onPressed: () { - PlatformAdapter.setClipboard(message.text ?? ''); - }, - child: Text('console_page.copy'.tr()), - ), - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: Text('basis.close'.tr()), - ), - ], - ), - ), - ); -} - -class _GraphQlResponseMessageItem extends StatelessWidget { - const _GraphQlResponseMessageItem({required this.message}); - - final GraphQlResponseMessage message; - - @override - Widget build(final BuildContext context) => ListTile( - title: Text( - 'GraphQL Response at ${message.timeString}', - ), - subtitle: Text( - message.data.toString(), - overflow: TextOverflow.ellipsis, - maxLines: 1, - ), - leading: const Icon(Icons.arrow_circle_down_outlined), - iconColor: Theme.of(context).colorScheme.tertiary, - onTap: () => showDialog( - context: context, - builder: (final BuildContext context) => AlertDialog( - scrollable: true, - title: Text( - 'GraphQL Response at ${message.timeString}', - ), - content: Column( - children: [ - Text(message.timeString), - const Divider(), - if (message.data != null) const Text('Data'), - // Data is a map of key-value pairs - if (message.data != null) - Text( - message.data!.entries - .map((final entry) => '${entry.key}: ${entry.value}') - .join('\n'), - ), - const Divider(), - if (message.errors != null) const Text('Errors'), - if (message.errors != null) - Text( - message.errors! - .map( - (final entry) => - '${entry.message} at ${entry.locations}', - ) - .join('\n'), - ), - const Divider(), - if (message.context != null) const Text('Context'), - if (message.context != null) - Text( - message.context!.toString(), - ), - ], - ), - actions: [ - // A button to copy the request to the clipboard - if (message.text != null) - TextButton( - onPressed: () { - PlatformAdapter.setClipboard(message.text ?? ''); - }, - child: Text('console_page.copy'.tr()), - ), - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: Text('basis.close'.tr()), - ), - ], - ), - ), - ); -} - -class _GraphQlRequestMessageItem extends StatelessWidget { - const _GraphQlRequestMessageItem({required this.message}); - - final GraphQlRequestMessage message; - - @override - Widget build(final BuildContext context) => ListTile( - title: Text( - 'GraphQL Request at ${message.timeString}', - ), - subtitle: Text( - message.operation.toString(), - overflow: TextOverflow.ellipsis, - maxLines: 1, - ), - leading: const Icon(Icons.arrow_circle_up_outlined), - iconColor: Theme.of(context).colorScheme.secondary, - onTap: () => showDialog( - context: context, - builder: (final BuildContext context) => AlertDialog( - scrollable: true, - title: Text( - 'GraphQL Response at ${message.timeString}', - ), - content: Column( - children: [ - Text(message.timeString), - const Divider(), - if (message.operation != null) const Text('Operation'), - // Data is a map of key-value pairs - if (message.operation != null) - Text( - message.operation!.toString(), - ), - const Divider(), - if (message.variables != null) const Text('Variables'), - if (message.variables != null) - Text( - message.variables!.entries - .map((final entry) => '${entry.key}: ${entry.value}') - .join('\n'), - ), - const Divider(), - if (message.context != null) const Text('Context'), - if (message.context != null) - Text( - message.context!.toString(), - ), - ], - ), - actions: [ - // A button to copy the request to the clipboard - if (message.text != null) - TextButton( - onPressed: () { - PlatformAdapter.setClipboard(message.text ?? ''); - }, - child: Text('console_page.copy'.tr()), - ), - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: Text('basis.close'.tr()), - ), - ], - ), - ), - ); -} - -class _DefaultMessageItem extends StatelessWidget { - const _DefaultMessageItem({required this.message}); - - final Message message; - - @override - Widget build(final BuildContext context) => Padding( - padding: const EdgeInsets.symmetric(vertical: 4), - child: RichText( - text: TextSpan( - style: DefaultTextStyle.of(context).style, - children: [ - TextSpan( - text: '${message.timeString}: \n', - style: const TextStyle( - fontWeight: FontWeight.bold, - ), - ), - TextSpan(text: message.text), - ], - ), - ), - ); -} diff --git a/lib/ui/helpers/empty_page_placeholder.dart b/lib/ui/helpers/empty_page_placeholder.dart index e96f91f9..5abc8434 100644 --- a/lib/ui/helpers/empty_page_placeholder.dart +++ b/lib/ui/helpers/empty_page_placeholder.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:gap/gap.dart'; import 'package:selfprivacy/ui/components/not_ready_card/not_ready_card.dart'; class EmptyPagePlaceholder extends StatelessWidget { @@ -10,50 +11,72 @@ class EmptyPagePlaceholder extends StatelessWidget { super.key, }); + final bool showReadyCard; + final IconData iconData; final String title; final String description; - final IconData iconData; - final bool showReadyCard; @override - Widget build(final BuildContext context) => !showReadyCard - ? _expandedContent(context) - : Column( + Widget build(final BuildContext context) => showReadyCard + ? 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: _expandedContent(context), + if (showReadyCard) + const Padding( + padding: EdgeInsets.symmetric( + vertical: 15, + horizontal: 15, ), + child: NotReadyCard(), + ), + Expanded( + child: _ContentWidget( + iconData: iconData, + title: title, + description: description, ), ), ], + ) + : _ContentWidget( + iconData: iconData, + title: title, + description: description, ); +} - Widget _expandedContent(final BuildContext context) => Padding( +class _ContentWidget extends StatelessWidget { + const _ContentWidget({ + required this.iconData, + required this.title, + required this.description, + }); + + final IconData iconData; + final String title; + final String description; + + @override + Widget build(final BuildContext context) => Container( padding: const EdgeInsets.symmetric(horizontal: 20), child: Column( mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, children: [ Icon( iconData, size: 50, color: Theme.of(context).colorScheme.onBackground, ), - const SizedBox(height: 16), + const Gap(16), Text( title, style: Theme.of(context).textTheme.headlineMedium?.copyWith( color: Theme.of(context).colorScheme.onBackground, ), + textAlign: TextAlign.center, ), - const SizedBox(height: 8), + const Gap(8), Text( description, textAlign: TextAlign.center, diff --git a/lib/ui/layouts/root_scaffold_with_navigation.dart b/lib/ui/layouts/root_scaffold_with_navigation.dart deleted file mode 100644 index d5332f24..00000000 --- a/lib/ui/layouts/root_scaffold_with_navigation.dart +++ /dev/null @@ -1,295 +0,0 @@ -import 'dart:io'; - -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:selfprivacy/ui/components/drawers/support_drawer.dart'; -import 'package:selfprivacy/ui/components/pre_styled_buttons/flash_fab.dart'; -import 'package:selfprivacy/ui/router/root_destinations.dart'; -import 'package:selfprivacy/utils/breakpoints.dart'; - -class RootScaffoldWithNavigation extends StatelessWidget { - const RootScaffoldWithNavigation({ - required this.child, - required this.title, - required this.destinations, - this.showBottomBar = true, - this.showFab = true, - super.key, - }); - - final Widget child; - final String title; - final bool showBottomBar; - final List destinations; - final bool showFab; - - @override - // ignore: prefer_expression_function_bodies - Widget build(final BuildContext context) { - return Scaffold( - appBar: Breakpoints.mediumAndUp.isActive(context) - ? PreferredSize( - preferredSize: const Size.fromHeight(52), - child: _RootAppBar(title: title), - ) - : null, - endDrawer: const SupportDrawer(), - endDrawerEnableOpenDragGesture: false, - body: Row( - children: [ - if (Breakpoints.medium.isActive(context)) - _MainScreenNavigationRail( - destinations: destinations, - showFab: showFab, - ), - if (Breakpoints.large.isActive(context)) - _MainScreenNavigationDrawer( - destinations: destinations, - showFab: showFab, - ), - Expanded(child: child), - ], - ), - bottomNavigationBar: _BottomBar( - destinations: destinations, - hidden: !(Breakpoints.small.isActive(context) && showBottomBar), - key: const Key('bottomBar'), - ), - floatingActionButton: - showFab && Breakpoints.small.isActive(context) && showBottomBar - ? const BrandFab() - : null, - ); - } -} - -class _RootAppBar extends StatelessWidget { - const _RootAppBar({ - required this.title, - }); - - final String title; - - @override - Widget build(final BuildContext context) => AppBar( - title: AnimatedSwitcher( - duration: const Duration(milliseconds: 200), - transitionBuilder: - (final Widget child, final Animation animation) => - SlideTransition( - position: animation.drive( - Tween( - begin: const Offset(0.0, 0.2), - end: Offset.zero, - ), - ), - child: FadeTransition( - opacity: animation, - child: child, - ), - ), - child: SizedBox( - key: ValueKey(title), - width: double.infinity, - child: Text( - title, - ), - ), - ), - leading: context.router.pageCount > 1 - ? IconButton( - icon: const Icon(Icons.arrow_back), - onPressed: () => context.router.maybePop(), - ) - : null, - actions: const [ - SizedBox.shrink(), - ], - ); -} - -class _MainScreenNavigationRail extends StatelessWidget { - const _MainScreenNavigationRail({ - required this.destinations, - this.showFab = true, - }); - - final List destinations; - final bool showFab; - - @override - Widget build(final BuildContext context) { - int? activeIndex = destinations.indexWhere( - (final destination) => - context.router.isRouteActive(destination.route.routeName), - ); - - final prevActiveIndex = destinations.indexWhere( - (final destination) => context.router.stack - .any((final route) => route.name == destination.route.routeName), - ); - - if (activeIndex == -1) { - if (prevActiveIndex != -1) { - activeIndex = prevActiveIndex; - } else { - activeIndex = 0; - } - } - - final isExtended = Breakpoints.large.isActive(context); - - return LayoutBuilder( - builder: (final context, final constraints) => SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints(minHeight: constraints.maxHeight), - child: IntrinsicHeight( - child: NavigationRail( - backgroundColor: Colors.transparent, - labelType: isExtended - ? NavigationRailLabelType.none - : NavigationRailLabelType.all, - extended: isExtended, - leading: showFab - ? const BrandFab( - extended: false, - ) - : null, - groupAlignment: 0.0, - destinations: destinations - .map( - (final destination) => NavigationRailDestination( - icon: Icon(destination.icon), - label: Text(destination.label), - ), - ) - .toList(), - selectedIndex: activeIndex, - onDestinationSelected: (final index) { - context.router.replaceAll([destinations[index].route]); - }, - ), - ), - ), - ), - ); - } -} - -class _BottomBar extends StatelessWidget { - const _BottomBar({ - required this.destinations, - required this.hidden, - super.key, - }); - - final List destinations; - final bool hidden; - - @override - Widget build(final BuildContext context) { - final prevActiveIndex = destinations.indexWhere( - (final destination) => context.router.stack - .any((final route) => route.name == destination.route.routeName), - ); - - return AnimatedContainer( - duration: const Duration(milliseconds: 500), - height: hidden ? 0 : 80, - curve: Curves.easeInOutCubicEmphasized, - clipBehavior: Clip.antiAlias, - decoration: BoxDecoration( - color: Theme.of(context).scaffoldBackgroundColor, - ), - child: Platform.isIOS - ? CupertinoTabBar( - currentIndex: prevActiveIndex == -1 ? 0 : prevActiveIndex, - onTap: (final index) { - context.router.replaceAll([destinations[index].route]); - }, - items: destinations - .map( - (final destination) => BottomNavigationBarItem( - icon: Icon(destination.icon), - label: destination.label, - ), - ) - .toList(), - ) - : NavigationBar( - selectedIndex: prevActiveIndex == -1 ? 0 : prevActiveIndex, - labelBehavior: NavigationDestinationLabelBehavior.alwaysShow, - onDestinationSelected: (final index) { - context.router.replaceAll([destinations[index].route]); - }, - destinations: destinations - .map( - (final destination) => NavigationDestination( - icon: Icon(destination.icon), - label: destination.label, - ), - ) - .toList(), - ), - ); - } -} - -class _MainScreenNavigationDrawer extends StatelessWidget { - const _MainScreenNavigationDrawer({ - required this.destinations, - this.showFab = true, - }); - - final List destinations; - final bool showFab; - - @override - Widget build(final BuildContext context) { - int? activeIndex = destinations.indexWhere( - (final destination) => - context.router.isRouteActive(destination.route.routeName), - ); - - final prevActiveIndex = destinations.indexWhere( - (final destination) => context.router.stack - .any((final route) => route.name == destination.route.routeName), - ); - - if (activeIndex == -1) { - if (prevActiveIndex != -1) { - activeIndex = prevActiveIndex; - } else { - activeIndex = 0; - } - } - - return SizedBox( - height: MediaQuery.of(context).size.height, - width: 296, - child: NavigationDrawer( - key: const Key('PrimaryNavigationDrawer'), - backgroundColor: Theme.of(context).scaffoldBackgroundColor, - surfaceTintColor: Colors.transparent, - selectedIndex: activeIndex, - onDestinationSelected: (final index) { - context.router.replaceAll([destinations[index].route]); - }, - children: [ - const Padding( - padding: EdgeInsets.all(16.0), - child: BrandFab(extended: true), - ), - const SizedBox(height: 16), - ...destinations.map( - (final destination) => NavigationDrawerDestination( - icon: Icon(destination.icon), - label: Text(destination.label), - ), - ), - ], - ), - ); - } -} diff --git a/lib/ui/layouts/root_scaffold_with_subroute_selector/bottom_tab_bar.dart b/lib/ui/layouts/root_scaffold_with_subroute_selector/bottom_tab_bar.dart new file mode 100644 index 00000000..a6a5b43a --- /dev/null +++ b/lib/ui/layouts/root_scaffold_with_subroute_selector/bottom_tab_bar.dart @@ -0,0 +1,50 @@ +part of 'root_scaffold_with_subroute_selector.dart'; + +class _BottomTabBar extends SubrouteSelector { + const _BottomTabBar({ + required super.subroutes, + required this.hidden, + super.key, + }); + + final bool hidden; + + @override + Widget build(final BuildContext context) { + final int activeIndex = getActiveIndex(context); + + return AnimatedContainer( + duration: const Duration(milliseconds: 500), + height: hidden ? 0 : 80, + curve: Curves.easeInOutCubicEmphasized, + clipBehavior: Clip.antiAlias, + decoration: BoxDecoration( + color: Theme.of(context).scaffoldBackgroundColor, + ), + child: Platform.isIOS + ? CupertinoTabBar( + currentIndex: activeIndex, + onTap: openSubpage(context), + items: [ + for (final destination in subroutes) + BottomNavigationBarItem( + icon: Icon(destination.icon), + label: destination.label.tr(), + ), + ], + ) + : NavigationBar( + selectedIndex: activeIndex, + labelBehavior: NavigationDestinationLabelBehavior.alwaysShow, + onDestinationSelected: openSubpage(context), + destinations: [ + for (final destination in subroutes) + NavigationDestination( + icon: Icon(destination.icon), + label: destination.label.tr(), + ), + ].toList(), + ), + ); + } +} diff --git a/lib/ui/layouts/root_scaffold_with_subroute_selector/navigation_drawer.dart b/lib/ui/layouts/root_scaffold_with_subroute_selector/navigation_drawer.dart new file mode 100644 index 00000000..31a9a47d --- /dev/null +++ b/lib/ui/layouts/root_scaffold_with_subroute_selector/navigation_drawer.dart @@ -0,0 +1,35 @@ +part of 'root_scaffold_with_subroute_selector.dart'; + +class _NavigationDrawer extends SubrouteSelector { + const _NavigationDrawer({ + required super.subroutes, + this.showFab = true, + }); + + final bool showFab; + + @override + Widget build(final BuildContext context) => SizedBox( + height: MediaQuery.of(context).size.height, + width: 296, + child: NavigationDrawer( + key: const Key('PrimaryNavigationDrawer'), + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + surfaceTintColor: Colors.transparent, + selectedIndex: getActiveIndex(context), + onDestinationSelected: openSubpage(context), + children: [ + const Padding( + padding: EdgeInsets.all(16.0), + child: BrandFab(extended: true), + ), + const SizedBox(height: 16), + for (final destination in subroutes) + NavigationDrawerDestination( + icon: Icon(destination.icon), + label: Text(destination.label.tr()), + ), + ], + ), + ); +} diff --git a/lib/ui/layouts/root_scaffold_with_subroute_selector/navigation_rail.dart b/lib/ui/layouts/root_scaffold_with_subroute_selector/navigation_rail.dart new file mode 100644 index 00000000..ef511018 --- /dev/null +++ b/lib/ui/layouts/root_scaffold_with_subroute_selector/navigation_rail.dart @@ -0,0 +1,47 @@ +part of 'root_scaffold_with_subroute_selector.dart'; + +class _NavigationRail extends SubrouteSelector { + const _NavigationRail({ + required super.subroutes, + this.showFab = true, + }); + + final bool showFab; + + @override + Widget build(final BuildContext context) { + final isExtended = Breakpoints.large.isActive(context); + + return LayoutBuilder( + builder: (final context, final constraints) => SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints(minHeight: constraints.maxHeight), + child: IntrinsicHeight( + child: NavigationRail( + backgroundColor: Colors.transparent, + labelType: isExtended + ? NavigationRailLabelType.none + : NavigationRailLabelType.all, + extended: isExtended, + leading: showFab + ? const BrandFab( + extended: false, + ) + : null, + groupAlignment: 0.0, + destinations: [ + for (final destination in subroutes) + NavigationRailDestination( + icon: Icon(destination.icon), + label: Text(destination.label.tr()), + ), + ], + selectedIndex: getActiveIndex(context), + onDestinationSelected: openSubpage(context), + ), + ), + ), + ), + ); + } +} diff --git a/lib/ui/layouts/root_scaffold_with_subroute_selector/root_app_bar.dart b/lib/ui/layouts/root_scaffold_with_subroute_selector/root_app_bar.dart new file mode 100644 index 00000000..b0d89bce --- /dev/null +++ b/lib/ui/layouts/root_scaffold_with_subroute_selector/root_app_bar.dart @@ -0,0 +1,50 @@ +part of 'root_scaffold_with_subroute_selector.dart'; + +class _RootAppBar extends StatelessWidget implements PreferredSizeWidget { + const _RootAppBar({ + required this.title, + }); + + final String title; + + @override + Size get preferredSize => const Size.fromHeight(52); + + @override + Widget build(final BuildContext context) => AppBar( + title: AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + transitionBuilder: + (final Widget child, final Animation animation) => + SlideTransition( + position: animation.drive( + Tween( + begin: const Offset(0.0, 0.2), + end: Offset.zero, + ), + ), + child: FadeTransition( + opacity: animation, + child: child, + ), + ), + child: SizedBox( + key: ValueKey(title), + width: double.infinity, + child: Text( + title, + maxLines: 1, + textAlign: TextAlign.start, + overflow: TextOverflow.fade, + ), + ), + ), + leading: context.router.pageCount > 1 + ? IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () => context.router.maybePop(), + ) + : null, + actions: const [SizedBox.shrink()], + ); +} diff --git a/lib/ui/layouts/root_scaffold_with_subroute_selector/root_scaffold_with_subroute_selector.dart b/lib/ui/layouts/root_scaffold_with_subroute_selector/root_scaffold_with_subroute_selector.dart new file mode 100644 index 00000000..2c5ea6d2 --- /dev/null +++ b/lib/ui/layouts/root_scaffold_with_subroute_selector/root_scaffold_with_subroute_selector.dart @@ -0,0 +1,67 @@ +import 'dart:io'; + +import 'package:auto_route/auto_route.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:selfprivacy/ui/components/drawers/support_drawer.dart'; +import 'package:selfprivacy/ui/components/pre_styled_buttons/flash_fab.dart'; +import 'package:selfprivacy/ui/router/root_destinations.dart'; +import 'package:selfprivacy/ui/router/router.dart'; +import 'package:selfprivacy/utils/breakpoints.dart'; + +part 'bottom_tab_bar.dart'; +part 'navigation_drawer.dart'; +part 'navigation_rail.dart'; +part 'root_app_bar.dart'; +part 'subroute_selector.dart'; + +class RootScaffoldWithSubrouteSelector extends StatelessWidget { + const RootScaffoldWithSubrouteSelector({ + required this.child, + required this.destinations, + this.showBottomBar = true, + this.showFab = true, + super.key, + }); + + final Widget child; + final bool showBottomBar; + final List destinations; + final bool showFab; + + @override + Widget build(final BuildContext context) => Scaffold( + appBar: Breakpoints.mediumAndUp.isActive(context) + ? _RootAppBar( + title: getRouteTitle(context.router.current.name).tr(), + ) + : null, + endDrawer: const SupportDrawer(), + endDrawerEnableOpenDragGesture: false, + body: Row( + children: [ + if (Breakpoints.medium.isActive(context)) + _NavigationRail( + subroutes: destinations, + showFab: showFab, + ) + else if (Breakpoints.large.isActive(context)) + _NavigationDrawer( + subroutes: destinations, + showFab: showFab, + ), + Expanded(child: child), + ], + ), + bottomNavigationBar: _BottomTabBar( + key: const ValueKey('bottomBar'), + subroutes: destinations, + hidden: !(Breakpoints.small.isActive(context) && showBottomBar), + ), + floatingActionButton: + showFab && Breakpoints.small.isActive(context) && showBottomBar + ? const BrandFab() + : null, + ); +} diff --git a/lib/ui/layouts/root_scaffold_with_subroute_selector/subroute_selector.dart b/lib/ui/layouts/root_scaffold_with_subroute_selector/subroute_selector.dart new file mode 100644 index 00000000..65e424d8 --- /dev/null +++ b/lib/ui/layouts/root_scaffold_with_subroute_selector/subroute_selector.dart @@ -0,0 +1,33 @@ +part of 'root_scaffold_with_subroute_selector.dart'; + +abstract class SubrouteSelector extends StatelessWidget { + const SubrouteSelector({ + required this.subroutes, + super.key, + }); + + final List subroutes; + + int getActiveIndex(final BuildContext context) { + int activeIndex = subroutes.indexWhere( + (final destination) => + context.router.isRouteActive(destination.route.routeName), + ); + + final prevActiveIndex = subroutes.indexWhere( + (final destination) => context.router.stack.any( + (final route) => route.name == destination.route.routeName, + ), + ); + + if (activeIndex == -1) { + activeIndex = prevActiveIndex != -1 ? prevActiveIndex : 0; + } + + return activeIndex; + } + + ValueSetter openSubpage(final BuildContext context) => (final index) { + context.router.replaceAll([subroutes[index].route]); + }; +} diff --git a/lib/ui/pages/devices/new_device.dart b/lib/ui/pages/devices/new_device.dart index bbf03728..78b69f04 100644 --- a/lib/ui/pages/devices/new_device.dart +++ b/lib/ui/pages/devices/new_device.dart @@ -39,6 +39,7 @@ class NewDeviceScreen extends StatelessWidget { class _KeyDisplay extends StatelessWidget { const _KeyDisplay({required this.newDeviceKey}); + final String newDeviceKey; @override @@ -47,7 +48,7 @@ class _KeyDisplay extends StatelessWidget { children: [ const Divider(), const SizedBox(height: 16), - Text( + SelectableText( newDeviceKey, style: Theme.of(context).textTheme.bodyLarge!.copyWith( fontSize: 24, diff --git a/lib/ui/pages/more/about_application.dart b/lib/ui/pages/more/about_application.dart index 9e6cae65..b2fd41fc 100644 --- a/lib/ui/pages/more/about_application.dart +++ b/lib/ui/pages/more/about_application.dart @@ -3,7 +3,7 @@ import 'dart:io'; import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:package_info/package_info.dart'; +import 'package:package_info_plus/package_info_plus.dart'; import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/ui/components/list_tiles/section_title.dart'; import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart'; diff --git a/lib/ui/pages/more/app_settings/app_settings.dart b/lib/ui/pages/more/app_settings/app_settings.dart index 7cce6715..be3df075 100644 --- a/lib/ui/pages/more/app_settings/app_settings.dart +++ b/lib/ui/pages/more/app_settings/app_settings.dart @@ -1,10 +1,17 @@ import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:selfprivacy/logic/cubit/app_settings/app_settings_cubit.dart'; +import 'package:gap/gap.dart'; +import 'package:selfprivacy/config/app_controller/inherited_app_controller.dart'; +import 'package:selfprivacy/config/localization.dart'; import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; import 'package:selfprivacy/ui/components/buttons/dialog_action_button.dart'; import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart'; +import 'package:selfprivacy/ui/router/router.dart'; + +part 'language_picker.dart'; +part 'reset_app_button.dart'; +part 'theme_picker.dart'; @RoutePage() class AppSettingsPage extends StatefulWidget { @@ -16,82 +23,35 @@ class AppSettingsPage extends StatefulWidget { class _AppSettingsPageState extends State { @override - Widget build(final BuildContext context) { - final bool isDarkModeOn = - context.watch().state.isDarkModeOn; - - final bool isSystemDarkModeOn = - context.watch().state.isAutoDarkModeOn; - - return BrandHeroScreen( - hasBackButton: true, - hasFlashButton: false, - bodyPadding: const EdgeInsets.symmetric(vertical: 16), - heroTitle: 'application_settings.title'.tr(), - children: [ - SwitchListTile.adaptive( - title: Text('application_settings.system_dark_theme_title'.tr()), - subtitle: - Text('application_settings.system_dark_theme_description'.tr()), - value: isSystemDarkModeOn, - onChanged: (final value) => context - .read() - .updateAutoDarkMode(isAutoDarkModeOn: !isSystemDarkModeOn), + Widget build(final BuildContext context) => BrandHeroScreen( + hasBackButton: true, + hasFlashButton: false, + bodyPadding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 16, ), - SwitchListTile.adaptive( - title: Text('application_settings.dark_theme_title'.tr()), - subtitle: Text('application_settings.dark_theme_description'.tr()), - value: Theme.of(context).brightness == Brightness.dark, - onChanged: isSystemDarkModeOn - ? null - : (final value) => context - .read() - .updateDarkMode(isDarkModeOn: !isDarkModeOn), - ), - const Divider(height: 0), - Padding( - padding: const EdgeInsets.all(16), - child: Text( - 'application_settings.dangerous_settings'.tr(), - style: Theme.of(context).textTheme.labelLarge!.copyWith( - color: Theme.of(context).colorScheme.error, - ), + heroTitle: 'application_settings.title'.tr(), + children: [ + _ThemePicker( + key: ValueKey('theme_picker'.tr()), ), - ), - const _ResetAppTile(), - ], - ); - } -} - -class _ResetAppTile extends StatelessWidget { - const _ResetAppTile(); - - @override - Widget build(final BuildContext context) => ListTile( - title: Text('application_settings.reset_config_title'.tr()), - subtitle: Text('application_settings.reset_config_description'.tr()), - onTap: () { - showDialog( - context: context, - builder: (final _) => AlertDialog( - title: Text('modals.are_you_sure'.tr()), - content: Text('modals.purge_all_keys'.tr()), - actions: [ - DialogActionButton( - text: 'modals.purge_all_keys_confirm'.tr(), - isRed: true, - onPressed: () { - context.read().clearAppConfig(); - Navigator.of(context).pop(); - }, - ), - DialogActionButton( - text: 'basis.cancel'.tr(), - ), - ], + _LanguagePicker( + key: ValueKey('language_picker'.tr()), + ), + const Gap(8), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Text( + 'application_settings.dangerous_settings'.tr(), + style: Theme.of(context).textTheme.labelLarge!.copyWith( + color: Theme.of(context).colorScheme.error, + ), ), - ); - }, + ), + const Gap(4), + _ResetAppTile( + key: ValueKey('reset_app'.tr()), + ), + ], ); } diff --git a/lib/ui/pages/more/app_settings/developer_settings.dart b/lib/ui/pages/more/app_settings/developer_settings.dart index 751eabb6..51e2a2b3 100644 --- a/lib/ui/pages/more/app_settings/developer_settings.dart +++ b/lib/ui/pages/more/app_settings/developer_settings.dart @@ -1,11 +1,12 @@ import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:selfprivacy/config/app_controller/inherited_app_controller.dart'; import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/logic/api_maps/tls_options.dart'; import 'package:selfprivacy/logic/bloc/services/services_bloc.dart'; import 'package:selfprivacy/logic/bloc/volumes/volumes_bloc.dart'; -import 'package:selfprivacy/logic/cubit/app_settings/app_settings_cubit.dart'; import 'package:selfprivacy/ui/components/list_tiles/section_title.dart'; import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart'; import 'package:selfprivacy/ui/router/router.dart'; @@ -60,17 +61,14 @@ class _DeveloperSettingsPageState extends State { title: Text('developer_settings.reset_onboarding'.tr()), subtitle: Text('developer_settings.reset_onboarding_description'.tr()), - enabled: - !context.watch().state.isOnboardingShowing, - onTap: () => context - .read() - .turnOffOnboarding(isOnboardingShowing: true), + enabled: !InheritedAppController.of(context).shouldShowOnboarding, + onTap: () => InheritedAppController.of(context) + .setShouldShowOnboarding(true), ), ListTile( title: Text('storage.start_migration_button'.tr()), subtitle: Text('storage.data_migration_notice'.tr()), - enabled: - !context.watch().state.isOnboardingShowing, + enabled: InheritedAppController.of(context).shouldShowOnboarding, onTap: () => context.pushRoute( ServicesMigrationRoute( diskStatus: context.read().state.diskStatus, diff --git a/lib/ui/pages/more/app_settings/language_picker.dart b/lib/ui/pages/more/app_settings/language_picker.dart new file mode 100644 index 00000000..ddd8dcbf --- /dev/null +++ b/lib/ui/pages/more/app_settings/language_picker.dart @@ -0,0 +1,62 @@ +part of 'app_settings.dart'; + +class _LanguagePicker extends StatelessWidget { + const _LanguagePicker({super.key}); + + @override + Widget build(final BuildContext context) => ListTile( + title: Text( + 'application_settings.language'.tr(), + ), + subtitle: Text('application_settings.click_to_change_locale'.tr()), + trailing: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Text( + Localization.getLanguageName(context.locale), + style: Theme.of(context).textTheme.labelLarge, + ), + ), + onTap: () async { + final appController = InheritedAppController.of(context); + final Locale? newLocale = await showDialog( + context: context, + builder: (final context) => const _LanguagePickerDialog(), + routeSettings: _LanguagePickerDialog.routeSettings, + ); + + if (newLocale != null) { + await appController.setLocale(newLocale); + } + }, + ); +} + +class _LanguagePickerDialog extends StatelessWidget { + const _LanguagePickerDialog(); + static const routeSettings = RouteSettings(name: 'LanguagePickerDialog'); + + @override + Widget build(final BuildContext context) { + final appController = InheritedAppController.of(context); + + return SimpleDialog( + title: Text('application_settings.language'.tr()), + children: [ + for (final locale in appController.supportedLocales) + RadioMenuButton( + groupValue: appController.locale, + value: locale, + child: Text( + Localization.getLanguageName(locale), + style: TextStyle( + fontWeight: locale == appController.locale + ? FontWeight.w800 + : FontWeight.w400, + ), + ), + onChanged: (final newValue) => Navigator.of(context).pop(newValue), + ), + ], + ); + } +} diff --git a/lib/ui/pages/more/app_settings/reset_app_button.dart b/lib/ui/pages/more/app_settings/reset_app_button.dart new file mode 100644 index 00000000..92ec3022 --- /dev/null +++ b/lib/ui/pages/more/app_settings/reset_app_button.dart @@ -0,0 +1,42 @@ +part of 'app_settings.dart'; + +class _ResetAppTile extends StatelessWidget { + const _ResetAppTile({super.key}); + + @override + Widget build(final BuildContext context) => ListTile( + title: Text('application_settings.reset_config_title'.tr()), + subtitle: Text('application_settings.reset_config_description'.tr()), + onTap: () => showDialog( + context: context, + builder: (final context) => const _ResetAppDialog(), + ), + ); +} + +class _ResetAppDialog extends StatelessWidget { + const _ResetAppDialog(); + + @override + Widget build(final BuildContext context) => AlertDialog( + title: Text('modals.are_you_sure'.tr()), + content: Text('modals.purge_all_keys'.tr()), + actions: [ + DialogActionButton( + text: 'modals.purge_all_keys_confirm'.tr(), + isRed: true, + onPressed: () { + context.read().clearAppConfig(); + + context.router.maybePop([ + const RootRoute(), + ]); + context.resetLocale(); + }, + ), + DialogActionButton( + text: 'basis.cancel'.tr(), + ), + ], + ); +} diff --git a/lib/ui/pages/more/app_settings/theme_picker.dart b/lib/ui/pages/more/app_settings/theme_picker.dart new file mode 100644 index 00000000..d08371be --- /dev/null +++ b/lib/ui/pages/more/app_settings/theme_picker.dart @@ -0,0 +1,31 @@ +part of 'app_settings.dart'; + +class _ThemePicker extends StatelessWidget { + const _ThemePicker({super.key}); + + @override + Widget build(final BuildContext context) { + final appController = InheritedAppController.of(context); + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + SwitchListTile.adaptive( + title: Text('application_settings.system_theme_mode_title'.tr()), + subtitle: + Text('application_settings.system_theme_mode_description'.tr()), + value: appController.systemThemeModeActive, + onChanged: appController.setSystemThemeModeFlag, + ), + SwitchListTile.adaptive( + title: Text('application_settings.dark_theme_title'.tr()), + subtitle: Text('application_settings.change_application_theme'.tr()), + value: appController.darkThemeModeActive, + onChanged: appController.systemThemeModeActive + ? null + : appController.setDarkThemeModeFlag, + ), + ], + ); + } +} diff --git a/lib/ui/pages/more/console.dart b/lib/ui/pages/more/console.dart deleted file mode 100644 index 0c60cf81..00000000 --- a/lib/ui/pages/more/console.dart +++ /dev/null @@ -1,107 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:gap/gap.dart'; -import 'package:selfprivacy/config/get_it_config.dart'; -import 'package:selfprivacy/logic/models/message.dart'; -import 'package:selfprivacy/ui/components/list_tiles/log_list_tile.dart'; - -@RoutePage() -class ConsolePage extends StatefulWidget { - const ConsolePage({super.key}); - - @override - State createState() => _ConsolePageState(); -} - -class _ConsolePageState extends State { - bool paused = false; - - @override - void initState() { - super.initState(); - - getIt().addListener(update); - } - - @override - void dispose() { - getIt().removeListener(update); - - super.dispose(); - } - - void update() { - /// listener update could come at any time, like when widget is already - /// unmounted or during frame build, adding as postframe callback ensures - /// that element is marked for rebuild - WidgetsBinding.instance.addPostFrameCallback((final _) { - if (!paused && mounted) { - setState(() => {}); - } - }); - } - - void togglePause() { - paused ^= true; - setState(() {}); - } - - @override - Widget build(final BuildContext context) => SafeArea( - child: Scaffold( - appBar: AppBar( - title: Text('console_page.title'.tr()), - leading: IconButton( - icon: const Icon(Icons.arrow_back), - onPressed: () => Navigator.of(context).pop(), - ), - actions: [ - IconButton( - icon: Icon( - paused ? Icons.play_arrow_outlined : Icons.pause_outlined, - ), - onPressed: togglePause, - ), - ], - ), - 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 Gap(20), - ...messages.reversed.map( - (final message) => LogListItem( - key: ValueKey(message), - message: message, - ), - ), - ], - ); - } else { - return Column( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text('console_page.waiting'.tr()), - const Gap(16), - const CircularProgressIndicator.adaptive(), - ], - ); - } - }, - ), - ), - ); -} diff --git a/lib/ui/pages/more/console/console_log_item_dialog.dart b/lib/ui/pages/more/console/console_log_item_dialog.dart new file mode 100644 index 00000000..dc5d55cd --- /dev/null +++ b/lib/ui/pages/more/console/console_log_item_dialog.dart @@ -0,0 +1,304 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:selfprivacy/logic/models/console_log.dart'; +import 'package:selfprivacy/utils/platform_adapter.dart'; + +extension on ConsoleLog { + List unwrapContent(final BuildContext context) => switch (this) { + (final RestApiRequestConsoleLog log) => [ + if (log.method != null) _KeyValueRow('method', log.method), + if (log.uri != null) _KeyValueRow('uri', '${log.uri}'), + + // headers bloc + if (log.headers?.isNotEmpty ?? false) ...[ + const _SectionRow('console_page.headers'), + for (final entry in log.headers!.entries) + _KeyValueRow(entry.key, '${entry.value}'), + ], + + // data + const _SectionRow('console_page.data'), + _DataRow('${log.data}'), + ], + (final RestApiResponseConsoleLog log) => [ + if (log.method != null) _KeyValueRow('method', '${log.method}'), + if (log.uri != null) _KeyValueRow('uri', '${log.uri}'), + if (log.statusCode != null) + _KeyValueRow('statusCode', '${log.statusCode}'), + + // data + const _SectionRow('console_page.response_data'), + _DataRow('${log.data}'), + ], + (final GraphQlRequestConsoleLog log) => [ + // // context + // if (log.context != null) ...[ + // const _SectionRow('console_page.context'), + // _DataRow('${log.context}'), + // ], + + const _SectionRow('console_page.operation'), + if (log.operation != null) ...[ + _KeyValueRow( + 'console_page.operation_type'.tr(), + log.operationType, + ), + _KeyValueRow( + 'console_page.operation_name'.tr(), + log.operation?.operationName, + ), + const Divider(), + // data + _DataRow(log.operationDocument), + ], + // preset variables + if (log.variables?.isNotEmpty ?? false) ...[ + const _SectionRow('console_page.variables'), + for (final entry in log.variables!.entries) + _KeyValueRow(entry.key, '${entry.value}'), + ], + ], + (final GraphQlResponseConsoleLog log) => [ + // // context + // const _SectionRow('console_page.context'), + // _DataRow('${log.context}'), + // data + if (log.data != null) ...[ + const _SectionRow('console_page.data'), + for (final entry in log.data!.entries) + _KeyValueRow(entry.key, '${entry.value}'), + ], + // errors + if (log.errors?.isNotEmpty ?? false) ...[ + const _SectionRow('console_page.errors'), + for (final entry in log.errors!) ...[ + _KeyValueRow( + '${'console_page.error_message'.tr()}: ', + entry.message, + ), + _KeyValueRow( + '${'console_page.error_path'.tr()}: ', + '${entry.path}', + ), + if (entry.locations?.isNotEmpty ?? false) + _KeyValueRow( + '${'console_page.error_locations'.tr()}: ', + '${entry.locations}', + ), + if (entry.extensions?.isNotEmpty ?? false) + _KeyValueRow( + '${'console_page.error_extensions'.tr()}: ', + '${entry.extensions}', + ), + const Divider(), + ], + ], + ], + (final ManualConsoleLog log) => [ + _DataRow(log.content), + ], + }; +} + +/// dialog with detailed log content +class ConsoleItemDialog extends StatelessWidget { + const ConsoleItemDialog({ + required this.log, + super.key, + }); + + final ConsoleLog log; + + @override + Widget build(final BuildContext context) => AlertDialog( + scrollable: true, + title: Text(log.title), + contentPadding: const EdgeInsets.symmetric( + vertical: 16, + horizontal: 12, + ), + content: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const Divider(), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: SelectableText.rich( + TextSpan( + style: DefaultTextStyle.of(context).style, + children: [ + TextSpan( + text: '${'console_page.logged_at'.tr()}: ', + style: const TextStyle(), + ), + TextSpan( + text: '${log.timeString} (${log.fullUTCString})', + style: const TextStyle( + fontWeight: FontWeight.w700, + fontFeatures: [FontFeature.tabularFigures()], + ), + ), + ], + ), + ), + ), + const Divider(), + ...log.unwrapContent(context), + ], + ), + actions: [ + if (log is LogWithRawResponse) + TextButton( + onPressed: () => PlatformAdapter.setClipboard( + (log as LogWithRawResponse).rawResponse, + ), + child: Text('console_page.copy_raw'.tr()), + ), + // A button to copy the request to the clipboard + if (log.shareableData?.isNotEmpty ?? false) + TextButton( + onPressed: () => PlatformAdapter.setClipboard(log.shareableData!), + child: Text('console_page.copy'.tr()), + ), + // close dialog + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text('basis.close'.tr()), + ), + ], + ); +} + +/// different sections delimiter with `title` +class _SectionRow extends StatelessWidget { + const _SectionRow(this.title); + + final String title; + + @override + Widget build(final BuildContext context) => Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: DecoratedBox( + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: Theme.of(context).colorScheme.outlineVariant, + width: 2.4, + ), + ), + ), + child: SelectableText( + title.tr(), + style: const TextStyle( + fontWeight: FontWeight.w800, + fontSize: 20, + ), + ), + ), + ); +} + +/// data row with a {key: value} pair +class _KeyValueRow extends StatelessWidget { + const _KeyValueRow(this.title, this.value); + + /// headers thath should be hidden in screenshots, but still accessible for + /// user, as opposed to `[[ConsoleLog]]` + /// `RestApiRequestConsoleLog.blacklistedHeaders` which need to be filtered + /// out from clipboard content + static const List hideList = ['Authorization']; + + final String title; + final String? value; + + @override + Widget build(final BuildContext context) => hideList.contains(title) + ? _ObscuredKeyValueRow(title, value) + : Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: SelectableText.rich( + TextSpan( + style: DefaultTextStyle.of(context).style, + children: [ + TextSpan( + text: '$title: ', + style: const TextStyle(fontWeight: FontWeight.bold), + ), + TextSpan(text: value ?? ''), + ], + ), + ), + ); +} + +class _ObscuredKeyValueRow extends StatefulWidget { + const _ObscuredKeyValueRow(this.title, this.value); + + final String title; + final String? value; + + @override + State<_ObscuredKeyValueRow> createState() => _ObscuredKeyValueRowState(); +} + +class _ObscuredKeyValueRowState extends State<_ObscuredKeyValueRow> { + static const obscuringCharacter = '•'; + bool _obscureValue = true; + + @override + Widget build(final BuildContext context) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Row( + children: [ + Expanded( + child: SelectableText.rich( + TextSpan( + style: DefaultTextStyle.of(context).style, + children: [ + TextSpan( + text: '${widget.title}: ', + style: const TextStyle(fontWeight: FontWeight.bold), + ), + TextSpan( + text: _obscureValue + ? obscuringCharacter * (widget.value?.length ?? 4) + : widget.value ?? '', + style: const TextStyle( + fontFeatures: [FontFeature.tabularFigures()], + ), + ), + ], + ), + ), + ), + IconButton( + icon: Icon( + _obscureValue + ? Icons.visibility_outlined + : Icons.visibility_off_outlined, + ), + onPressed: () { + _obscureValue ^= true; // toggle value + setState(() {}); + }, + ), + ], + ), + ); +} + +/// data row with only text +class _DataRow extends StatelessWidget { + const _DataRow(this.data); + + final String? data; + + @override + Widget build(final BuildContext context) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: SelectableText( + data ?? 'null', + style: const TextStyle(fontWeight: FontWeight.w400), + ), + ); +} diff --git a/lib/ui/pages/more/console/console_log_item_widget.dart b/lib/ui/pages/more/console/console_log_item_widget.dart new file mode 100644 index 00000000..71d4c138 --- /dev/null +++ b/lib/ui/pages/more/console/console_log_item_widget.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; +import 'package:selfprivacy/logic/models/console_log.dart'; +import 'package:selfprivacy/ui/pages/more/console/console_log_item_dialog.dart'; + +extension on ConsoleLog { + Color resolveColor(final BuildContext context) => isError + ? Theme.of(context).colorScheme.error + : switch (this) { + (final RestApiRequestConsoleLog _) => + Theme.of(context).colorScheme.secondary, + (final RestApiResponseConsoleLog _) => + Theme.of(context).colorScheme.primary, + (final GraphQlRequestConsoleLog _) => + Theme.of(context).colorScheme.secondary, + (final GraphQlResponseConsoleLog _) => + Theme.of(context).colorScheme.tertiary, + (final ManualConsoleLog _) => Theme.of(context).colorScheme.tertiary, + }; + + IconData resolveIcon() => switch (this) { + (final RestApiRequestConsoleLog _) => Icons.upload_outlined, + (final RestApiResponseConsoleLog _) => Icons.download_outlined, + (final GraphQlRequestConsoleLog _) => Icons.arrow_circle_up_outlined, + (final GraphQlResponseConsoleLog _) => Icons.arrow_circle_down_outlined, + (final ManualConsoleLog _) => Icons.read_more_outlined, + }; +} + +class ConsoleLogItemWidget extends StatelessWidget { + const ConsoleLogItemWidget({ + required this.log, + super.key, + }); + + final ConsoleLog log; + + @override + Widget build(final BuildContext context) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: ListTile( + dense: true, + title: Text.rich( + TextSpan( + style: DefaultTextStyle.of(context).style, + children: [ + TextSpan( + text: '${log.timeString}: ', + style: const TextStyle( + fontFeatures: [FontFeature.tabularFigures()], + ), + ), + TextSpan( + text: log.title, + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + subtitle: Text( + log.content, + overflow: TextOverflow.ellipsis, + maxLines: 3, + ), + leading: Icon(log.resolveIcon()), + iconColor: log.resolveColor(context), + onTap: () => showDialog( + context: context, + builder: (final BuildContext context) => + ConsoleItemDialog(log: log), + ), + ), + ); +} diff --git a/lib/ui/pages/more/console/console_page.dart b/lib/ui/pages/more/console/console_page.dart new file mode 100644 index 00000000..e69eb2fe --- /dev/null +++ b/lib/ui/pages/more/console/console_page.dart @@ -0,0 +1,143 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:gap/gap.dart'; +import 'package:selfprivacy/config/get_it_config.dart'; +import 'package:selfprivacy/logic/models/console_log.dart'; +import 'package:selfprivacy/ui/pages/more/console/console_log_item_widget.dart'; + +/// listing with 500 latest app operations. +@RoutePage() +class ConsolePage extends StatefulWidget { + const ConsolePage({super.key}); + + @override + State createState() => _ConsolePageState(); +} + +class _ConsolePageState extends State { + ConsoleModel get console => getIt(); + + /// should freeze logs state to properly read logs + late final Future future; + + @override + void initState() { + super.initState(); + + future = getIt.allReady(); + console.addListener(update); + } + + @override + void dispose() { + console.removeListener(update); + + super.dispose(); + } + + void update() { + /// listener update could come at any time, like when widget is already + /// unmounted or during frame build, adding as postframe callback ensures + /// that element is marked for rebuild + WidgetsBinding.instance.addPostFrameCallback((final _) { + if (mounted) { + setState(() => {}); + } + }); + } + + @override + Widget build(final BuildContext context) => SafeArea( + child: Scaffold( + appBar: AppBar( + title: Text('console_page.title'.tr()), + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () => Navigator.of(context).maybePop(), + ), + actions: [ + IconButton( + icon: Icon( + console.paused + ? Icons.play_arrow_outlined + : Icons.pause_outlined, + ), + onPressed: console.paused ? console.play : console.pause, + ), + ], + ), + body: Scrollbar( + child: FutureBuilder( + future: future, + builder: ( + final BuildContext context, + final AsyncSnapshot snapshot, + ) { + if (snapshot.hasData) { + final List logs = console.logs; + + return logs.isEmpty + ? const _ConsoleViewEmpty() + : _ConsoleViewLoaded(logs: logs); + } + + return const _ConsoleViewLoading(); + }, + ), + ), + ), + ); +} + +class _ConsoleViewLoading extends StatelessWidget { + const _ConsoleViewLoading(); + + @override + Widget build(final BuildContext context) => Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text('console_page.waiting'.tr()), + const Gap(16), + const Expanded( + child: Center( + child: CircularProgressIndicator.adaptive(), + ), + ), + ], + ); +} + +class _ConsoleViewEmpty extends StatelessWidget { + const _ConsoleViewEmpty(); + + @override + Widget build(final BuildContext context) => Align( + alignment: Alignment.topCenter, + child: Text('console_page.history_empty'.tr()), + ); +} + +class _ConsoleViewLoaded extends StatelessWidget { + const _ConsoleViewLoaded({required this.logs}); + + final List logs; + + @override + Widget build(final BuildContext context) => ListView.separated( + primary: true, + padding: const EdgeInsets.symmetric(vertical: 8), + itemCount: logs.length, + itemBuilder: (final BuildContext context, final int index) { + final log = logs[logs.length - 1 - index]; + + return ConsoleLogItemWidget( + key: ValueKey(log), + log: log, + ); + }, + separatorBuilder: (final context, final _) => const Divider(), + ); +} diff --git a/lib/ui/pages/more/more.dart b/lib/ui/pages/more/more.dart index 6e72bdd9..cf297328 100644 --- a/lib/ui/pages/more/more.dart +++ b/lib/ui/pages/more/more.dart @@ -21,11 +21,8 @@ class MorePage extends StatelessWidget { return Scaffold( appBar: Breakpoints.small.isActive(context) - ? PreferredSize( - preferredSize: const Size.fromHeight(52), - child: BrandHeader( - title: 'basis.more'.tr(), - ), + ? BrandHeader( + title: 'basis.more'.tr(), ) : null, body: ListView( diff --git a/lib/ui/pages/onboarding/onboarding.dart b/lib/ui/pages/onboarding/onboarding.dart index 141c9463..14d7a976 100644 --- a/lib/ui/pages/onboarding/onboarding.dart +++ b/lib/ui/pages/onboarding/onboarding.dart @@ -1,6 +1,6 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; -import 'package:selfprivacy/logic/cubit/app_settings/app_settings_cubit.dart'; +import 'package:selfprivacy/config/app_controller/inherited_app_controller.dart'; import 'package:selfprivacy/ui/pages/onboarding/views/views.dart'; import 'package:selfprivacy/ui/router/router.dart'; @@ -37,7 +37,8 @@ class _OnboardingPageState extends State { ), OnboardingSecondView( onProceed: () { - context.read().turnOffOnboarding(); + InheritedAppController.of(context) + .setShouldShowOnboarding(false); context.router.replaceAll([ const RootRoute(), const InitializingRoute(), diff --git a/lib/ui/pages/providers/providers.dart b/lib/ui/pages/providers/providers.dart index ec397805..8e70e609 100644 --- a/lib/ui/pages/providers/providers.dart +++ b/lib/ui/pages/providers/providers.dart @@ -65,11 +65,8 @@ class _ProvidersPageState extends State { return Scaffold( appBar: Breakpoints.small.isActive(context) - ? PreferredSize( - preferredSize: const Size.fromHeight(52), - child: BrandHeader( - title: 'basis.providers_title'.tr(), - ), + ? BrandHeader( + title: 'basis.providers_title'.tr(), ) : null, body: ListView( diff --git a/lib/ui/pages/root_route.dart b/lib/ui/pages/root_route.dart index 6ae7607c..50da7178 100644 --- a/lib/ui/pages/root_route.dart +++ b/lib/ui/pages/root_route.dart @@ -1,9 +1,8 @@ import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:selfprivacy/logic/cubit/app_settings/app_settings_cubit.dart'; +import 'package:selfprivacy/config/app_controller/inherited_app_controller.dart'; import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; -import 'package:selfprivacy/ui/layouts/root_scaffold_with_navigation.dart'; +import 'package:selfprivacy/ui/layouts/root_scaffold_with_subroute_selector/root_scaffold_with_subroute_selector.dart'; import 'package:selfprivacy/ui/router/root_destinations.dart'; import 'package:selfprivacy/ui/router/router.dart'; @@ -19,31 +18,31 @@ class RootPage extends StatefulWidget implements AutoRouteWrapper { } class _RootPageState extends State with TickerProviderStateMixin { - bool shouldUseSplitView() => false; + @override + void didChangeDependencies() { + if (InheritedAppController.of(context).shouldShowOnboarding) { + context.router.replace(const OnboardingRoute()); + } - final destinations = rootDestinations; + super.didChangeDependencies(); + } @override Widget build(final BuildContext context) { final bool isReady = context.watch().state is ServerInstallationFinished; - if (context.read().state.isOnboardingShowing) { - context.router.replace(const OnboardingRoute()); - } - return AutoRouter( builder: (final context, final child) { - final currentDestinationIndex = destinations.indexWhere( + final currentDestinationIndex = rootDestinations.indexWhere( (final destination) => context.router.isRouteActive(destination.route.routeName), ); final isOtherRouterActive = context.router.root.current.name != RootRoute.name; - final routeName = getRouteTitle(context.router.current.name).tr(); - return RootScaffoldWithNavigation( - title: routeName, - destinations: destinations, + + return RootScaffoldWithSubrouteSelector( + destinations: rootDestinations, showBottomBar: !(currentDestinationIndex == -1 && !isOtherRouterActive), showFab: isReady, @@ -53,99 +52,3 @@ class _RootPageState extends State with TickerProviderStateMixin { ); } } - -class MainScreenNavigationRail extends StatelessWidget { - const MainScreenNavigationRail({ - required this.destinations, - super.key, - }); - - final List destinations; - - @override - Widget build(final BuildContext context) { - int? activeIndex = destinations.indexWhere( - (final destination) => - context.router.isRouteActive(destination.route.routeName), - ); - if (activeIndex == -1) { - activeIndex = null; - } - - return Padding( - padding: const EdgeInsets.all(8.0), - child: SizedBox( - height: MediaQuery.of(context).size.height, - width: 72, - child: LayoutBuilder( - builder: (final context, final constraints) => SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints(minHeight: constraints.maxHeight), - child: IntrinsicHeight( - child: NavigationRail( - backgroundColor: Colors.transparent, - labelType: NavigationRailLabelType.all, - destinations: destinations - .map( - (final destination) => NavigationRailDestination( - icon: Icon(destination.icon), - label: Text(destination.label), - ), - ) - .toList(), - selectedIndex: activeIndex, - onDestinationSelected: (final index) { - context.router.replaceAll([destinations[index].route]); - }, - ), - ), - ), - ), - ), - ), - ); - } -} - -class MainScreenNavigationDrawer extends StatelessWidget { - const MainScreenNavigationDrawer({ - required this.destinations, - super.key, - }); - - final List destinations; - - @override - Widget build(final BuildContext context) { - int? activeIndex = destinations.indexWhere( - (final destination) => - context.router.isRouteActive(destination.route.routeName), - ); - if (activeIndex == -1) { - activeIndex = null; - } - - return SizedBox( - height: MediaQuery.of(context).size.height, - width: 296, - child: LayoutBuilder( - builder: (final context, final constraints) => NavigationDrawer( - key: const Key('PrimaryNavigationDrawer'), - selectedIndex: activeIndex, - onDestinationSelected: (final index) { - context.router.replaceAll([destinations[index].route]); - }, - children: [ - const SizedBox(height: 18), - ...destinations.map( - (final destination) => NavigationDrawerDestination( - icon: Icon(destination.icon), - label: Text(destination.label), - ), - ), - ], - ), - ), - ); - } -} diff --git a/lib/ui/pages/services/services.dart b/lib/ui/pages/services/services.dart index 684af19a..15a1b3d9 100644 --- a/lib/ui/pages/services/services.dart +++ b/lib/ui/pages/services/services.dart @@ -37,11 +37,8 @@ class _ServicesPageState extends State { return Scaffold( appBar: Breakpoints.small.isActive(context) - ? PreferredSize( - preferredSize: const Size.fromHeight(52), - child: BrandHeader( - title: 'basis.services'.tr(), - ), + ? BrandHeader( + title: 'basis.services'.tr(), ) : null, body: !isReady diff --git a/lib/ui/pages/setup/initializing/server_type_picker.dart b/lib/ui/pages/setup/initializing/server_type_picker.dart index bdcabe92..25f559b2 100644 --- a/lib/ui/pages/setup/initializing/server_type_picker.dart +++ b/lib/ui/pages/setup/initializing/server_type_picker.dart @@ -1,7 +1,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:selfprivacy/config/app_controller/inherited_app_controller.dart'; import 'package:selfprivacy/illustrations/stray_deer.dart'; -import 'package:selfprivacy/logic/cubit/app_settings/app_settings_cubit.dart'; import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; import 'package:selfprivacy/logic/models/price.dart'; import 'package:selfprivacy/logic/models/server_provider_location.dart'; @@ -205,10 +205,8 @@ class SelectTypePage extends StatelessWidget { ), painter: StrayDeerPainter( colorScheme: Theme.of(context).colorScheme, - colorPalette: context - .read() - .state - .corePaletteOrDefault, + colorPalette: + InheritedAppController.of(context).corePalette, ), ), ), diff --git a/lib/ui/pages/users/new_user.dart b/lib/ui/pages/users/new_user.dart index a55fb681..3327da33 100644 --- a/lib/ui/pages/users/new_user.dart +++ b/lib/ui/pages/users/new_user.dart @@ -14,17 +14,16 @@ class NewUserPage extends StatelessWidget { return BlocProvider( create: (final BuildContext context) { final jobCubit = context.read(); - final jobState = jobCubit.state; - final users = []; - users.addAll(context.read().state.users); - if (jobState is JobsStateWithJobs) { - final jobs = jobState.clientJobList; - for (final job in jobs) { - if (job is CreateUserJob) { - users.add(job.user); - } - } - } + + // final jobsState = jobCubit.state; + // final users = [ + // ...context.read().state.users, + // if (jobsState is JobsStateWithJobs) + // ...jobsState.clientJobList + // .whereType() + // .map((final job) => job.user), + // ]; + return UserFormCubit( jobsCubit: jobCubit, fieldFactory: FieldCubitFactory(context), diff --git a/lib/ui/pages/users/users.dart b/lib/ui/pages/users/users.dart index b7469a6e..ea282f95 100644 --- a/lib/ui/pages/users/users.dart +++ b/lib/ui/pages/users/users.dart @@ -129,11 +129,8 @@ class UsersPage extends StatelessWidget { return Scaffold( appBar: Breakpoints.small.isActive(context) - ? PreferredSize( - preferredSize: const Size.fromHeight(52), - child: BrandHeader( - title: 'basis.users'.tr(), - ), + ? BrandHeader( + title: 'basis.users'.tr(), ) : null, body: child, diff --git a/lib/ui/router/root_destinations.dart b/lib/ui/router/root_destinations.dart index 0d981894..3bd749ba 100644 --- a/lib/ui/router/root_destinations.dart +++ b/lib/ui/router/root_destinations.dart @@ -1,5 +1,4 @@ import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart'; import 'package:selfprivacy/ui/router/router.dart'; @@ -18,29 +17,29 @@ class RouteDestination { final String title; } -final rootDestinations = [ +const List rootDestinations = [ RouteDestination( - route: const ProvidersRoute(), + route: ProvidersRoute(), icon: BrandIcons.server, - label: 'basis.providers'.tr(), - title: 'basis.providers_title'.tr(), + label: 'basis.providers', + title: 'basis.providers_title', ), RouteDestination( - route: const ServicesRoute(), + route: ServicesRoute(), icon: BrandIcons.box, - label: 'basis.services'.tr(), - title: 'basis.services'.tr(), + label: 'basis.services', + title: 'basis.services', ), RouteDestination( - route: const UsersRoute(), + route: UsersRoute(), icon: BrandIcons.users, - label: 'basis.users'.tr(), - title: 'basis.users'.tr(), + label: 'basis.users', + title: 'basis.users', ), RouteDestination( - route: const MoreRoute(), + route: MoreRoute(), icon: Icons.menu_rounded, - label: 'basis.more'.tr(), - title: 'basis.more'.tr(), + label: 'basis.more', + title: 'basis.more', ), ]; diff --git a/lib/ui/router/router.dart b/lib/ui/router/router.dart index 89c43618..44bcd3de 100644 --- a/lib/ui/router/router.dart +++ b/lib/ui/router/router.dart @@ -10,7 +10,7 @@ import 'package:selfprivacy/ui/pages/dns_details/dns_details.dart'; import 'package:selfprivacy/ui/pages/more/about_application.dart'; import 'package:selfprivacy/ui/pages/more/app_settings/app_settings.dart'; import 'package:selfprivacy/ui/pages/more/app_settings/developer_settings.dart'; -import 'package:selfprivacy/ui/pages/more/console.dart'; +import 'package:selfprivacy/ui/pages/more/console/console_page.dart'; import 'package:selfprivacy/ui/pages/more/more.dart'; import 'package:selfprivacy/ui/pages/onboarding/onboarding.dart'; import 'package:selfprivacy/ui/pages/providers/providers.dart'; @@ -53,6 +53,7 @@ class RootRouter extends _$RootRouter { @override RouteType get defaultRouteType => const RouteType.material(); + @override final List routes = [ AutoRoute(page: OnboardingRoute.page), @@ -143,6 +144,8 @@ String getRouteTitle(final String routeName) { return 'domain.screen_title'; case 'ServerDetailsRoute': return 'server.card_title'; + case 'ServerSettingsRoute': + return 'server.settings'; case 'BackupDetailsRoute': return 'backup.card_title'; case 'BackupsListRoute': diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 13f6db44..d831f93f 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -9,7 +9,7 @@ import connectivity_plus import device_info_plus import dynamic_color import flutter_secure_storage_macos -import package_info +import package_info_plus import path_provider_foundation import shared_preferences_foundation import url_launcher_macos @@ -19,7 +19,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin")) FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) - FLTPackageInfoPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlugin")) + FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 7d720b06..1629a8ee 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -9,7 +9,7 @@ PODS: - flutter_secure_storage_macos (6.1.1): - FlutterMacOS - FlutterMacOS (1.0.0) - - package_info (0.0.1): + - package_info_plus (0.0.1): - FlutterMacOS - path_provider_foundation (0.0.1): - Flutter @@ -27,7 +27,7 @@ DEPENDENCIES: - dynamic_color (from `Flutter/ephemeral/.symlinks/plugins/dynamic_color/macos`) - flutter_secure_storage_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos`) - FlutterMacOS (from `Flutter/ephemeral`) - - package_info (from `Flutter/ephemeral/.symlinks/plugins/package_info/macos`) + - package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`) - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) @@ -47,8 +47,8 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos FlutterMacOS: :path: Flutter/ephemeral - package_info: - :path: Flutter/ephemeral/.symlinks/plugins/package_info/macos + package_info_plus: + :path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos path_provider_foundation: :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin shared_preferences_foundation: @@ -58,16 +58,16 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: connectivity_plus: 18d3c32514c886e046de60e9c13895109866c747 - device_info_plus: 5401765fde0b8d062a2f8eb65510fb17e77cf07f + device_info_plus: ce1b7762849d3ec103d0e0517299f2db7ad60720 dynamic_color: 2eaa27267de1ca20d879fbd6e01259773fb1670f - flutter_secure_storage_macos: d56e2d218c1130b262bef8b4a7d64f88d7f9c9ea + flutter_secure_storage_macos: 59459653abe1adb92abbc8ea747d79f8d19866c9 FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 - package_info: 6eba2fd8d3371dda2d85c8db6fe97488f24b74b2 - path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 + package_info_plus: fa739dd842b393193c5ca93c26798dff6e3d0e0c + path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 - shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126 - url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95 + shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 + url_launcher_macos: 5f437abeda8c85500ceb03f5c1938a8c5a705399 PODFILE CHECKSUM: b0cc1fdf1eda0fefb5163971bbf18550427d02c4 -COCOAPODS: 1.15.2 +COCOAPODS: 1.15.1 diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index 37e2180a..f6ef3a25 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -202,7 +202,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 1430; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 33CC10EC2044A3C60003C045 = { @@ -419,6 +419,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; DEVELOPMENT_TEAM = 46723VZHWZ; @@ -550,9 +551,10 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = 46723VZHWZ; + DEVELOPMENT_TEAM = 7SWL2X7X4N; INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = SelfPrivacy; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; @@ -562,6 +564,7 @@ ); MACOSX_DEPLOYMENT_TARGET = 10.14; MARKETING_VERSION = 0.8.0; + PRODUCT_BUNDLE_IDENTIFIER = dev.misterfourtytwo.selfprivacy; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -575,6 +578,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; DEVELOPMENT_TEAM = 46723VZHWZ; diff --git a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index aa456674..ce01778a 100644 --- a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ =3.3.0 <4.0.0" + dart: ">=3.3.1 <4.0.0" flutter: ">=3.19.5" diff --git a/pubspec.yaml b/pubspec.yaml index b9f79c39..3872d6e2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -9,58 +9,59 @@ environment: dependencies: animations: ^2.0.11 - auto_route: ^8.0.3 + auto_route: ^8.1.3 auto_size_text: ^3.0.0 bloc_concurrency: ^0.2.5 - crypt: ^4.3.1 collection: ^1.18.0 + crypt: ^4.3.1 cubit_form: ^2.0.1 device_info_plus: ^10.0.1 dio: ^5.4.2+1 duration: ^3.0.13 dynamic_color: ^1.7.0 - easy_localization: ^3.0.5 + easy_localization: ^3.0.6 equatable: ^2.0.5 - fl_chart: ^0.67.0 + fl_chart: ^0.68.0 flutter: sdk: flutter flutter_bloc: ^8.1.5 - flutter_markdown: ^0.6.22 + flutter_markdown: ^0.7.1 flutter_secure_storage: ^9.0.0 flutter_svg: ^2.0.10+1 gap: ^3.0.1 - get_it: ^7.6.4 - gql: ^1.0.0 + get_it: ^7.6.7 + gql: ^1.0.0+1 graphql: ^5.1.3 - graphql_codegen: ^0.13.9 graphql_flutter: ^5.1.2 hive: ^2.2.3 hive_flutter: ^1.1.0 - http: ^1.1.2 + http: ^1.2.1 intl: ^0.18.1 ionicons: ^0.2.2 - json_annotation: ^4.8.1 + json_annotation: ^4.9.0 + local_auth: ^2.2.0 material_color_utilities: ^0.8.0 - modal_bottom_sheet: ^3.0.0 + modal_bottom_sheet: ^3.0.0-pre nanoid: ^1.0.0 - package_info: ^2.0.2 + package_info_plus: ^8.0.0 pretty_dio_logger: ^1.3.1 - provider: ^6.1.1 + provider: ^6.1.2 pub_semver: ^2.1.4 - timezone: ^0.9.2 - url_launcher: ^6.2.1 - # TODO: Developer is not available, update later. -# wakelock: ^0.6.2 + timezone: ^0.9.3 + url_launcher: ^6.2.5 +# wakelock: ^0.6.2 # TODO: Developer is not available, update later. + dev_dependencies: auto_route_generator: ^8.0.0 + build_runner: ^2.4.9 + cupertino_icons: ^1.0.8 + flutter_launcher_icons: ^0.13.1 + flutter_lints: ^3.0.2 flutter_test: sdk: flutter - build_runner: ^2.4.9 - flutter_launcher_icons: ^0.13.1 hive_generator: ^2.0.1 - json_serializable: ^6.7.1 - flutter_lints: ^3.0.2 + json_serializable: ^6.8.0 flutter_icons: android: "launcher_icon"