language picker, console_page refactor, app settings controller #482
|
@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,3 +1,10 @@
|
|||
plugins {
|
||||
id "com.android.application"
|
||||
id "kotlin-android"
|
||||
id "dev.flutter.flutter-gradle-plugin"
|
||||
}
|
||||
|
||||
|
||||
def localProperties = new Properties()
|
||||
def localPropertiesFile = rootProject.file('local.properties')
|
||||
if (localPropertiesFile.exists()) {
|
||||
|
@ -6,10 +13,6 @@ if (localPropertiesFile.exists()) {
|
|||
}
|
||||
}
|
||||
|
||||
def flutterRoot = localProperties.getProperty('flutter.sdk')
|
||||
if (flutterRoot == null) {
|
||||
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
|
||||
}
|
||||
|
||||
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
|
||||
if (flutterVersionCode == null) {
|
||||
|
@ -21,14 +24,9 @@ if (flutterVersionName == null) {
|
|||
flutterVersionName = '1.0'
|
||||
}
|
||||
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
|
||||
|
||||
android {
|
||||
namespace 'org.selfprivacy.app'
|
||||
|
||||
compileSdkVersion flutter.compileSdkVersion
|
||||
namespace 'org.selfprivacy.app'
|
||||
compileSdkVersion flutter.compileSdkVersion
|
||||
ndkVersion flutter.ndkVersion
|
||||
|
||||
sourceSets {
|
||||
|
@ -43,13 +41,16 @@ android {
|
|||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main.java.srcDirs += 'src/main/kotlin'
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
disable 'InvalidPackage'
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId "org.selfprivacy.app"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 34
|
||||
|
@ -57,31 +58,33 @@ android {
|
|||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
|
||||
flavorDimensions "default"
|
||||
productFlavors {
|
||||
fdroid {
|
||||
applicationId "pro.kherel.selfprivacy"
|
||||
}
|
||||
production {
|
||||
applicationIdSuffix ""
|
||||
profile {
|
||||
|
||||
}
|
||||
nightly {
|
||||
applicationIdSuffix ".nightly"
|
||||
versionCode project.getVersionCode()
|
||||
versionName "nightly-" + project.getVersionCode()
|
||||
release {
|
||||
|
||||
}
|
||||
}
|
||||
buildFeatures {
|
||||
flavorDimensions = ["default"]
|
||||
}
|
||||
|
||||
|
||||
flavorDimensions "default"
|
||||
productFlavors {
|
||||
fdroid {
|
||||
dimension 'default'
|
||||
applicationId "pro.kherel.selfprivacy"
|
||||
}
|
||||
production {
|
||||
applicationIdSuffix ""
|
||||
dimension 'default'
|
||||
}
|
||||
nightly {
|
||||
dimension 'default'
|
||||
applicationIdSuffix ".nightly"
|
||||
versionCode project.getVersionCode()
|
||||
versionName "nightly-" + project.getVersionCode()
|
||||
|
@ -93,6 +96,5 @@ flutter {
|
|||
source '../..'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
}
|
||||
dependencies {}
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
buildscript {
|
||||
ext.kotlin_version = '1.9.21'
|
||||
ext.getVersionCode = { ->
|
||||
ext.getVersionCode = { ->
|
||||
try {
|
||||
def stdout = new ByteArrayOutputStream()
|
||||
exec {
|
||||
|
@ -13,15 +12,6 @@ buildscript {
|
|||
return -1
|
||||
}
|
||||
}
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:7.1.2'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
org.gradle.jvmargs=-Xmx1536M
|
||||
org.gradle.jvmargs=-Xmx4G
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
android.bundle.enableUncompressedNativeLibs=false
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
|
@ -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": {
|
||||
|
|
|
@ -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ı",
|
||||
|
|
|
@ -51,7 +51,7 @@
|
|||
"connect_to_server_provider": "Аўтарызавацца ў ",
|
||||
"connect_to_server_provider_text": "З дапамогай API токена праграма SelfPrivacy зможа ад вашага імя замовіць і наладзіць сервер",
|
||||
"steps": {
|
||||
"nixos_installation": "Ўстаноўка NixOS",
|
||||
"nixos_installation": "Ўсталёўка NixOS",
|
||||
|
||||
"hosting": "Хостынг",
|
||||
"server_type": "Тып сервера",
|
||||
"dns_provider": "DNS правайдэр",
|
||||
|
@ -59,7 +59,7 @@
|
|||
"domain": "Дамен",
|
||||
"master_account": "Майстар акаўнт",
|
||||
"server": "Сервер",
|
||||
"dns_setup": "Устаноўка DNS",
|
||||
"dns_setup": "Усталёўка DNS",
|
||||
"server_reboot": "Перазагрузка сервера",
|
||||
"final_checks": "Фінальныя праверкі"
|
||||
},
|
||||
|
@ -100,7 +100,7 @@
|
|||
"modal_confirmation_dns_invalid": "Зваротны DNS паказвае на іншы дамен",
|
||||
"modal_confirmation_ip_invalid": "IP не супадае з паказаным у DNS запісу",
|
||||
"fallback_select_provider_console": "Доступ да кансолі хостынгу.",
|
||||
"provider_connected_description": "Сувязь устаноўлена. Увядзіце свой токен з доступам да {}:",
|
||||
"provider_connected_description": "Сувязь наладжана. Увядзіце свой токен з доступам да {}:",
|
||||
"choose_server": "Выберыце сервер",
|
||||
"no_servers": "На вашым акаўнце няма даступных сэрвэраў.",
|
||||
"modal_confirmation_description": "Падлучэнне да няправільнага сервера можа прывесці да дэструктыўных наступстваў.",
|
||||
|
@ -114,7 +114,7 @@
|
|||
"authorize_new_device": "Аўтарызаваць новую прыладу",
|
||||
"access_granted_on": "Доступ выдадзены {}",
|
||||
"tip": "Націсніце на прыладу, каб адклікаць доступ.",
|
||||
"description": "Гэтыя прылады маюць поўны доступ да кіравання серверам праз прыкладанне SelfPrivacy."
|
||||
"description": "Гэтыя прылады маюць поўны доступ да кіравання серверам праз прыладу SelfPrivacy."
|
||||
},
|
||||
"add_new_device_screen": {
|
||||
"description": "Увядзіце гэты ключ на новай прыладзе:",
|
||||
|
@ -127,7 +127,7 @@
|
|||
"revoke_device_alert": {
|
||||
"header": "Адклікаць доступ?",
|
||||
"yes": "Адклікаць",
|
||||
"no": "Адменіць",
|
||||
"no": "Адхіліць",
|
||||
"description": "Прылада {} больш не зможа кіраваць серверам."
|
||||
}
|
||||
},
|
||||
|
@ -143,7 +143,7 @@
|
|||
"later": "Прапусціць і наладзіць потым",
|
||||
"no_data": "Няма дадзеных",
|
||||
"services": "Сэрвісы",
|
||||
"users": "Ужыткоўнікі",
|
||||
"users": "Карыстальнікі",
|
||||
"more": "Дадаткова",
|
||||
"got_it": "Зразумеў",
|
||||
"settings": "Налады",
|
||||
|
@ -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": "Уладальнікі паказаных тут ключоў атрымліваюць поўны доступ да дадзеных і налад сервера. Дадавайце выключна свае ключы.",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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."
|
||||
|
|
|
@ -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?",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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": "לבחור מה לגבות",
|
||||
|
|
|
@ -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": "Ай",
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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 ключи",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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-ключ?",
|
||||
|
|
|
@ -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密钥",
|
||||
|
|
|
@ -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;
|
||||
inex
commented
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
|
||||
inex
commented
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);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
);
|
||||
}
|
|
@ -1,14 +1,13 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:selfprivacy/logic/bloc/backups/backups_bloc.dart';
|
||||
import 'package:selfprivacy/logic/bloc/connection_status/connection_status_bloc.dart';
|
||||
import 'package:selfprivacy/logic/bloc/connection_status_bloc.dart';
|
||||
import 'package:selfprivacy/logic/bloc/devices/devices_bloc.dart';
|
||||
import 'package:selfprivacy/logic/bloc/recovery_key/recovery_key_bloc.dart';
|
||||
import 'package:selfprivacy/logic/bloc/server_jobs/server_jobs_bloc.dart';
|
||||
import 'package:selfprivacy/logic/bloc/services/services_bloc.dart';
|
||||
import 'package:selfprivacy/logic/bloc/users/users_bloc.dart';
|
||||
import 'package:selfprivacy/logic/bloc/volumes/volumes_bloc.dart';
|
||||
import 'package:selfprivacy/logic/cubit/app_settings/app_settings_cubit.dart';
|
||||
import 'package:selfprivacy/logic/cubit/client_jobs/client_jobs_cubit.dart';
|
||||
import 'package:selfprivacy/logic/cubit/dns_records/dns_records_cubit.dart';
|
||||
import 'package:selfprivacy/logic/cubit/server_detailed_info/server_detailed_info_cubit.dart';
|
||||
|
@ -56,58 +55,46 @@ class BlocAndProviderConfigState extends State<BlocAndProviderConfig> {
|
|||
}
|
||||
|
||||
@override
|
||||
Widget build(final BuildContext context) {
|
||||
const isDark = false;
|
||||
const isAutoDark = true;
|
||||
|
||||
return MultiProvider(
|
||||
providers: [
|
||||
BlocProvider(
|
||||
create: (final _) => AppSettingsCubit(
|
||||
isDarkModeOn: isDark,
|
||||
isAutoDarkModeOn: isAutoDark,
|
||||
isOnboardingShowing: true,
|
||||
)..load(),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (final _) => supportSystemCubit,
|
||||
),
|
||||
BlocProvider(
|
||||
create: (final _) => serverInstallationCubit,
|
||||
lazy: false,
|
||||
),
|
||||
BlocProvider(
|
||||
create: (final _) => usersBloc,
|
||||
lazy: false,
|
||||
),
|
||||
BlocProvider(
|
||||
create: (final _) => servicesBloc,
|
||||
),
|
||||
BlocProvider(
|
||||
create: (final _) => backupsBloc,
|
||||
),
|
||||
BlocProvider(
|
||||
create: (final _) => dnsRecordsCubit,
|
||||
),
|
||||
BlocProvider(
|
||||
create: (final _) => recoveryKeyBloc,
|
||||
),
|
||||
BlocProvider(
|
||||
create: (final _) => devicesBloc,
|
||||
),
|
||||
BlocProvider(
|
||||
create: (final _) => serverJobsBloc,
|
||||
),
|
||||
BlocProvider(create: (final _) => connectionStatusBloc),
|
||||
BlocProvider(
|
||||
create: (final _) => serverDetailsCubit,
|
||||
),
|
||||
BlocProvider(create: (final _) => volumesBloc),
|
||||
BlocProvider(
|
||||
create: (final _) => JobsCubit(),
|
||||
),
|
||||
],
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
Widget build(final BuildContext context) => MultiProvider(
|
||||
providers: [
|
||||
BlocProvider(
|
||||
create: (final _) => supportSystemCubit,
|
||||
),
|
||||
BlocProvider(
|
||||
create: (final _) => serverInstallationCubit,
|
||||
lazy: false,
|
||||
),
|
||||
BlocProvider(
|
||||
create: (final _) => usersBloc,
|
||||
lazy: false,
|
||||
),
|
||||
BlocProvider(
|
||||
create: (final _) => servicesBloc,
|
||||
),
|
||||
BlocProvider(
|
||||
create: (final _) => backupsBloc,
|
||||
),
|
||||
BlocProvider(
|
||||
create: (final _) => dnsRecordsCubit,
|
||||
),
|
||||
BlocProvider(
|
||||
create: (final _) => recoveryKeyBloc,
|
||||
),
|
||||
BlocProvider(
|
||||
create: (final _) => devicesBloc,
|
||||
),
|
||||
BlocProvider(
|
||||
create: (final _) => serverJobsBloc,
|
||||
),
|
||||
BlocProvider(create: (final _) => connectionStatusBloc),
|
||||
BlocProvider(
|
||||
create: (final _) => serverDetailsCubit,
|
||||
),
|
||||
BlocProvider(create: (final _) => volumesBloc),
|
||||
BlocProvider(
|
||||
create: (final _) => JobsCubit(),
|
||||
),
|
||||
],
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -3,40 +3,72 @@ import 'package:flutter/material.dart';
|
|||
|
||||
class Localization extends StatelessWidget {
|
||||
const Localization({
|
||||
required this.child,
|
||||
super.key,
|
||||
this.child,
|
||||
});
|
||||
|
||||
final Widget? child;
|
||||
// when adding new locale, add corresponding native language name to mapper
|
||||
// below
|
||||
static const supportedLocales = [
|
||||
Locale('ar'),
|
||||
Locale('az'),
|
||||
Locale('be'),
|
||||
Locale('cs'),
|
||||
Locale('de'),
|
||||
Locale('en'),
|
||||
Locale('es'),
|
||||
Locale('et'),
|
||||
Locale('fr'),
|
||||
Locale('he'),
|
||||
Locale('kk'),
|
||||
Locale('lv'),
|
||||
Locale('mk'),
|
||||
Locale('pl'),
|
||||
Locale('ru'),
|
||||
Locale('sk'),
|
||||
Locale('sl'),
|
||||
Locale('th'),
|
||||
Locale('uk'),
|
||||
Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hans'),
|
||||
];
|
||||
|
||||
// https://en.wikipedia.org/wiki/IETF_language_tag#List_of_common_primary_language_subtags
|
||||
static final _languageNames = {
|
||||
const Locale('ar'): 'العربية',
|
||||
const Locale('az'): 'Azərbaycan',
|
||||
const Locale('be'): 'беларуская',
|
||||
const Locale('cs'): 'čeština',
|
||||
const Locale('de'): 'Deutsch',
|
||||
const Locale('en'): 'English',
|
||||
const Locale('es'): 'español',
|
||||
const Locale('et'): 'eesti',
|
||||
const Locale('fr'): 'français',
|
||||
const Locale('he'): 'עברית',
|
||||
const Locale('kk'): 'Қазақша',
|
||||
const Locale('lv'): 'latviešu',
|
||||
const Locale('mk'): 'македонски јазик',
|
||||
const Locale('pl'): 'polski',
|
||||
const Locale('ru'): 'русский',
|
||||
const Locale('sk'): 'slovenčina',
|
||||
const Locale('sl'): 'slovenski',
|
||||
const Locale('th'): 'ไทย',
|
||||
const Locale('uk'): 'Українська',
|
||||
const Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hans'): '中文',
|
||||
};
|
||||
|
||||
static String getLanguageName(final Locale locale) =>
|
||||
_languageNames[locale] ?? locale.languageCode;
|
||||
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(final BuildContext context) => EasyLocalization(
|
||||
supportedLocales: const [
|
||||
Locale('ar'),
|
||||
Locale('az'),
|
||||
Locale('be'),
|
||||
Locale('cs'),
|
||||
Locale('de'),
|
||||
Locale('en'),
|
||||
Locale('es'),
|
||||
Locale('et'),
|
||||
Locale('fr'),
|
||||
Locale('he'),
|
||||
Locale('kk'),
|
||||
Locale('lv'),
|
||||
Locale('mk'),
|
||||
Locale('pl'),
|
||||
Locale('ru'),
|
||||
Locale('sk'),
|
||||
Locale('sl'),
|
||||
Locale('th'),
|
||||
Locale('uk'),
|
||||
Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hans'),
|
||||
],
|
||||
supportedLocales: supportedLocales,
|
||||
path: 'assets/translations',
|
||||
fallbackLocale: const Locale('en'),
|
||||
useFallbackTranslations: true,
|
||||
saveLocale: false,
|
||||
useOnlyLangCode: false,
|
||||
child: child!,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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,
|
||||
);
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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,13 +16,14 @@ class RequestLoggingLink extends Link {
|
|||
final Request request, [
|
||||
final NextLink? forward,
|
||||
]) async* {
|
||||
getIt.get<ConsoleModel>().addMessage(
|
||||
GraphQlRequestMessage(
|
||||
operation: request.operation,
|
||||
variables: request.variables,
|
||||
context: request.context,
|
||||
),
|
||||
);
|
||||
_addConsoleLog(
|
||||
GraphQlRequestConsoleLog(
|
||||
// context: request.context,
|
||||
operationType: request.type.name,
|
||||
operation: request.operation,
|
||||
variables: request.variables,
|
||||
),
|
||||
);
|
||||
yield* forward!(request);
|
||||
}
|
||||
}
|
||||
|
@ -35,20 +32,26 @@ class ResponseLoggingParser extends ResponseParser {
|
|||
@override
|
||||
Response parseResponse(final Map<String, dynamic> body) {
|
||||
final response = super.parseResponse(body);
|
||||
getIt.get<ConsoleModel>().addMessage(
|
||||
GraphQlResponseMessage(
|
||||
data: response.data,
|
||||
errors: response.errors,
|
||||
context: response.context,
|
||||
),
|
||||
);
|
||||
_addConsoleLog(
|
||||
GraphQlResponseConsoleLog(
|
||||
// context: response.context,
|
||||
data: response.data,
|
||||
errors: response.errors,
|
||||
rawResponse: jsonEncode(response.response),
|
||||
),
|
||||
);
|
||||
return response;
|
||||
}
|
||||
|
||||
@override
|
||||
GraphQLError parseError(final Map<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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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];
|
||||
}
|
|
@ -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];
|
||||
}
|
|
@ -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
|
||||
inex
commented
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.
misterfourtytwo
commented
Review
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> {
|
||||
inex
commented
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?
misterfourtytwo
commented
Review
why won't it? 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();
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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];
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
inex
commented
Review
Why not using the same optimization as with Why not using the same optimization as with `_incomingQueue`?
misterfourtytwo
commented
Review
my guess was that buffer of 500 messages was a console feature, so I left it as is.
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();
|
||||
}
|
||||
}
|
|
@ -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';
|
||||
}
|
|
@ -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';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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?,
|
||||
);
|
||||
|
||||
|
|
|
@ -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(),
|
||||
);
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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?,
|
||||
);
|
||||
|
|
|
@ -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) =>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
|
||||
|
|
174
lib/main.dart
|
@ -2,28 +2,20 @@ import 'package:easy_localization/easy_localization.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:selfprivacy/config/app_controller/inherited_app_controller.dart';
|
||||
import 'package:selfprivacy/config/bloc_config.dart';
|
||||
import 'package:selfprivacy/config/bloc_observer.dart';
|
||||
import 'package:selfprivacy/config/brand_colors.dart';
|
||||
import 'package:selfprivacy/config/get_it_config.dart';
|
||||
import 'package:selfprivacy/config/hive_config.dart';
|
||||
import 'package:selfprivacy/config/localization.dart';
|
||||
import 'package:selfprivacy/logic/cubit/app_settings/app_settings_cubit.dart';
|
||||
import 'package:selfprivacy/theming/factory/app_theme_factory.dart';
|
||||
import 'package:selfprivacy/config/preferences_repository/datasources/preferences_hive_datasource.dart';
|
||||
import 'package:selfprivacy/config/preferences_repository/inherited_preferences_repository.dart';
|
||||
import 'package:selfprivacy/ui/pages/errors/failed_to_init_secure_storage.dart';
|
||||
import 'package:selfprivacy/ui/router/router.dart';
|
||||
// import 'package:wakelock/wakelock.dart';
|
||||
import 'package:timezone/data/latest.dart' as tz;
|
||||
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
try {
|
||||
await HiveConfig.init();
|
||||
} on PlatformException catch (e) {
|
||||
runApp(
|
||||
FailedToInitSecureStorageScreen(e: e),
|
||||
);
|
||||
}
|
||||
// await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
|
||||
|
||||
// try {
|
||||
|
@ -34,85 +26,117 @@ void main() async {
|
|||
// print(e);
|
||||
// }
|
||||
|
||||
await getItSetup();
|
||||
await EasyLocalization.ensureInitialized();
|
||||
tz.initializeTimeZones();
|
||||
try {
|
||||
await Future.wait(
|
||||
<Future<void>>[
|
||||
HiveConfig.init(),
|
||||
EasyLocalization.ensureInitialized(),
|
||||
],
|
||||
);
|
||||
await getItSetup();
|
||||
} on PlatformException catch (e) {
|
||||
runApp(
|
||||
FailedToInitSecureStorageScreen(e: e),
|
||||
);
|
||||
}
|
||||
|
||||
final ThemeData lightThemeData = await AppThemeFactory.create(
|
||||
isDark: false,
|
||||
fallbackColor: BrandColors.primary,
|
||||
);
|
||||
final ThemeData darkThemeData = await AppThemeFactory.create(
|
||||
isDark: true,
|
||||
fallbackColor: BrandColors.primary,
|
||||
);
|
||||
tz.initializeTimeZones();
|
||||
|
||||
Bloc.observer = SimpleBlocObserver();
|
||||
|
||||
runApp(
|
||||
Localization(
|
||||
child: SelfprivacyApp(
|
||||
lightThemeData: lightThemeData,
|
||||
darkThemeData: darkThemeData,
|
||||
child: InheritedPreferencesRepository(
|
||||
dataSource: PreferencesHiveDataSource(),
|
||||
child: const InheritedAppController(
|
||||
child: AppBuilder(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class SelfprivacyApp extends StatelessWidget {
|
||||
SelfprivacyApp({
|
||||
required this.lightThemeData,
|
||||
required this.darkThemeData,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final ThemeData lightThemeData;
|
||||
final ThemeData darkThemeData;
|
||||
|
||||
final _appRouter = RootRouter(getIt.get<NavigationService>().navigatorKey);
|
||||
class AppBuilder extends StatelessWidget {
|
||||
const AppBuilder({super.key});
|
||||
|
||||
@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(
|
||||
routeInformationParser: _appRouter.defaultRouteParser(),
|
||||
routerDelegate: _appRouter.delegate(),
|
||||
scaffoldMessengerKey:
|
||||
getIt.get<NavigationService>().scaffoldMessengerKey,
|
||||
localizationsDelegates: context.localizationDelegates,
|
||||
supportedLocales: context.supportedLocales,
|
||||
locale: context.locale,
|
||||
debugShowCheckedModeBanner: false,
|
||||
title: 'SelfPrivacy',
|
||||
theme: lightThemeData,
|
||||
darkTheme: darkThemeData,
|
||||
themeMode: appSettings.isAutoDarkModeOn
|
||||
? ThemeMode.system
|
||||
: appSettings.isDarkModeOn
|
||||
? ThemeMode.dark
|
||||
: ThemeMode.light,
|
||||
builder: (final BuildContext context, final Widget? widget) {
|
||||
Widget error =
|
||||
const Center(child: Text('...rendering error...'));
|
||||
if (widget is Scaffold || widget is Navigator) {
|
||||
error = Scaffold(body: error);
|
||||
}
|
||||
ErrorWidget.builder =
|
||||
(final FlutterErrorDetails errorDetails) => error;
|
||||
Widget build(final BuildContext context) {
|
||||
final appController = InheritedAppController.of(context);
|
||||
|
||||
return widget ?? error;
|
||||
},
|
||||
);
|
||||
},
|
||||
if (appController.loaded) {
|
||||
return const SelfprivacyApp();
|
||||
}
|
||||
|
||||
return const SplashScreen();
|
||||
}
|
||||
}
|
||||
|
||||
/// Widget to be shown
|
||||
/// until essential app initialization is completed
|
||||
class SplashScreen extends StatelessWidget {
|
||||
const SplashScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(final BuildContext context) => const ColoredBox(
|
||||
color: Colors.white,
|
||||
child: Center(
|
||||
child: CircularProgressIndicator.adaptive(
|
||||
valueColor: AlwaysStoppedAnimation(BrandColors.primary),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class SelfprivacyApp extends StatefulWidget {
|
||||
const SelfprivacyApp({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<SelfprivacyApp> createState() => _SelfprivacyAppState();
|
||||
}
|
||||
|
||||
class _SelfprivacyAppState extends State<SelfprivacyApp> {
|
||||
final appKey = UniqueKey();
|
||||
final _appRouter = RootRouter(getIt.get<NavigationService>().navigatorKey);
|
||||
|
||||
@override
|
||||
Widget build(final BuildContext context) {
|
||||
final appController = InheritedAppController.of(context);
|
||||
|
||||
return BlocAndProviderConfig(
|
||||
child: MaterialApp.router(
|
||||
key: appKey,
|
||||
title: 'SelfPrivacy',
|
||||
// routing
|
||||
routeInformationParser: _appRouter.defaultRouteParser(),
|
||||
routerDelegate: _appRouter.delegate(),
|
||||
scaffoldMessengerKey:
|
||||
getIt.get<NavigationService>().scaffoldMessengerKey,
|
||||
// localization settings
|
||||
locale: context.locale,
|
||||
supportedLocales: context.supportedLocales,
|
||||
localizationsDelegates: context.localizationDelegates,
|
||||
// theme settings
|
||||
themeMode: appController.themeMode,
|
||||
theme: appController.lightTheme,
|
||||
darkTheme: appController.darkTheme,
|
||||
// other preferences
|
||||
debugShowCheckedModeBanner: false,
|
||||
scrollBehavior:
|
||||
const MaterialScrollBehavior().copyWith(scrollbars: false),
|
||||
builder: _builder,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _builder(final BuildContext context, final Widget? widget) {
|
||||
Widget error = const Center(child: Text('...rendering error...'));
|
||||
if (widget is Scaffold || widget is Navigator) {
|
||||
error = Scaffold(body: error);
|
||||
}
|
||||
ErrorWidget.builder = (final FlutterErrorDetails errorDetails) => error;
|
||||
|
||||
return widget ?? error;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:selfprivacy/ui/components/not_ready_card/not_ready_card.dart';
|
||||
|
||||
class EmptyPagePlaceholder extends StatelessWidget {
|
||||
|
@ -10,50 +11,72 @@ class EmptyPagePlaceholder extends StatelessWidget {
|
|||
super.key,
|
||||
});
|
||||
|
||||
final bool showReadyCard;
|
||||
final IconData iconData;
|
||||
final String title;
|
||||
final String description;
|
||||
final IconData iconData;
|
||||
final bool showReadyCard;
|
||||
|
||||
@override
|
||||
Widget build(final BuildContext context) => !showReadyCard
|
||||
? _expandedContent(context)
|
||||
: Column(
|
||||
Widget build(final BuildContext context) => showReadyCard
|
||||
? Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 15),
|
||||
child: NotReadyCard(),
|
||||
),
|
||||
Expanded(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||||
child: Center(
|
||||
child: _expandedContent(context),
|
||||
if (showReadyCard)
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: 15,
|
||||
horizontal: 15,
|
||||
),
|
||||
child: NotReadyCard(),
|
||||
),
|
||||
Expanded(
|
||||
child: _ContentWidget(
|
||||
iconData: iconData,
|
||||
title: title,
|
||||
description: description,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: _ContentWidget(
|
||||
iconData: iconData,
|
||||
title: title,
|
||||
description: description,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _expandedContent(final BuildContext context) => Padding(
|
||||
class _ContentWidget extends StatelessWidget {
|
||||
const _ContentWidget({
|
||||
required this.iconData,
|
||||
required this.title,
|
||||
required this.description,
|
||||
});
|
||||
|
||||
final IconData iconData;
|
||||
final String title;
|
||||
final String description;
|
||||
|
||||
@override
|
||||
Widget build(final BuildContext context) => Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
iconData,
|
||||
size: 50,
|
||||
color: Theme.of(context).colorScheme.onBackground,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Gap(16),
|
||||
Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onBackground,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Gap(8),
|
||||
Text(
|
||||
description,
|
||||
textAlign: TextAlign.center,
|
||||
|
|
|
@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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()),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
|
@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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()],
|
||||
);
|
||||
}
|
|
@ -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,
|
||||
);
|
||||
}
|
|
@ -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]);
|
||||
};
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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(
|
||||
hasBackButton: true,
|
||||
hasFlashButton: false,
|
||||
bodyPadding: const EdgeInsets.symmetric(vertical: 16),
|
||||
heroTitle: 'application_settings.title'.tr(),
|
||||
children: [
|
||||
SwitchListTile.adaptive(
|
||||
title: Text('application_settings.system_dark_theme_title'.tr()),
|
||||
subtitle:
|
||||
Text('application_settings.system_dark_theme_description'.tr()),
|
||||
value: isSystemDarkModeOn,
|
||||
onChanged: (final value) => context
|
||||
.read<AppSettingsCubit>()
|
||||
.updateAutoDarkMode(isAutoDarkModeOn: !isSystemDarkModeOn),
|
||||
Widget build(final BuildContext context) => BrandHeroScreen(
|
||||
hasBackButton: true,
|
||||
hasFlashButton: false,
|
||||
bodyPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 16,
|
||||
),
|
||||
SwitchListTile.adaptive(
|
||||
title: Text('application_settings.dark_theme_title'.tr()),
|
||||
subtitle: Text('application_settings.dark_theme_description'.tr()),
|
||||
value: Theme.of(context).brightness == Brightness.dark,
|
||||
onChanged: isSystemDarkModeOn
|
||||
? null
|
||||
: (final value) => context
|
||||
.read<AppSettingsCubit>()
|
||||
.updateDarkMode(isDarkModeOn: !isDarkModeOn),
|
||||
),
|
||||
const Divider(height: 0),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Text(
|
||||
'application_settings.dangerous_settings'.tr(),
|
||||
style: Theme.of(context).textTheme.labelLarge!.copyWith(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
heroTitle: 'application_settings.title'.tr(),
|
||||
children: [
|
||||
_ThemePicker(
|
||||
key: ValueKey('theme_picker'.tr()),
|
||||
),
|
||||
),
|
||||
const _ResetAppTile(),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ResetAppTile extends StatelessWidget {
|
||||
const _ResetAppTile();
|
||||
|
||||
@override
|
||||
Widget build(final BuildContext context) => ListTile(
|
||||
title: Text('application_settings.reset_config_title'.tr()),
|
||||
subtitle: Text('application_settings.reset_config_description'.tr()),
|
||||
onTap: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (final _) => AlertDialog(
|
||||
title: Text('modals.are_you_sure'.tr()),
|
||||
content: Text('modals.purge_all_keys'.tr()),
|
||||
actions: [
|
||||
DialogActionButton(
|
||||
text: 'modals.purge_all_keys_confirm'.tr(),
|
||||
isRed: true,
|
||||
onPressed: () {
|
||||
context.read<ServerInstallationCubit>().clearAppConfig();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
DialogActionButton(
|
||||
text: 'basis.cancel'.tr(),
|
||||
),
|
||||
],
|
||||
const Divider(height: 5, thickness: 0),
|
||||
_LanguagePicker(
|
||||
key: ValueKey('language_picker'.tr()),
|
||||
),
|
||||
const Divider(height: 5, thickness: 0),
|
||||
const Gap(4),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Text(
|
||||
'application_settings.dangerous_settings'.tr(),
|
||||
style: Theme.of(context).textTheme.titleLarge!.copyWith(
|
||||
inex
commented
Review
This should be a This should be a `labelLarge`, looks broken as a `titleLarge`.
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
_ResetAppTile(
|
||||
key: ValueKey('reset_app'.tr()),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
inex
commented
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
|
||||
inex
commented
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
|
||||
inex
commented
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);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
|
@ -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(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
|
@ -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(
|
||||
inex
commented
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,
|
||||
// ),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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(),
|
||||
],
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
|
@ -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) ...[
|
||||
inex
commented
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)
misterfourtytwo
commented
Review
so you propose to remove headers section or to hide it? so you propose to remove headers section or to hide it?
inex
commented
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),
|
||||
),
|
||||
);
|
||||
}
|
|
@ -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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
|
@ -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(),
|
||||
);
|
||||
}
|
|
@ -21,11 +21,8 @@ class MorePage extends StatelessWidget {
|
|||
|
||||
return Scaffold(
|
||||
appBar: Breakpoints.small.isActive(context)
|
||||
? PreferredSize(
|
||||
preferredSize: const Size.fromHeight(52),
|
||||
child: BrandHeader(
|
||||
title: 'basis.more'.tr(),
|
||||
),
|
||||
? BrandHeader(
|
||||
title: 'basis.more'.tr(),
|
||||
)
|
||||
: null,
|
||||
body: ListView(
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -65,11 +65,8 @@ class _ProvidersPageState extends State<ProvidersPage> {
|
|||
|
||||
return Scaffold(
|
||||
appBar: Breakpoints.small.isActive(context)
|
||||
? PreferredSize(
|
||||
preferredSize: const Size.fromHeight(52),
|
||||
child: BrandHeader(
|
||||
title: 'basis.providers_title'.tr(),
|
||||
),
|
||||
? BrandHeader(
|
||||
title: 'basis.providers_title'.tr(),
|
||||
)
|
||||
: null,
|
||||
body: ListView(
|
||||
|
|
|
@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,11 +37,8 @@ class _ServicesPageState extends State<ServicesPage> {
|
|||
|
||||
return Scaffold(
|
||||
appBar: Breakpoints.small.isActive(context)
|
||||
? PreferredSize(
|
||||
preferredSize: const Size.fromHeight(52),
|
||||
child: BrandHeader(
|
||||
title: 'basis.services'.tr(),
|
||||
),
|
||||
? BrandHeader(
|
||||
title: 'basis.services'.tr(),
|
||||
)
|
||||
: null,
|
||||
body: !isReady
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -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),
|
||||
// ];
|
||||
NaiJi
commented
Review
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
misterfourtytwo
commented
Review
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),
|
||||
|
|
|
@ -129,11 +129,8 @@ class UsersPage extends StatelessWidget {
|
|||
|
||||
return Scaffold(
|
||||
appBar: Breakpoints.small.isActive(context)
|
||||
? PreferredSize(
|
||||
preferredSize: const Size.fromHeight(52),
|
||||
child: BrandHeader(
|
||||
title: 'basis.users'.tr(),
|
||||
),
|
||||
? BrandHeader(
|
||||
title: 'basis.users'.tr(),
|
||||
)
|
||||
: null,
|
||||
body: child,
|
||||
|
|
|
@ -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',
|
||||
),
|
||||
];
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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"))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
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?