language picker, console_page refactor, app settings controller #482

Open
misterfourtytwo wants to merge 34 commits from misterfourtytwo/selfprivacy.org.app:feat_token_management into master
103 changed files with 2524 additions and 1859 deletions

53
.vscode/launch.json vendored Normal file
View File

@ -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"
]
}
]
}

View File

@ -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,13 +24,8 @@ 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
ndkVersion flutter.ndkVersion
@ -44,12 +42,15 @@ android {
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
@ -58,30 +59,32 @@ android {
versionName flutterVersionName
}
flavorDimensions "default"
productFlavors {
fdroid {
applicationId "pro.kherel.selfprivacy"
buildTypes {
debug {
}
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 {}

View File

@ -1,5 +1,4 @@
buildscript {
ext.kotlin_version = '1.9.21'
ext.getVersionCode = { ->
try {
def stdout = new ByteArrayOutputStream()
@ -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 {

View File

@ -1,4 +1,4 @@
org.gradle.jvmargs=-Xmx1536M
org.gradle.jvmargs=-Xmx4G
android.useAndroidX=true
android.enableJetifier=true
android.bundle.enableUncompressedNativeLibs=false

View File

@ -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

View File

@ -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"

View File

@ -333,14 +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": "قم بإعادة ضبط إعدادات التطبيق",
"delete_server_title": "قم بحذف الخادم",
"delete_server_description": "سيزيل هذا الخادم الخاص بك، حيث أنه لن تتمكن من الوصول إليه بعد ذلك.",
"dark_theme_description": "قم بتبديل وضع التطبيق",
"reset_config_description": "قم بإعادة ضبط مفاتيح API والمستخدم المميز."
},
"ssh": {

View File

@ -54,15 +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.",
"delete_server_title": "Serveri silin",
"dark_theme_description": "Rəng mövzusunu dəyişdirin",
"delete_server_description": "Əməliyyat serveri siləcək. Bundan sonra o, əlçatmaz olacaq.",
"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ı",

View File

@ -51,7 +51,7 @@
"connect_to_server_provider": "Аўтарызавацца ў ",
"connect_to_server_provider_text": "З дапамогай API токена праграма SelfPrivacy зможа ад вашага імя замовіць і наладзіць сервер",
"steps": {
"nixos_installation": "Ўстаноўка NixOS",
"nixos_installation": "Ўсталёўка NixOS",
Review

Translations directly to the JSON files might cause conflicts with Weblate. It is better to use weblate.selfprivacy.org in the future if you want to change a translation.

Changing en.json is fine, as this is a source for all translations.

Translations directly to the JSON files might cause conflicts with Weblate. It is better to use weblate.selfprivacy.org in the future if you want to change a translation. Changing `en.json` is fine, as this is a source for all translations.

I guess it is about already existing keys, like, for when ill want to add translation for a new key, it will be ok?

I guess it is about already existing keys, like, for when ill want to add translation for a new key, it will be ok?
"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": "Налады",
@ -234,7 +234,7 @@
},
"more_page": {
"configuration_wizard": "Майстар наладкі",
"onboarding": "Прівітанне",
"onboarding": "Прывітанне",
"create_ssh_key": "SSH ключы адміністратара"
},
"about_application_page": {
@ -244,16 +244,16 @@
"privacy_policy": "Палітыка прыватнасці"
},
"application_settings": {
"reset_config_description": "Скінуць API ключы i суперкарыстальніка.",
"delete_server_description": "Дзеянне прывядзе да выдалення сервера. Пасля гэтага ён будзе недаступны.",
"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": "Скід налад",
"delete_server_title": "Выдаліць сервер",
"system_dark_theme_title": "Сістэмная тэма па-змаўчанні",
"system_dark_theme_description": "Выкарыстоўвайце светлую ці цёмную тэмы ў залежнасці ад сістэмных налад",
"dangerous_settings": "Небяспечныя наладкі"
"reset_config_description": "Скінуць API ключы i суперкарыстальніка."
},
"ssh": {
"root_subtitle": "Уладальнікі паказаных тут ключоў атрымліваюць поўны доступ да дадзеных і налад сервера. Дадавайце выключна свае ключы.",

View File

@ -54,15 +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.",
"delete_server_title": "Odstranit server",
"dark_theme_description": "Přepnutí tématu aplikace",
"delete_server_description": "Tím odstraníte svůj server. Nebude již přístupný.",
"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",

View File

@ -57,15 +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.",
"delete_server_title": "Server löschen",
"delete_server_description": "Das wird Ihren Server löschen. Es wird nicht mehr zugänglich sein.",
"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",

View File

@ -47,7 +47,29 @@
"console_page": {
"title": "Console",
"waiting": "Waiting for initialization…",
"copy": "Copy"
"copy": "Copy",
"copy_raw": "Raw response",
"historyEmpty": "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."

View File

@ -39,16 +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.",
"delete_server_title": "Eliminar servidor",
"delete_server_description": "Esto elimina su servidor. Ya no será accesible.",
"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?",

View File

@ -1,15 +1,13 @@
{
"application_settings": {
"system_dark_theme_description": "Kasutage valgus- või tumeteemat sõltuvalt süsteemi seadetest",
"delete_server_description": "See eemaldab teie serveri. Seda ei saa enam juurde pääseda.",
"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.",
"delete_server_title": "Kustuta server"
"reset_config_description": "Lähtestab API võtmed ja juurkasutaja."
},
"server": {
"reboot_after_upgrade": "Taaskäivita pärast värskendust",

View File

@ -56,15 +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",
"delete_server_title": "Supprimer le serveur",
"delete_server_description": "Cela va supprimer votre serveur. Celui-ci ne sera plus accessible.",
"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",

View File

@ -81,15 +81,13 @@
},
"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 ומשתמש העל.",
"delete_server_title": "מחיקת שרת",
"delete_server_description": "מסיר את השרת שלך. הוא לא יהיה זמין עוד."
"reset_config_description": "איפוס מפתחות ה־API ומשתמש העל."
},
"backup": {
"create_new_select_heading": "לבחור מה לגבות",

View File

@ -91,16 +91,14 @@
"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": "Қараңғы тақырып сипаттамасы",
"delete_server_title": "Серверді жою",
"reset_config_description": "Конфигурацияны қалпына келтіру сипаттамасы.",
"delete_server_description": "Серверді жою сипаттамасы."
"reset_config_description": "Конфигурацияны қалпына келтіру сипаттамасы."
},
"resource_chart": {
"month": "Ай",

View File

@ -52,16 +52,14 @@
"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.",
"delete_server_title": "Izdzēst serveri",
"delete_server_description": "Šis izdzēš jūsu serveri. Tas vairs nebūs pieejams."
"reset_config_description": "Atiestatīt API atslēgas un saknes lietotāju."
},
"locale": "lv",
"ssh": {

View File

@ -56,15 +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.",
"delete_server_title": "Usuń serwer",
"delete_server_description": "Ta czynność usunie serwer. Po tym będzie niedostępny.",
"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",

View File

@ -75,15 +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 пользователя.",
"delete_server_title": "Удалить сервер",
"delete_server_description": "Действие приведёт к удалению сервера. После этого он будет недоступен.",
"system_dark_theme_title": "Системная тема",
"system_dark_theme_description": "Будет использована светлая или тёмная тема в зависимости от системных настроек",
"dangerous_settings": "Опасные настройки"
"reset_config_description": "Сбросить API ключи и root пользователя."
},
"ssh": {
"title": "SSH ключи",

View File

@ -103,15 +103,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.",
"delete_server_title": "Zmazať server",
"delete_server_description": "Tým sa odstráni váš server. Už nebude prístupným.",
"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",

View File

@ -53,15 +53,13 @@
"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",
"delete_server_title": "Brisanje strežnika",
"delete_server_description": "To dejanje povzroči izbris strežnika. Nato bo nedosegljiv."
"reset_config_title": "Ponastavitev konfiguracije aplikacije"
},
"onboarding": {
"page1_title": "Digitalna neodvisnost je na voljo vsem",

View File

@ -47,13 +47,11 @@
"privacy_policy": "นโยบายความเป็นส่วนตัว"
},
"application_settings": {
"dark_theme_description": "สลับธีมแอปพลิเคชั่นของคุณ",
"delete_server_description": "การกระทำนี้จะลบเซิฟเวอร์ของคุณทิ้งและคุณจะไม่สามารถเข้าถึงมันได้อีก",
"title": "การตั้งค่าแอปพลิเคชัน",
"dark_theme_title": "ธีมมืด",
"change_application_theme": "สลับธีมแอปพลิเคชั่นของคุณ",
"reset_config_title": "รีเซ็ตค่าดั้งเดิมการตั้งค่าของแอปพลิเคชั่น",
"reset_config_description": "รีเซ็ต API key และผู้ใช้งาน root",
"delete_server_title": "ลบเซิฟเวอร์"
"reset_config_description": "รีเซ็ต API key และผู้ใช้งาน root"
},
"ssh": {
"create": "สร้างกุญแจ SSH",

View File

@ -41,15 +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 користувача.",
"delete_server_title": "Видалити сервер",
"delete_server_description": "Це видалить ваш сервер. Він більше не буде доступний.",
"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-ключ?",

View File

@ -476,15 +476,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": "删除服务器",
"delete_server_description": "这将移除您的服务器。它将不再可以访问。",
"reset_config_description": "重置API密钥和root用户。"
"delete_server_title": "删除服务器"
},
"ssh": {
"title": "SSH密钥",

View File

@ -0,0 +1,183 @@
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/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<ApiConfigModel>();
bool _loaded = false;
bool get loaded => _loaded;
// localization
late Locale _locale;
Locale get locale => _locale;
late List<Locale> _supportedLocales;
List<Locale> 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;
// // Make ThemeMode a private variable so it is not updated directly without
// // also persisting the changes with the repo..
// late ThemeMode _themeMode;
Review

I guess it is no longer needed?

I guess it is no longer needed?
// // Allow Widgets to read the user's preferred ThemeMode.
// ThemeMode get themeMode => _themeMode;
late bool _shouldShowOnboarding;
bool get shouldShowOnboarding => _shouldShowOnboarding;
/// Load the user's settings from the SettingsService. It may load from a
/// local database or the internet. The controller only knows it can load the
Review

From the internet?

From the internet?
/// settings from the service.
Future<void> 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(<Future>[
// load locale
() async {
_supportedLocales = await _repo.getSupportedLocales();
_locale = await _repo.getActiveLocale();
// 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;
// _themeMode = await _repo.getThemeMode();
_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<void> 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<void> 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<void> 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);
}
// /// Update and persist the ThemeMode based on the user's selection.
// Future<void> setThemeMode(final ThemeMode newThemeMode) async {
// // Do not perform any work if new and old ThemeMode are identical
// if (newThemeMode == themeMode) {
// return;
// }
// // Store the new ThemeMode in memory
// _themeMode = newThemeMode;
// // Inform listeners a change has occurred.
// notifyListeners();
// // Persist the change
// await _repo.setThemeMode(newThemeMode);
// }
Future<void> 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;
/// 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);
}
}

View File

@ -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<AppController> {
const _AppControllerInjector({
required super.child,
required super.notifier,
});
}
class InheritedAppController extends StatefulWidget {
const InheritedAppController({
required this.child,
super.key,
});
final Widget child;
@override
State<InheritedAppController> createState() => _InheritedAppControllerState();
static AppController of(final BuildContext context) => context
.dependOnInheritedWidgetOfExactType<_AppControllerInjector>()!
.notifier!;
}
class _InheritedAppControllerState extends State<InheritedAppController> {
// 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<void> initialize() async {
late final ThemeData lightThemeData;
late final ThemeData darkThemeData;
late final color_utils.CorePalette colorPalette;
await Future.wait(
<Future<void>>[
() 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,
);
}

View File

@ -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,19 +55,8 @@ class BlocAndProviderConfigState extends State<BlocAndProviderConfig> {
}
@override
Widget build(final BuildContext context) {
const isDark = false;
const isAutoDark = true;
return MultiProvider(
Widget build(final BuildContext context) => MultiProvider(
providers: [
BlocProvider(
create: (final _) => AppSettingsCubit(
isDarkModeOn: isDark,
isAutoDarkModeOn: isAutoDark,
isOnboardingShowing: true,
)..load(),
),
BlocProvider(
create: (final _) => supportSystemCubit,
),
@ -109,5 +97,4 @@ class BlocAndProviderConfigState extends State<BlocAndProviderConfig> {
],
child: widget.child,
);
}
}

View File

@ -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<void> getItSetup() async {
getIt.registerSingleton<NavigationService>(NavigationService());
getIt.registerSingleton<ConsoleModel>(ConsoleModel());
getIt.registerSingleton<ApiConfigModel>(ApiConfigModel()..init());
final apiConfigModel = ApiConfigModel();
await apiConfigModel.init();
getIt.registerSingleton<ApiConfigModel>(apiConfigModel);
getIt.registerSingleton<ApiConnectionRepository>(
ApiConnectionRepository()..init(),

View File

@ -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';

View File

@ -3,14 +3,13 @@ import 'package:flutter/material.dart';
class Localization extends StatelessWidget {
const Localization({
required this.child,
super.key,
this.child,
});
final Widget? child;
@override
Widget build(final BuildContext context) => EasyLocalization(
supportedLocales: const [
// when adding new locale, add corresponding native language name to mapper
// below
static const supportedLocales = [
Locale('ar'),
Locale('az'),
Locale('be'),
@ -31,12 +30,45 @@ class Localization extends StatelessWidget {
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 = {
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: supportedLocales,
path: 'assets/translations',
fallbackLocale: const Locale('en'),
useFallbackTranslations: true,
saveLocale: false,
useOnlyLangCode: false,
child: child!,
child: child,
);
}

View File

@ -0,0 +1,36 @@
/// abstraction for manipulation of stored app preferences
abstract class PreferencesDataSource {
/// should onboarding be shown
Future<bool> getOnboardingFlag();
/// should onboarding be shown
Future<void> 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<bool?> getSystemThemeModeFlag();
/// should system theme mode be enabled
Future<void> setSystemThemeModeFlag(final bool newValue);
/// should dark theme be enabled
Future<bool?> getDarkThemeModeFlag();
/// should dark theme be enabled
Future<void> setDarkThemeModeFlag(final bool newValue);
/// locale, as set by user
///
///
/// when null, app takes system locale
Future<String?> getLocale();
/// locale, as set by user
///
///
/// when null, app takes system locale
Future<void> setLocale(final String newLocale);
}

View File

@ -0,0 +1,39 @@
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<bool> getOnboardingFlag() async =>
_appSettingsBox.get(BNames.shouldShowOnboarding, defaultValue: true);
@override
Future<void> setOnboardingFlag(final bool newValue) async =>
_appSettingsBox.put(BNames.shouldShowOnboarding, newValue);
@override
Future<bool?> getSystemThemeModeFlag() async =>
_appSettingsBox.get(BNames.systemThemeModeOn);
@override
Future<void> setSystemThemeModeFlag(final bool newValue) async =>
_appSettingsBox.put(BNames.systemThemeModeOn, newValue);
@override
Future<bool?> getDarkThemeModeFlag() async =>
_appSettingsBox.get(BNames.darkThemeModeOn);
@override
Future<void> setDarkThemeModeFlag(final bool newValue) async =>
_appSettingsBox.put(BNames.darkThemeModeOn, newValue);
@override
Future<String?> getLocale() async => _appSettingsBox.get(BNames.appLocale);
@override
Future<void> setLocale(final String newLocale) async =>
_appSettingsBox.put(BNames.appLocale, newLocale);
}

View File

@ -0,0 +1,63 @@
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<InheritedPreferencesRepository> createState() =>
_InheritedPreferencesRepositoryState();
static PreferencesRepository? of(final BuildContext context) => context
.dependOnInheritedWidgetOfExactType<_PreferencesRepositoryInjector>()
?.settingsRepository;
}
class _InheritedPreferencesRepositoryState
extends State<InheritedPreferencesRepository> {
late PreferencesRepository repo;
@override
void didChangeDependencies() {
super.didChangeDependencies();
/// recreate repo each time dependencies change
repo = PreferencesRepository(
dataSource: widget.dataSource,
setDelegateLocale: EasyLocalization.of(context)!.setLocale,
getDelegateLocale: () => EasyLocalization.of(context)!.locale,
getSupportedLocales: () => EasyLocalization.of(context)!.supportedLocales,
);
}
@override
Widget build(final BuildContext context) => _PreferencesRepositoryInjector(
settingsRepository: repo,
child: widget.child,
);
}

View File

@ -0,0 +1,73 @@
import 'dart:async';
import 'package:flutter/material.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,
});
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<void> Function(Locale) setDelegateLocale;
final FutureOr<List<Locale>> Function() getSupportedLocales;
final FutureOr<Locale> Function() getDelegateLocale;
Future<bool> getSystemThemeModeFlag() async =>
(await dataSource.getSystemThemeModeFlag()) ?? true;
Future<void> setSystemThemeModeFlag(final bool newValue) async =>
dataSource.setSystemThemeModeFlag(newValue);
Future<bool> getDarkThemeModeFlag() async =>
(await dataSource.getDarkThemeModeFlag()) ?? false;
Future<void> setDarkThemeModeFlag(final bool newValue) async =>
dataSource.setDarkThemeModeFlag(newValue);
Future<void> setSystemModeFlag(final bool newValue) async =>
dataSource.setSystemThemeModeFlag(newValue);
// Future<ThemeMode> getThemeMode() async {
// final themeMode = await dataSource.getThemeMode()?? ThemeMode.system;
// }
//
// Future<void> setThemeMode(final ThemeMode newThemeMode) =>
// dataSource.setThemeMode(newThemeMode);
Future<List<Locale>> supportedLocales() async => getSupportedLocales();
Future<Locale> 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 ??= await getDelegateLocale();
return chosenLocale;
}
Future<void> setActiveLocale(final Locale newLocale) async {
await dataSource.setLocale(newLocale.toString());
}
/// true when we need to show onboarding
Future<bool> getShouldShowOnboarding() async =>
dataSource.getOnboardingFlag();
/// true when we need to show onboarding
Future<void> setShouldShowOnboarding(final bool newValue) =>
dataSource.setOnboardingFlag(newValue);
}

View File

@ -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<T>(final T objectToLog) {
getIt.get<ConsoleModel>().addMessage(
Message(
text: objectToLog.toString(),
),
);
}
void _addConsoleLog(final ConsoleLog message) =>
getIt.get<ConsoleModel>().log(message);
class RequestLoggingLink extends Link {
@override
@ -20,11 +16,12 @@ class RequestLoggingLink extends Link {
final Request request, [
final NextLink? forward,
]) async* {
getIt.get<ConsoleModel>().addMessage(
GraphQlRequestMessage(
_addConsoleLog(
GraphQlRequestConsoleLog(
// context: request.context,
operationType: request.type.name,
operation: request.operation,
variables: request.variables,
context: request.context,
),
);
yield* forward!(request);
@ -35,11 +32,12 @@ class ResponseLoggingParser extends ResponseParser {
@override
Response parseResponse(final Map<String, dynamic> body) {
final response = super.parseResponse(body);
getIt.get<ConsoleModel>().addMessage(
GraphQlResponseMessage(
_addConsoleLog(
GraphQlResponseConsoleLog(
// context: response.context,
data: response.data,
errors: response.errors,
context: response.context,
rawResponse: jsonEncode(response.response),
),
);
return response;
@ -48,7 +46,12 @@ class ResponseLoggingParser extends ResponseParser {
@override
GraphQLError parseError(final Map<String, dynamic> error) {
final graphQlError = super.parseError(error);
_logToAppConsole(graphQlError);
_addConsoleLog(
ManualConsoleLog.warning(
customTitle: 'GraphQL Error',
content: graphQlError.toString(),
),
);
return graphQlError;
}
}
@ -119,8 +122,9 @@ abstract class GraphQLApiMap {
String token = '';
final serverDetails = getIt<ApiConfigModel>().serverDetails;
if (serverDetails != null) {
token = getIt<ApiConfigModel>().serverDetails!.apiToken;
token = serverDetails.apiToken;
}
return token;
}

View File

@ -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<Dio> getClient({final BaseOptions? customOptions}) async {
@ -57,8 +58,8 @@ abstract class RestApiMap {
}
class ConsoleInterceptor extends InterceptorsWrapper {
void addMessage(final Message message) {
getIt.get<ConsoleModel>().addMessage(message);
void addConsoleLog(final ConsoleLog message) {
getIt.get<ConsoleModel>().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);

View File

@ -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<ConnectionStatusEvent, ConnectionStatusState> {
ConnectionStatusBloc()
: super(
const ConnectionStatusState(
connectionStatus: ConnectionStatus.nonexistent,
),
) {
on<ConnectionStatusChanged>((final event, final emit) {
emit(ConnectionStatusState(connectionStatus: event.connectionStatus));
});
final apiConnectionRepository = getIt<ApiConnectionRepository>();
_apiConnectionStatusSubscription =
apiConnectionRepository.connectionStatusStream.listen(
(final ConnectionStatus connectionStatus) {
add(
ConnectionStatusChanged(connectionStatus),
);
},
);
}
StreamSubscription? _apiConnectionStatusSubscription;
@override
Future<void> close() {
_apiConnectionStatusSubscription?.cancel();
return super.close();
}
}

View File

@ -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<Object?> get props => [connectionStatus];
}

View File

@ -1,12 +0,0 @@
part of 'connection_status_bloc.dart';
class ConnectionStatusState extends Equatable {
const ConnectionStatusState({
required this.connectionStatus,
});
final ConnectionStatus connectionStatus;
@override
List<Object> get props => [connectionStatus];
}

View File

@ -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
Review

Unused as of now, and I'm still not sure if it is a good idea to create dependencies between blocs.

Unused as of now, and I'm still not sure if it is a good idea to create dependencies between blocs.

well, i'd prefer an inheritedWidget, which holds this value for dependencies, but also explicitly listens to connectivity and app lifecycle state changes (l mean here when it is suspended, like when app active app changes on android), thus it will hold more meaning and could spare us from some errors.

well, i'd prefer an inheritedWidget, which holds this value for dependencies, but also explicitly listens to connectivity and app lifecycle state changes (l mean here when it is suspended, like when app active app changes on android), thus it will hold more meaning and could spare us from some errors.
class ConnectionStatusBloc extends Bloc<ConnectionStatus, ConnectionStatus> {
Review

Does passing the same enum as both Event and State even work?

Does passing the same enum as both Event and State even work?

why won't it?
you have one place, where events come, and another where state is. also, states are set from inside the bloc only.

why won't it? you have one place, where events come, and another where state is. also, states are set from inside the bloc only.
ConnectionStatusBloc() : super(ConnectionStatus.nonexistent) {
on<ConnectionStatus>(
(final newStatus, final emit) => emit(newStatus),
);
final apiConnectionRepository = getIt<ApiConnectionRepository>();
_apiConnectionStatusSubscription =
apiConnectionRepository.connectionStatusStream.listen(
(final ConnectionStatus newStatus) => add(newStatus),
);
}
StreamSubscription? _apiConnectionStatusSubscription;
@override
Future<void> close() {
_apiConnectionStatusSubscription?.cancel();
return super.close();
}
}

View File

@ -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<AppSettingsState> {
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));
}
}

View File

@ -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<dynamic> get props =>
[isDarkModeOn, isAutoDarkModeOn, isOnboardingShowing, corePalette];
}

View File

@ -475,7 +475,7 @@ class ServerInstallationRepository {
Future<void> deleteServerDetails() async {
await box.delete(BNames.serverDetails);
getIt<ApiConfigModel>().init();
await getIt<ApiConfigModel>().init();
}
Future<void> saveServerProviderType(final ServerProviderType type) async {
@ -501,7 +501,7 @@ class ServerInstallationRepository {
Future<void> deleteServerProviderKey() async {
await box.delete(BNames.hetznerKey);
getIt<ApiConfigModel>().init();
await getIt<ApiConfigModel>().init();
}
Future<void> saveBackblazeKey(
@ -512,7 +512,7 @@ class ServerInstallationRepository {
Future<void> deleteBackblazeKey() async {
await box.delete(BNames.backblazeCredential);
getIt<ApiConfigModel>().init();
await getIt<ApiConfigModel>().init();
}
Future<void> setDnsApiToken(final String key) async {
@ -521,7 +521,7 @@ class ServerInstallationRepository {
Future<void> deleteDnsProviderKey() async {
await box.delete(BNames.cloudFlareKey);
getIt<ApiConfigModel>().init();
await getIt<ApiConfigModel>().init();
}
Future<void> saveDomain(final ServerDomain serverDomain) async {
@ -530,7 +530,7 @@ class ServerInstallationRepository {
Future<void> deleteDomain() async {
await box.delete(BNames.serverDomain);
getIt<ApiConfigModel>().init();
await getIt<ApiConfigModel>().init();
}
Future<void> saveIsServerStarted(final bool value) async {
@ -604,6 +604,6 @@ class ServerInstallationRepository {
BNames.hasFinalChecked,
BNames.isLoading,
]);
getIt<ApiConfigModel>().init();
await getIt<ApiConfigModel>().init();
}
}

View File

@ -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<bool?> get _fulfilementList {
List<bool?> get _fulfillmentList {
final List<bool> res = [
isServerProviderApiKeyFilled,
isServerTypeFilled,

View File

@ -88,7 +88,6 @@ class ApiConfigModel {
}
void clear() {
_localeCode = null;
_serverProviderKey = null;
_dnsProvider = null;
_serverLocation = null;
@ -101,7 +100,7 @@ class ApiConfigModel {
_serverProvider = null;
}
void init() {
Future<void> init() async {
_localeCode = 'en';
_serverProviderKey = _box.get(BNames.hetznerKey);
_serverLocation = _box.get(BNames.serverLocation);

View File

@ -1,17 +0,0 @@
import 'package:flutter/material.dart';
import 'package:selfprivacy/logic/models/message.dart';
class ConsoleModel extends ChangeNotifier {
final List<Message> _messages = [];
List<Message> 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);
}
}
}

View File

@ -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<ConsoleLog> _logs = [];
final List<ConsoleLog> _incomingQueue = [];
bool _paused = false;
bool get paused => _paused;
List<ConsoleLog> 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);
Review

Why not using the same optimization as with _incomingQueue?

Why not using the same optimization as with `_incomingQueue`?

my guess was that buffer of 500 messages was a console feature, so I left it as is.
also, jumps between 750 and 500 elements in listing user sees are not perceived as stable behavior from user point of view.

_incomingQueue is an offscreen buffer, so it doesn't affect what is rendered and we could truncate it lazily, so jumps between 750 and 500 elements should be a ok.

my guess was that buffer of 500 messages was a console feature, so I left it as is. also, jumps between 750 and 500 elements in listing user sees are not perceived as stable behavior from user point of view. `_incomingQueue` is an offscreen buffer, so it doesn't affect what is rendered and we could truncate it lazily, so jumps between 750 and 500 elements should be a ok.
}
notifyListeners();
}
}

View File

@ -0,0 +1,175 @@
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.
///
/// TODO(misterfourtytwo): should we add?
///
/// * equality override
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,
});
final String? method;
final Uri? uri;
final Map<String, dynamic>? headers;
final String? data;
@override
String get title => 'Rest API Request';
@override
String get content => '"method": "$method",\n'
'"uri": "$uri",\n'
'"headers": ${jsonEncode(headers)},\n'
'"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<String, dynamic>? 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<String, dynamic>? data;
final List<gql_client.GraphQLError>? errors;
@override
String get title => 'GraphQL Response';
@override
String get content =>
// '"context": ${context?.encode},\n'
'"data": ${jsonEncode(data)},\n'
'"errors": $errors';
}

View File

@ -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';
}
}

View File

@ -10,8 +10,10 @@ DigitalOceanVolume _$DigitalOceanVolumeFromJson(Map<String, dynamic> json) =>
DigitalOceanVolume(
json['id'] as String,
json['name'] as String,
json['size_gigabytes'] as int,
(json['droplet_ids'] as List<dynamic>?)?.map((e) => e as int).toList(),
(json['size_gigabytes'] as num).toInt(),
(json['droplet_ids'] as List<dynamic>?)
?.map((e) => (e as num).toInt())
.toList(),
);
Map<String, dynamic> _$DigitalOceanVolumeToJson(DigitalOceanVolume instance) =>
@ -42,10 +44,10 @@ DigitalOceanServerType _$DigitalOceanServerTypeFromJson(
(json['regions'] as List<dynamic>).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<String, dynamic> _$DigitalOceanServerTypeToJson(

View File

@ -24,8 +24,8 @@ CloudflareDnsRecord _$CloudflareDnsRecordFromJson(Map<String, dynamic> 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?,
);

View File

@ -8,7 +8,7 @@ part of 'desec_dns_info.dart';
DesecDomain _$DesecDomainFromJson(Map<String, dynamic> json) => DesecDomain(
name: json['name'] as String,
minimumTtl: json['minimum_ttl'] as int?,
minimumTtl: (json['minimum_ttl'] as num?)?.toInt(),
);
Map<String, dynamic> _$DesecDomainToJson(DesecDomain instance) =>
@ -21,7 +21,7 @@ DesecDnsRecord _$DesecDnsRecordFromJson(Map<String, dynamic> 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<dynamic>).map((e) => e as String).toList(),
);

View File

@ -9,7 +9,7 @@ part of 'digital_ocean_dns_info.dart';
DigitalOceanDomain _$DigitalOceanDomainFromJson(Map<String, dynamic> json) =>
DigitalOceanDomain(
name: json['name'] as String,
ttl: json['ttl'] as int?,
ttl: (json['ttl'] as num?)?.toInt(),
);
Map<String, dynamic> _$DigitalOceanDomainToJson(DigitalOceanDomain instance) =>
@ -21,12 +21,12 @@ Map<String, dynamic> _$DigitalOceanDomainToJson(DigitalOceanDomain instance) =>
DigitalOceanDnsRecord _$DigitalOceanDnsRecordFromJson(
Map<String, dynamic> 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<String, dynamic> _$DigitalOceanDnsRecordToJson(

View File

@ -8,7 +8,7 @@ part of 'hetzner_server_info.dart';
HetznerServerInfo _$HetznerServerInfoFromJson(Map<String, dynamic> 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<String, dynamic> json) =>
json['server_type'] as Map<String, dynamic>),
HetznerServerInfo.locationFromJson(json['datacenter'] as Map),
HetznerPublicNetInfo.fromJson(json['public_net'] as Map<String, dynamic>),
(json['volumes'] as List<dynamic>).map((e) => e as int).toList(),
(json['volumes'] as List<dynamic>)
.map((e) => (e as num).toInt())
.toList(),
);
Map<String, dynamic> _$HetznerServerInfoToJson(HetznerServerInfo instance) =>
@ -58,7 +60,7 @@ Map<String, dynamic> _$HetznerPublicNetInfoToJson(
};
HetznerIp4 _$HetznerIp4FromJson(Map<String, dynamic> 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<String, dynamic> _$HetznerIp4ToJson(HetznerIp4 instance) =>
HetznerServerTypeInfo _$HetznerServerTypeInfoFromJson(
Map<String, dynamic> 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<dynamic>)
.map((e) => HetznerPriceInfo.fromJson(e as Map<String, dynamic>))
.toList(),
@ -132,9 +134,9 @@ Map<String, dynamic> _$HetznerLocationToJson(HetznerLocation instance) =>
HetznerVolume _$HetznerVolumeFromJson(Map<String, dynamic> 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?,
);

View File

@ -15,7 +15,7 @@ RecoveryKeyStatus _$RecoveryKeyStatusFromJson(Map<String, dynamic> 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<String, dynamic> _$RecoveryKeyStatusToJson(RecoveryKeyStatus instance) =>

View File

@ -15,7 +15,7 @@ ServerJob _$ServerJobFromJson(Map<String, dynamic> 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

View File

@ -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<String, dynamic>? 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<String, dynamic>? data;
final List<GraphQLError>? 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<String, dynamic>? variables;
final Context? context;
}

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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,
);

View File

@ -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,
);

View File

@ -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);
// }
try {
await Future.wait(
<Future<void>>[
HiveConfig.init(),
EasyLocalization.ensureInitialized(),
],
);
await getItSetup();
await EasyLocalization.ensureInitialized();
tz.initializeTimeZones();
} 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,
class AppBuilder extends StatelessWidget {
const AppBuilder({super.key});
@override
Widget build(final BuildContext context) {
final appController = InheritedAppController.of(context);
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,
});
final ThemeData lightThemeData;
final ThemeData darkThemeData;
@override
State<SelfprivacyApp> createState() => _SelfprivacyAppState();
}
class _SelfprivacyAppState extends State<SelfprivacyApp> {
final appKey = UniqueKey();
final _appRouter = RootRouter(getIt.get<NavigationService>().navigatorKey);
@override
Widget build(final BuildContext context) => Localization(
child: BlocAndProviderConfig(
child: BlocBuilder<AppSettingsCubit, AppSettingsState>(
builder: (
final BuildContext context,
final AppSettingsState appSettings,
) {
getIt.get<ApiConfigModel>().setLocaleCode(
context.locale.languageCode,
);
return MaterialApp.router(
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<NavigationService>().scaffoldMessengerKey,
localizationsDelegates: context.localizationDelegates,
supportedLocales: context.supportedLocales,
// 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,
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...'));
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;
ErrorWidget.builder = (final FlutterErrorDetails errorDetails) => error;
return widget ?? error;
},
);
},
),
),
);
}
}

View File

@ -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<ColorScheme?> _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);

View File

@ -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;

View File

@ -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(

View File

@ -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>[
TextSpan(
text: '${message.timeString}: \n',
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
TextSpan(text: message.text),
],
),
),
);
}

View File

@ -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: [
if (showReadyCard)
const Padding(
padding: EdgeInsets.symmetric(horizontal: 15),
padding: EdgeInsets.symmetric(
vertical: 15,
horizontal: 15,
),
child: NotReadyCard(),
),
Expanded(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: Center(
child: _expandedContent(context),
),
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,

View File

@ -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<RouteDestination> 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<double> animation) =>
SlideTransition(
position: animation.drive(
Tween<Offset>(
begin: const Offset(0.0, 0.2),
end: Offset.zero,
),
),
child: FadeTransition(
opacity: animation,
child: child,
),
),
child: SizedBox(
key: ValueKey<String>(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<RouteDestination> 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<RouteDestination> 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<RouteDestination> 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),
),
),
],
),
);
}
}

View File

@ -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(),
),
);
}
}

View File

@ -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()),
),
],
),
);
}

View File

@ -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),
),
),
),
),
);
}
}

View File

@ -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<double> animation) =>
SlideTransition(
position: animation.drive(
Tween<Offset>(
begin: const Offset(0.0, 0.2),
end: Offset.zero,
),
),
child: FadeTransition(
opacity: animation,
child: child,
),
),
child: SizedBox(
key: ValueKey<String>(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()],
);
}

View File

@ -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<RouteDestination> 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,
);
}

View File

@ -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<RouteDestination> 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<int> openSubpage(final BuildContext context) => (final index) {
context.router.replaceAll([subroutes[index].route]);
};
}

View File

@ -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,

View File

@ -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';

View File

@ -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,36 @@ class AppSettingsPage extends StatefulWidget {
class _AppSettingsPageState extends State<AppSettingsPage> {
@override
Widget build(final BuildContext context) {
final bool isDarkModeOn =
context.watch<AppSettingsCubit>().state.isDarkModeOn;
final bool isSystemDarkModeOn =
context.watch<AppSettingsCubit>().state.isAutoDarkModeOn;
return BrandHeroScreen(
Widget build(final BuildContext context) => BrandHeroScreen(
hasBackButton: true,
hasFlashButton: false,
bodyPadding: const EdgeInsets.symmetric(vertical: 16),
bodyPadding: const EdgeInsets.symmetric(
horizontal: 12,
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<AppSettingsCubit>()
.updateAutoDarkMode(isAutoDarkModeOn: !isSystemDarkModeOn),
_ThemePicker(
key: ValueKey('theme_picker'.tr()),
),
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<AppSettingsCubit>()
.updateDarkMode(isDarkModeOn: !isDarkModeOn),
const Divider(height: 5, thickness: 0),
_LanguagePicker(
key: ValueKey('language_picker'.tr()),
),
const Divider(height: 0),
const Divider(height: 5, thickness: 0),
const Gap(4),
Padding(
padding: const EdgeInsets.all(16),
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
'application_settings.dangerous_settings'.tr(),
style: Theme.of(context).textTheme.labelLarge!.copyWith(
style: Theme.of(context).textTheme.titleLarge!.copyWith(
Review

This should be a labelLarge, looks broken as a titleLarge.

This should be a `labelLarge`, looks broken as a `titleLarge`.
color: Theme.of(context).colorScheme.error,
),
),
),
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<ServerInstallationCubit>().clearAppConfig();
Navigator.of(context).pop();
},
),
DialogActionButton(
text: 'basis.cancel'.tr(),
_ResetAppTile(
key: ValueKey('reset_app'.tr()),
),
],
),
);
},
);
}

View File

@ -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<DeveloperSettingsPage> {
title: Text('developer_settings.reset_onboarding'.tr()),
subtitle:
Text('developer_settings.reset_onboarding_description'.tr()),
enabled:
!context.watch<AppSettingsCubit>().state.isOnboardingShowing,
onTap: () => context
.read<AppSettingsCubit>()
.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<AppSettingsCubit>().state.isOnboardingShowing,
enabled: InheritedAppController.of(context).shouldShowOnboarding,
onTap: () => context.pushRoute(
ServicesMigrationRoute(
diskStatus: context.read<VolumesBloc>().state.diskStatus,

View File

@ -0,0 +1,59 @@
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.headlineSmall,
Review

This is too big

This is too big
),
),
onTap: () async {
final appController = InheritedAppController.of(context);
final Locale? newLocale = await showDialog<Locale?>(
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) => SimpleDialog(
title: Text('application_settings.language'.tr()),
children: [
for (final locale
Review

Probably add a "System default" value to reset our overwrite?

Probably add a "System default" value to reset our overwrite?
in InheritedAppController.of(context).supportedLocales)
ListTile(
title: Text(
Localization.getLanguageName(locale),
style: TextStyle(
fontWeight: locale == context.locale
? FontWeight.w800
Review

How about using a radio button, not just the bold text?

How about using a radio button, not just the bold text?
: FontWeight.w400,
),
),
onTap: () {
Navigator.of(context).pop(locale);
},
),
],
);
}

View File

@ -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<ServerInstallationCubit>().clearAppConfig();
context.router.maybePop([
const RootRoute(),
]);
context.resetLocale();
},
),
DialogActionButton(
text: 'basis.cancel'.tr(),
),
],
);
}

View File

@ -0,0 +1,44 @@
part of 'app_settings.dart';
class _ThemePicker extends StatelessWidget {
const _ThemePicker({super.key});
@override
Widget build(final BuildContext context) {
final appController = InheritedAppController.of(context);
// final themeMode = appController.themeMode;
// final bool isSystemThemeModeEnabled = themeMode == ThemeMode.system;
// final bool isDarkModeOn = themeMode == ThemeMode.dark;
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,
// onChanged: (final newValue) => appController.setThemeMode(
Review

Why is it still there, commented out?

Why is it still there, commented out?
// newValue
// ? ThemeMode.system
// : (isDarkModeOn ? ThemeMode.dark : ThemeMode.light),
// ),
),
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,
// onChanged: isSystemThemeModeEnabled
// ? null
// : (final newValue) => appController.setThemeMode(
// newValue ? ThemeMode.dark : ThemeMode.light,
// ),
),
],
);
}
}

View File

@ -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<ConsolePage> createState() => _ConsolePageState();
}
class _ConsolePageState extends State<ConsolePage> {
bool paused = false;
@override
void initState() {
super.initState();
getIt<ConsoleModel>().addListener(update);
}
@override
void dispose() {
getIt<ConsoleModel>().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<void> snapshot,
) {
if (snapshot.hasData) {
final List<Message> messages =
getIt.get<ConsoleModel>().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(),
],
);
}
},
),
),
);
}

View File

@ -0,0 +1,240 @@
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<Widget> 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) ...[
Review

Rest API requests dialogs expose the API token, and we don't want that (as this console is usually used to send us screenshots/copies of request data)

Rest API requests dialogs expose the API token, and we don't want that (as this console is usually used to send us screenshots/copies of request data)

so you propose to remove headers section or to hide it?

so you propose to remove headers section or to hide it?
Review

Not necessary, just censoring the Authorization header should be enough.

Not necessary, just censoring the Authorization header should be enough.
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>[
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);
final String title;
final String? value;
@override
Widget build(final BuildContext context) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: SelectableText.rich(
TextSpan(
style: DefaultTextStyle.of(context).style,
children: <TextSpan>[
TextSpan(
text: '$title: ',
style: const TextStyle(fontWeight: FontWeight.bold),
),
TextSpan(text: value ?? ''),
],
),
),
);
}
/// 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),
),
);
}

View File

@ -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>[
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),
),
),
);
}

View File

@ -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<ConsolePage> createState() => _ConsolePageState();
}
class _ConsolePageState extends State<ConsolePage> {
ConsoleModel get console => getIt<ConsoleModel>();
/// should freeze logs state to properly read logs
late final Future<void> 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<void> snapshot,
) {
if (snapshot.hasData) {
final List<ConsoleLog> 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.historyEmpty'.tr()),
);
}
class _ConsoleViewLoaded extends StatelessWidget {
const _ConsoleViewLoaded({required this.logs});
final List<ConsoleLog> 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(),
);
}

View File

@ -21,11 +21,8 @@ class MorePage extends StatelessWidget {
return Scaffold(
appBar: Breakpoints.small.isActive(context)
? PreferredSize(
preferredSize: const Size.fromHeight(52),
child: BrandHeader(
? BrandHeader(
title: 'basis.more'.tr(),
),
)
: null,
body: ListView(

View File

@ -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<OnboardingPage> {
),
OnboardingSecondView(
onProceed: () {
context.read<AppSettingsCubit>().turnOffOnboarding();
InheritedAppController.of(context)
.setShouldShowOnboarding(false);
context.router.replaceAll([
const RootRoute(),
const InitializingRoute(),

View File

@ -65,11 +65,8 @@ class _ProvidersPageState extends State<ProvidersPage> {
return Scaffold(
appBar: Breakpoints.small.isActive(context)
? PreferredSize(
preferredSize: const Size.fromHeight(52),
child: BrandHeader(
? BrandHeader(
title: 'basis.providers_title'.tr(),
),
)
: null,
body: ListView(

View File

@ -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<RootPage> 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<ServerInstallationCubit>().state
is ServerInstallationFinished;
if (context.read<AppSettingsCubit>().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<RootPage> with TickerProviderStateMixin {
);
}
}
class MainScreenNavigationRail extends StatelessWidget {
const MainScreenNavigationRail({
required this.destinations,
super.key,
});
final List<RouteDestination> 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<RouteDestination> 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),
),
),
],
),
),
);
}
}

View File

@ -37,11 +37,8 @@ class _ServicesPageState extends State<ServicesPage> {
return Scaffold(
appBar: Breakpoints.small.isActive(context)
? PreferredSize(
preferredSize: const Size.fromHeight(52),
child: BrandHeader(
? BrandHeader(
title: 'basis.services'.tr(),
),
)
: null,
body: !isReady

View File

@ -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<AppSettingsCubit>()
.state
.corePaletteOrDefault,
colorPalette:
InheritedAppController.of(context).corePalette,
),
),
),

View File

@ -14,17 +14,16 @@ class NewUserPage extends StatelessWidget {
return BlocProvider(
create: (final BuildContext context) {
final jobCubit = context.read<JobsCubit>();
final jobState = jobCubit.state;
final users = <User>[];
users.addAll(context.read<UsersBloc>().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 = <User>[
// ...context.read<UsersBloc>().state.users,
// if (jobsState is JobsStateWithJobs)
// ...jobsState.clientJobList
// .whereType<CreateUserJob>()
// .map((final job) => job.user),
// ];

If we don't need the block of code, why not just delete it though

If we don't need the block of code, why not just delete it though

it's simplified logic for filtering jobs with creation of the same(by name) user, which should be added later, but out of scope of changes now

it's simplified logic for filtering jobs with creation of the same(by name) user, which should be added later, but out of scope of changes now
return UserFormCubit(
jobsCubit: jobCubit,
fieldFactory: FieldCubitFactory(context),

View File

@ -129,11 +129,8 @@ class UsersPage extends StatelessWidget {
return Scaffold(
appBar: Breakpoints.small.isActive(context)
? PreferredSize(
preferredSize: const Size.fromHeight(52),
child: BrandHeader(
? BrandHeader(
title: 'basis.users'.tr(),
),
)
: null,
body: child,

View File

@ -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<RouteDestination> 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',
),
];

View File

@ -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<AutoRoute> routes = [
AutoRoute(page: OnboardingRoute.page),

View File

@ -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"))

View File

@ -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

View File

@ -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;

Some files were not shown because too many files have changed in this diff Show More