mirror of
https://git.selfprivacy.org/kherel/selfprivacy.org.app.git
synced 2024-11-19 07:09:14 +00:00
Merge branch 'master' into translations
# Conflicts: # assets/markdown/how_fallback_ssh-ja.md # assets/translations/ar.json # assets/translations/az.json # assets/translations/be.json # assets/translations/cs.json # assets/translations/de.json # assets/translations/es.json # assets/translations/et.json # assets/translations/fr.json # assets/translations/kk.json # assets/translations/pl.json # assets/translations/ru.json # assets/translations/sk.json # assets/translations/th.json # assets/translations/uk.json # assets/translations/zh-Hans.json
This commit is contained in:
commit
8318a7a7eb
53
.vscode/launch.json
vendored
Normal file
53
.vscode/launch.json
vendored
Normal file
|
@ -0,0 +1,53 @@
|
|||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "debug",
|
||||
"request": "launch",
|
||||
"type": "dart"
|
||||
},
|
||||
{
|
||||
"name": "profile mode",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"flutterMode": "profile"
|
||||
},
|
||||
{
|
||||
"name": "release mode",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"flutterMode": "release"
|
||||
},
|
||||
{
|
||||
"name": "debug (fdroid)",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"args": [
|
||||
"--flavor",
|
||||
"fdroid"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "debug (production)",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"args": [
|
||||
"--flavor",
|
||||
"production"
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
"name": "debug (nightly)",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"args": [
|
||||
"--flavor",
|
||||
"nightly"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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"
|
|
@ -1,19 +1,7 @@
|
|||
Login as root user to your server and look at the contents of the file `/etc/nixos/userdata/tokens.json`
|
||||
[Login as root user to your server](https://selfprivacy.org/docs/how-to-guides/root_ssh/) and enter this command:
|
||||
|
||||
```sh
|
||||
cat /etc/nixos/userdata/tokens.json
|
||||
sp-print-api-token
|
||||
```
|
||||
|
||||
This file will have a similar construction:
|
||||
|
||||
```json
|
||||
{
|
||||
"tokens": [
|
||||
{
|
||||
"token": "token_to_copy",
|
||||
"name": "device_name",
|
||||
"date": "date"
|
||||
}
|
||||
```
|
||||
|
||||
Copy the token from the file and paste it in the next window.
|
||||
Copy the token from the terminal and paste it in the next window.
|
||||
|
|
|
@ -1,19 +1,7 @@
|
|||
Login as root user to your server and look at the contents of the file `/etc/nixos/userdata/tokens.json`
|
||||
[Login as root user to your server](https://selfprivacy.org/docs/how-to-guides/root_ssh/) and enter this command:
|
||||
|
||||
```sh
|
||||
cat /etc/nixos/userdata/tokens.json
|
||||
sp-print-api-token
|
||||
```
|
||||
|
||||
This file will have a similar construction:
|
||||
|
||||
```json
|
||||
{
|
||||
"tokens": [
|
||||
{
|
||||
"token": "token_to_copy",
|
||||
"name": "device_name",
|
||||
"date": "date"
|
||||
}
|
||||
```
|
||||
|
||||
Copy the token from the file and paste it in the next window.
|
||||
Copy the token from the terminal and paste it in the next window.
|
||||
|
|
|
@ -1,19 +1,7 @@
|
|||
Login as root user to your server and look at the contents of the file `/etc/nixos/userdata/tokens.json`
|
||||
[Login as root user to your server](https://selfprivacy.org/docs/how-to-guides/root_ssh/) and enter this command:
|
||||
|
||||
```sh
|
||||
cat /etc/nixos/userdata/tokens.json
|
||||
sp-print-api-token
|
||||
```
|
||||
|
||||
This file will have a similar construction:
|
||||
|
||||
```json
|
||||
{
|
||||
"tokens": [
|
||||
{
|
||||
"token": "token_to_copy",
|
||||
"name": "device_name",
|
||||
"date": "date"
|
||||
}
|
||||
```
|
||||
|
||||
Copy the token from the file and paste it in the next window.
|
||||
Copy the token from the terminal and paste it in the next window.
|
||||
|
|
|
@ -1,19 +1,7 @@
|
|||
Login as root user to your server and look at the contents of the file `/etc/nixos/userdata/tokens.json`
|
||||
[Login as root user to your server](https://selfprivacy.org/docs/how-to-guides/root_ssh/) and enter this command:
|
||||
|
||||
```sh
|
||||
cat /etc/nixos/userdata/tokens.json
|
||||
sp-print-api-token
|
||||
```
|
||||
|
||||
This file will have a similar construction:
|
||||
|
||||
```json
|
||||
{
|
||||
"tokens": [
|
||||
{
|
||||
"token": "token_to_copy",
|
||||
"name": "device_name",
|
||||
"date": "date"
|
||||
}
|
||||
```
|
||||
|
||||
Copy the token from the file and paste it in the next window.
|
||||
Copy the token from the terminal and paste it in the next window.
|
||||
|
|
|
@ -1,19 +1,7 @@
|
|||
Login as root user to your server and look at the contents of the file `/etc/nixos/userdata/tokens.json`
|
||||
[Login as root user to your server](https://selfprivacy.org/docs/how-to-guides/root_ssh/) and enter this command:
|
||||
|
||||
```sh
|
||||
cat /etc/nixos/userdata/tokens.json
|
||||
sp-print-api-token
|
||||
```
|
||||
|
||||
This file will have a similar construction:
|
||||
|
||||
```json
|
||||
{
|
||||
"tokens": [
|
||||
{
|
||||
"token": "token_to_copy",
|
||||
"name": "device_name",
|
||||
"date": "date"
|
||||
}
|
||||
```
|
||||
|
||||
Copy the token from the file and paste it in the next window.
|
||||
Copy the token from the terminal and paste it in the next window.
|
||||
|
|
|
@ -1,19 +1,7 @@
|
|||
Login as root user to your server and look at the contents of the file `/etc/nixos/userdata/tokens.json`
|
||||
[Login as root user to your server](https://selfprivacy.org/docs/how-to-guides/root_ssh/) and enter this command:
|
||||
|
||||
```sh
|
||||
cat /etc/nixos/userdata/tokens.json
|
||||
sp-print-api-token
|
||||
```
|
||||
|
||||
This file will have a similar construction:
|
||||
|
||||
```json
|
||||
{
|
||||
"tokens": [
|
||||
{
|
||||
"token": "token_to_copy",
|
||||
"name": "device_name",
|
||||
"date": "date"
|
||||
}
|
||||
```
|
||||
|
||||
Copy the token from the file and paste it in the next window.
|
||||
Copy the token from the terminal and paste it in the next window.
|
||||
|
|
|
@ -1,19 +1,7 @@
|
|||
Login as root user to your server and look at the contents of the file `/etc/nixos/userdata/tokens.json`
|
||||
[Login as root user to your server](https://selfprivacy.org/docs/how-to-guides/root_ssh/) and enter this command:
|
||||
|
||||
```sh
|
||||
cat /etc/nixos/userdata/tokens.json
|
||||
sp-print-api-token
|
||||
```
|
||||
|
||||
This file will have a similar construction:
|
||||
|
||||
```json
|
||||
{
|
||||
"tokens": [
|
||||
{
|
||||
"token": "token_to_copy",
|
||||
"name": "device_name",
|
||||
"date": "date"
|
||||
}
|
||||
```
|
||||
|
||||
Copy the token from the file and paste it in the next window.
|
||||
Copy the token from the terminal and paste it in the next window.
|
||||
|
|
|
@ -173,6 +173,7 @@
|
|||
"destroy_server": "هل تريد إنهاء هذا الخادم وإنشاء واحد جديد؟",
|
||||
"try_again": "هل تريد المحاولة مرة أخرى؟",
|
||||
"purge_all_keys": "هل تريد محو جميع مفاتيح الّتي مُنحت الموافقة؟",
|
||||
"delete_server_volume": "هل تريد حذف الخادم ووحدة التخزين؟",
|
||||
"reboot": "قم بإعادة التشغيل",
|
||||
"yes": "نعم",
|
||||
"no": "لا"
|
||||
|
@ -332,12 +333,12 @@
|
|||
},
|
||||
"application_settings": {
|
||||
"title": "إعدادات التطبيق",
|
||||
"system_dark_theme_title": "الوضع الافتراضي للنظام",
|
||||
"system_dark_theme_description": "قم بتطبيق الوضع الفاتح أو الداكن حسب إعدادات النظام",
|
||||
"system_theme_mode_title": "الوضع الافتراضي للنظام",
|
||||
"system_theme_mode_description": "قم بتطبيق الوضع الفاتح أو الداكن حسب إعدادات النظام",
|
||||
"dark_theme_title": "الوضع الداكن",
|
||||
"change_application_theme": "قم بتبديل وضع التطبيق",
|
||||
"dangerous_settings": "إعدادات خطرة",
|
||||
"reset_config_title": "قم بإعادة ضبط إعدادات التطبيق",
|
||||
"dark_theme_description": "قم بتبديل وضع التطبيق",
|
||||
"reset_config_description": "قم بإعادة ضبط مفاتيح API والمستخدم المميز."
|
||||
},
|
||||
"ssh": {
|
||||
|
|
|
@ -54,13 +54,15 @@
|
|||
},
|
||||
"application_settings": {
|
||||
"title": "Tətbiq parametrləri",
|
||||
"system_theme_mode_title": "Defolt sistem mövzusu",
|
||||
"system_theme_mode_description": "Sistem parametrlərindən asılı olaraq açıq və ya qaranlıq mövzudan istifadə edin",
|
||||
"dark_theme_title": "Qaranlıq mövzu",
|
||||
"change_application_theme": "Rəng mövzusunu dəyişdirin",
|
||||
"dangerous_settings": "Təhlükəli Parametrlər",
|
||||
"reset_config_title": "Tətbiq Sıfırlayın",
|
||||
"reset_config_description": "API və Super İstifadəçi Açarlarını sıfırlayın.",
|
||||
"dark_theme_description": "Rəng mövzusunu dəyişdirin",
|
||||
"system_dark_theme_title": "Defolt sistem mövzusu",
|
||||
"system_dark_theme_description": "Sistem parametrlərindən asılı olaraq açıq və ya qaranlıq mövzudan istifadə edin",
|
||||
"dangerous_settings": "Təhlükəli Parametrlər"
|
||||
"reset_config_description": "API və Super İstifadəçi Açarlarını sıfırlayın."
|
||||
|
||||
|
||||
},
|
||||
"ssh": {
|
||||
"title": "SSH açarları",
|
||||
|
@ -379,6 +381,7 @@
|
|||
"are_you_sure": "Sən əminsən?",
|
||||
"purge_all_keys": "Bütün avtorizasiya açarları silinsin?",
|
||||
"purge_all_keys_confirm": "Bəli, bütün düymələri silin",
|
||||
"delete_server_volume": "Server və yaddaş silinsin?",
|
||||
"reboot": "Yenidən yükləyin",
|
||||
"yes": "Bəli",
|
||||
"no": "Yox"
|
||||
|
|
|
@ -51,7 +51,7 @@
|
|||
"connect_to_server_provider": "Аўтарызавацца ў ",
|
||||
"connect_to_server_provider_text": "З дапамогай API токена праграма SelfPrivacy зможа ад вашага імя замовіць і наладзіць сервер",
|
||||
"steps": {
|
||||
"nixos_installation": "Ўстаноўка NixOS",
|
||||
"nixos_installation": "Ўсталёўка NixOS",
|
||||
"hosting": "Хостынг",
|
||||
"server_type": "Тып сервера",
|
||||
"dns_provider": "DNS правайдэр",
|
||||
|
@ -59,7 +59,7 @@
|
|||
"domain": "Дамен",
|
||||
"master_account": "Майстар акаўнт",
|
||||
"server": "Сервер",
|
||||
"dns_setup": "Устаноўка DNS",
|
||||
"dns_setup": "Усталёўка DNS",
|
||||
"server_reboot": "Перазагрузка сервера",
|
||||
"final_checks": "Фінальныя праверкі"
|
||||
},
|
||||
|
@ -100,7 +100,7 @@
|
|||
"modal_confirmation_dns_invalid": "Зваротны DNS паказвае на іншы дамен",
|
||||
"modal_confirmation_ip_invalid": "IP не супадае з паказаным у DNS запісу",
|
||||
"fallback_select_provider_console": "Доступ да кансолі хостынгу.",
|
||||
"provider_connected_description": "Сувязь устаноўлена. Увядзіце свой токен з доступам да {}:",
|
||||
"provider_connected_description": "Сувязь наладжана. Увядзіце свой токен з доступам да {}:",
|
||||
"choose_server": "Выберыце сервер",
|
||||
"no_servers": "На вашым акаўнце няма даступных сэрвэраў.",
|
||||
"modal_confirmation_description": "Падлучэнне да няправільнага сервера можа прывесці да дэструктыўных наступстваў.",
|
||||
|
@ -114,7 +114,7 @@
|
|||
"authorize_new_device": "Аўтарызаваць новую прыладу",
|
||||
"access_granted_on": "Доступ выдадзены {}",
|
||||
"tip": "Націсніце на прыладу, каб адклікаць доступ.",
|
||||
"description": "Гэтыя прылады маюць поўны доступ да кіравання серверам праз прыкладанне SelfPrivacy."
|
||||
"description": "Гэтыя прылады маюць поўны доступ да кіравання серверам праз прыладу SelfPrivacy."
|
||||
},
|
||||
"add_new_device_screen": {
|
||||
"description": "Увядзіце гэты ключ на новай прыладзе:",
|
||||
|
@ -127,7 +127,7 @@
|
|||
"revoke_device_alert": {
|
||||
"header": "Адклікаць доступ?",
|
||||
"yes": "Адклікаць",
|
||||
"no": "Адменіць",
|
||||
"no": "Адхіліць",
|
||||
"description": "Прылада {} больш не зможа кіраваць серверам."
|
||||
}
|
||||
},
|
||||
|
@ -143,7 +143,7 @@
|
|||
"later": "Прапусціць і наладзіць потым",
|
||||
"no_data": "Няма дадзеных",
|
||||
"services": "Сэрвісы",
|
||||
"users": "Ужыткоўнікі",
|
||||
"users": "Карыстальнікі",
|
||||
"more": "Дадаткова",
|
||||
"got_it": "Зразумеў",
|
||||
"settings": "Налады",
|
||||
|
@ -205,6 +205,7 @@
|
|||
"dns_removal_error": "Немагчыма выдаліць запісы DNS.",
|
||||
"server_deletion_error": "Немагчыма выдаліць сервер.",
|
||||
"unexpected_error": "Непрадбачаная памылка з боку правайдэра.",
|
||||
"delete_server_volume": "Выдаліць сервер і сховішча?",
|
||||
"volume_creation_error": "Не ўдалося стварыць том."
|
||||
},
|
||||
"timer": {
|
||||
|
@ -235,7 +236,7 @@
|
|||
},
|
||||
"more_page": {
|
||||
"configuration_wizard": "Майстар наладкі",
|
||||
"onboarding": "Прівітанне",
|
||||
"onboarding": "Прывітанне",
|
||||
"create_ssh_key": "SSH ключы адміністратара"
|
||||
},
|
||||
"about_application_page": {
|
||||
|
@ -265,12 +266,15 @@
|
|||
"application_settings": {
|
||||
"reset_config_description": "Скінуць API ключы i суперкарыстальніка.",
|
||||
"title": "Налады праграмы",
|
||||
"system_theme_mode_title": "Сістэмная тэма па-змаўчанні",
|
||||
"system_theme_mode_description": "Выкарыстоўвайце светлую ці цёмную тэмы ў залежнасці ад сістэмных налад",
|
||||
"dark_theme_title": "Цёмная тэма",
|
||||
"dark_theme_description": "Змяніць каляровую тэму",
|
||||
"change_application_theme": "Змяніць каляровую тэму",
|
||||
"language": "Мова",
|
||||
"click_to_change_locale": "Націсніце, каб адчыніць меню выбару мовы",
|
||||
"dangerous_settings": "Небяспечныя налады",
|
||||
"reset_config_title": "Скід налад",
|
||||
"system_dark_theme_title": "Сістэмная тэма па-змаўчанні",
|
||||
"system_dark_theme_description": "Выкарыстоўвайце светлую ці цёмную тэмы ў залежнасці ад сістэмных налад",
|
||||
"dangerous_settings": "Небяспечныя наладкі"
|
||||
"reset_config_description": "Скінуць API ключы i суперкарыстальніка."
|
||||
},
|
||||
"ssh": {
|
||||
"root_subtitle": "Уладальнікі паказаных тут ключоў атрымліваюць поўны доступ да дадзеных і налад сервера. Дадавайце выключна свае ключы.",
|
||||
|
|
|
@ -54,13 +54,13 @@
|
|||
},
|
||||
"application_settings": {
|
||||
"title": "Nastavení aplikace",
|
||||
"system_theme_mode_title": "Výchozí téma systému",
|
||||
"system_theme_mode_description": "Použití světlého nebo tmavého motivu v závislosti na nastavení systému",
|
||||
"dark_theme_title": "Tmavé téma",
|
||||
"change_application_theme": "Přepnutí tématu aplikace",
|
||||
"dangerous_settings": "Nebezpečná nastavení",
|
||||
"reset_config_title": "Obnovení konfigurace aplikace",
|
||||
"reset_config_description": "Obnovení klíčů API a uživatele root.",
|
||||
"dark_theme_description": "Přepnutí tématu aplikace",
|
||||
"system_dark_theme_title": "Výchozí téma systému",
|
||||
"system_dark_theme_description": "Použití světlého nebo tmavého motivu v závislosti na nastavení systému",
|
||||
"dangerous_settings": "Nebezpečná nastavení"
|
||||
"reset_config_description": "Obnovení klíčů API a uživatele root."
|
||||
},
|
||||
"ssh": {
|
||||
"title": "Klíče SSH",
|
||||
|
|
|
@ -57,13 +57,13 @@
|
|||
},
|
||||
"application_settings": {
|
||||
"title": "Anwendungseinstellungen",
|
||||
"system_theme_mode_title": "Standard-Systemthema",
|
||||
"system_theme_mode_description": "Verwenden Sie je nach Systemeinstellungen ein helles oder dunkles Thema",
|
||||
"dark_theme_title": "Dunkles Thema",
|
||||
"dark_theme_description": "Ihr Anwendungsdesign wechseln",
|
||||
"change_application_theme": "Ihr Anwendungsdesign wechseln",
|
||||
"dangerous_settings": "Gefährliche Einstellungen",
|
||||
"reset_config_title": "Anwendungseinstellungen zurücksetzen",
|
||||
"reset_config_description": "API Sclüssel und root Benutzer zurücksetzen.",
|
||||
"system_dark_theme_title": "Standard-Systemthema",
|
||||
"system_dark_theme_description": "Verwenden Sie je nach Systemeinstellungen ein helles oder dunkles Thema",
|
||||
"dangerous_settings": "Gefährliche Einstellungen"
|
||||
"reset_config_description": "API Sclüssel und root Benutzer zurücksetzen."
|
||||
},
|
||||
"ssh": {
|
||||
"title": "SSH Schlüssel",
|
||||
|
|
|
@ -47,7 +47,29 @@
|
|||
"console_page": {
|
||||
"title": "Console",
|
||||
"waiting": "Waiting for initialization…",
|
||||
"copy": "Copy"
|
||||
"copy": "Copy",
|
||||
"copy_raw": "Raw response",
|
||||
"history_empty": "No data yet",
|
||||
"error": "Error",
|
||||
"log": "Log",
|
||||
"rest_api_request": "Rest API Request",
|
||||
"rest_api_response": "Rest API Response",
|
||||
"graphql_request": "GraphQL Request",
|
||||
"graphql_response": "GraphQL Response",
|
||||
"logged_at": "Logged at",
|
||||
"data": "Data",
|
||||
"errors": "Errors",
|
||||
"error_path": "Path",
|
||||
"error_locations": "Locations",
|
||||
"error_extensions": "Extensions",
|
||||
"request_data": "Request data",
|
||||
"headers": "Headers",
|
||||
"response_data": "Response data",
|
||||
"context": "Context",
|
||||
"operation": "Operation",
|
||||
"operation_type": "Operation type",
|
||||
"operation_name": "Operation name",
|
||||
"variables": "Variables"
|
||||
},
|
||||
"about_application_page": {
|
||||
"title": "About & support",
|
||||
|
@ -75,10 +97,12 @@
|
|||
},
|
||||
"application_settings": {
|
||||
"title": "Application settings",
|
||||
"system_dark_theme_title": "System default theme",
|
||||
"system_dark_theme_description": "Use light or dark theme depending on system settings",
|
||||
"system_theme_mode_title": "System default theme",
|
||||
"system_theme_mode_description": "Use light or dark theme depending on system settings",
|
||||
"dark_theme_title": "Dark theme",
|
||||
"dark_theme_description": "Switch your application theme",
|
||||
"change_application_theme": "Switch your application theme",
|
||||
"language": "Language",
|
||||
"click_to_change_locale": "Click to open language list",
|
||||
"dangerous_settings": "Dangerous settings",
|
||||
"reset_config_title": "Reset application config",
|
||||
"reset_config_description": "Resets API keys and root user."
|
||||
|
|
|
@ -39,14 +39,14 @@
|
|||
"test": "es-test",
|
||||
"locale": "es",
|
||||
"application_settings": {
|
||||
"reset_config_title": "Restablecer la configuración de la aplicación",
|
||||
"dark_theme_description": "Cambia el tema de tu aplicación",
|
||||
"reset_config_description": "Restablecer claves API y usuario root.",
|
||||
"title": "Ajustes de la aplicación",
|
||||
"system_theme_mode_title": "Tema del sistema",
|
||||
"system_theme_mode_description": "Utiliza un tema claro u oscuro de la configuración del sistema",
|
||||
"dark_theme_title": "Tema oscuro",
|
||||
"system_dark_theme_title": "Tema del sistema",
|
||||
"system_dark_theme_description": "Utiliza un tema claro u oscuro de la configuración del sistema",
|
||||
"dangerous_settings": "Configuraciones peligrosas"
|
||||
"change_application_theme": "Cambia el tema de tu aplicación",
|
||||
"dangerous_settings": "Configuraciones peligrosas",
|
||||
"reset_config_title": "Restablecer la configuración de la aplicación",
|
||||
"reset_config_description": "Restablecer claves API y usuario root."
|
||||
},
|
||||
"ssh": {
|
||||
"delete_confirm_question": "¿Está seguro de que desea eliminar la clave SSH?",
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
{
|
||||
"application_settings": {
|
||||
"system_dark_theme_description": "Kasutage valgus- või tumeteemat sõltuvalt süsteemi seadetest",
|
||||
"title": "Rakenduse seaded",
|
||||
"system_dark_theme_title": "Süsteemi vaiketeema",
|
||||
"system_theme_mode_title": "Süsteemi vaiketeema",
|
||||
"system_theme_mode_description": "Kasutage valgus- või tumeteemat sõltuvalt süsteemi seadetest",
|
||||
"dark_theme_title": "Tume teema",
|
||||
"dark_theme_description": "Vaheta oma rakenduse teemat",
|
||||
"change_application_theme": "Vaheta oma rakenduse teemat",
|
||||
"dangerous_settings": "Ohtlikud seaded",
|
||||
"reset_config_title": "Lähtesta rakenduse konfiguratsioon",
|
||||
"reset_config_description": "Lähtestab API võtmed ja juurkasutaja."
|
||||
|
|
|
@ -56,13 +56,13 @@
|
|||
},
|
||||
"application_settings": {
|
||||
"title": "Paramètres de l'application",
|
||||
"dark_theme_description": "Changer le thème de l'application",
|
||||
"reset_config_title": "Réinitialiser la configuration de l'application",
|
||||
"system_theme_mode_title": "Thème par défaut du système",
|
||||
"system_theme_mode_description": "Affichage de jour ou de nuit en fonction du paramétrage système",
|
||||
"dark_theme_title": "Thème sombre",
|
||||
"reset_config_description": "Réinitialiser les clés API et l'utilisateur root.",
|
||||
"system_dark_theme_title": "Thème par défaut du système",
|
||||
"system_dark_theme_description": "Affichage de jour ou de nuit en fonction du paramétrage système",
|
||||
"dangerous_settings": "Paramètres dangereux"
|
||||
"change_application_theme": "Changer le thème de l'application",
|
||||
"dangerous_settings": "Paramètres dangereux",
|
||||
"reset_config_title": "Réinitialiser la configuration de l'application",
|
||||
"reset_config_description": "Réinitialiser les clés API et l'utilisateur root."
|
||||
},
|
||||
"ssh": {
|
||||
"title": "Clés SSH",
|
||||
|
|
|
@ -81,10 +81,10 @@
|
|||
},
|
||||
"application_settings": {
|
||||
"title": "הגדרות יישום",
|
||||
"system_dark_theme_title": "ערכת העיצוב כברירת המחדל של המערכת",
|
||||
"system_dark_theme_description": "להשתמש בערכות עיצוב בהירה או כהה בהתאם להגדרות המערכת שלך",
|
||||
"system_theme_mode_title": "ערכת העיצוב כברירת המחדל של המערכת",
|
||||
"system_theme_mode_description": "להשתמש בערכות עיצוב בהירה או כהה בהתאם להגדרות המערכת שלך",
|
||||
"dark_theme_title": "ערכת עיצוב כהה",
|
||||
"dark_theme_description": "החלפת ערכת העיצוב של המערכת שלך",
|
||||
"change_application_theme": "החלפת ערכת העיצוב של המערכת שלך",
|
||||
"dangerous_settings": "הגדרות מסוכנות",
|
||||
"reset_config_title": "איפוס הגדרות היישומון",
|
||||
"reset_config_description": "איפוס מפתחות ה־API ומשתמש העל."
|
||||
|
|
|
@ -92,13 +92,13 @@
|
|||
"bug_report_subtitle": "Спамға байланысты есептік жазбаны қолмен растау қажет. Тіркелгіні белсендіру үшін Қолдау чатында бізге хабарласыңыз."
|
||||
},
|
||||
"application_settings": {
|
||||
"title": "Қосымша параметрлері",
|
||||
"system_theme_mode_title": "Системалық қараңғы тақырып",
|
||||
"system_theme_mode_description": "Системалық қараңғы тақырып сипаттамасы",
|
||||
"dark_theme_title": "Қараңғы тақырып",
|
||||
"change_application_theme": "Қараңғы тақырып сипаттамасы",
|
||||
"dangerous_settings": "Қауіпті параметрлер",
|
||||
"reset_config_title": "Конфигурацияны қалпына келтіру",
|
||||
"title": "Қосымша параметрлері",
|
||||
"system_dark_theme_title": "Системалық қараңғы тақырып",
|
||||
"system_dark_theme_description": "Системалық қараңғы тақырып сипаттамасы",
|
||||
"dark_theme_title": "Қараңғы тақырып",
|
||||
"dark_theme_description": "Қараңғы тақырып сипаттамасы",
|
||||
"reset_config_description": "Конфигурацияны қалпына келтіру сипаттамасы."
|
||||
},
|
||||
"resource_chart": {
|
||||
|
|
|
@ -52,11 +52,11 @@
|
|||
"privacy_policy": "Privātuma politika"
|
||||
},
|
||||
"application_settings": {
|
||||
"system_dark_theme_title": "Sistēmas noklusējuma dizains",
|
||||
"dark_theme_title": "Tumšs dizains",
|
||||
"title": "Aplikācijas iestatījumi",
|
||||
"system_dark_theme_description": "Izmantojiet gaišu vai tumšu dizainu atkarībā no sistēmas iestatījumiem",
|
||||
"dark_theme_description": "Lietojumprogrammas dizaina pārslēgšana",
|
||||
"system_theme_mode_title": "Sistēmas noklusējuma dizains",
|
||||
"system_theme_mode_description": "Izmantojiet gaišu vai tumšu dizainu atkarībā no sistēmas iestatījumiem",
|
||||
"dark_theme_title": "Tumšs dizains",
|
||||
"change_application_theme": "Lietojumprogrammas dizaina pārslēgšana",
|
||||
"dangerous_settings": "Bīstamie iestatījumi",
|
||||
"reset_config_title": "Atiestatīt lietojumprogrammas konfigurāciju",
|
||||
"reset_config_description": "Atiestatīt API atslēgas un saknes lietotāju."
|
||||
|
|
|
@ -56,13 +56,13 @@
|
|||
},
|
||||
"application_settings": {
|
||||
"title": "Ustawienia aplikacji",
|
||||
"system_theme_mode_description": "Użyj jasnego lub ciemnego motywu w zależności od ustawień systemu",
|
||||
"system_theme_mode_title": "Domyślny motyw systemowy",
|
||||
"dark_theme_title": "Ciemny motyw aplikacji",
|
||||
"dark_theme_description": "Zmień kolor motywu aplikacji",
|
||||
"change_application_theme": "Zmień kolor motywu aplikacji",
|
||||
"dangerous_settings": "Niebezpieczne ustawienia",
|
||||
"reset_config_title": "Resetowanie",
|
||||
"reset_config_description": "Zresetuj klucze API i użytkownika root.",
|
||||
"system_dark_theme_description": "Użyj jasnego lub ciemnego motywu w zależności od ustawień systemu",
|
||||
"system_dark_theme_title": "Domyślny motyw systemowy",
|
||||
"dangerous_settings": "Niebezpieczne ustawienia"
|
||||
"reset_config_description": "Zresetuj klucze API i użytkownika root."
|
||||
},
|
||||
"ssh": {
|
||||
"title": "klucze SSH",
|
||||
|
|
|
@ -75,13 +75,15 @@
|
|||
},
|
||||
"application_settings": {
|
||||
"title": "Настройки приложения",
|
||||
"system_theme_mode_title": "Системная тема",
|
||||
"system_theme_mode_description": "Будет использована светлая или тёмная тема в зависимости от системных настроек",
|
||||
"dark_theme_title": "Тёмная тема",
|
||||
"dark_theme_description": "Сменить цветовую тему",
|
||||
"change_application_theme": "Сменить цветовую тему",
|
||||
"language": "Язык",
|
||||
"click_to_change_locale": "Нажмите, чтобы открыть список языков",
|
||||
"dangerous_settings": "Опасные настройки",
|
||||
"reset_config_title": "Сброс настроек",
|
||||
"reset_config_description": "Сбросить API ключи и root пользователя.",
|
||||
"system_dark_theme_title": "Системная тема",
|
||||
"system_dark_theme_description": "Будет использована светлая или тёмная тема в зависимости от системных настроек",
|
||||
"dangerous_settings": "Опасные настройки"
|
||||
"reset_config_description": "Сбросить API ключи и root пользователя."
|
||||
},
|
||||
"ssh": {
|
||||
"title": "SSH ключи",
|
||||
|
|
|
@ -102,13 +102,13 @@
|
|||
},
|
||||
"application_settings": {
|
||||
"title": "Nastavenia aplikácie",
|
||||
"system_theme_mode_description": "Použitie svetlej alebo tmavej témy v závislosti od nastavení systému",
|
||||
"system_theme_mode_title": "Systémová predvolená téma",
|
||||
"dark_theme_title": "Temná téma",
|
||||
"dark_theme_description": "Zmeniť tému aplikácie",
|
||||
"change_application_theme": "Zmeniť tému aplikácie",
|
||||
"dangerous_settings": "Nebezpečné nastavenia",
|
||||
"reset_config_title": "Resetovať nastavenia aplikácie",
|
||||
"reset_config_description": "Resetovať kľúče API a užívateľa root.",
|
||||
"system_dark_theme_description": "Použitie svetlej alebo tmavej témy v závislosti od nastavení systému",
|
||||
"system_dark_theme_title": "Systémová predvolená téma",
|
||||
"dangerous_settings": "Nebezpečné nastavenia"
|
||||
"reset_config_description": "Resetovať kľúče API a užívateľa root."
|
||||
},
|
||||
"ssh": {
|
||||
"title": "Kľúče SSH",
|
||||
|
|
|
@ -53,11 +53,11 @@
|
|||
"application_version_text": "Različica aplikacije"
|
||||
},
|
||||
"application_settings": {
|
||||
"dark_theme_title": "Temna tema",
|
||||
"title": "Nastavitve aplikacije",
|
||||
"system_dark_theme_title": "Privzeta tema sistema",
|
||||
"system_dark_theme_description": "Uporaba svetle ali temne teme glede na sistemske nastavitve",
|
||||
"dark_theme_description": "Spreminjanje barvne teme",
|
||||
"system_theme_mode_title": "Privzeta tema sistema",
|
||||
"system_theme_mode_description": "Uporaba svetle ali temne teme glede na sistemske nastavitve",
|
||||
"dark_theme_title": "Temna tema",
|
||||
"change_application_theme": "Spreminjanje barvne teme",
|
||||
"dangerous_settings": "Nevarne nastavitve",
|
||||
"reset_config_title": "Ponastavitev konfiguracije aplikacije"
|
||||
},
|
||||
|
|
|
@ -47,9 +47,9 @@
|
|||
"privacy_policy": "นโยบายความเป็นส่วนตัว"
|
||||
},
|
||||
"application_settings": {
|
||||
"dark_theme_description": "สลับธีมแอปพลิเคชั่นของคุณ",
|
||||
"title": "การตั้งค่าแอปพลิเคชัน",
|
||||
"dark_theme_title": "ธีมมืด",
|
||||
"change_application_theme": "สลับธีมแอปพลิเคชั่นของคุณ",
|
||||
"reset_config_title": "รีเซ็ตค่าดั้งเดิมการตั้งค่าของแอปพลิเคชั่น",
|
||||
"reset_config_description": "รีเซ็ต API key และผู้ใช้งาน root"
|
||||
},
|
||||
|
|
|
@ -41,13 +41,14 @@
|
|||
"locale": "ua",
|
||||
"application_settings": {
|
||||
"title": "Налаштування додатка",
|
||||
"reset_config_title": "Скинути налаштування",
|
||||
"system_theme_mode_title": "Системна тема за замовчуванням",
|
||||
"system_theme_mode_description": "Використовуйте світлу або темну теми залежно від системних налаштувань",
|
||||
"dark_theme_title": "Темна тема",
|
||||
"dark_theme_description": "Змінити тему додатка",
|
||||
"reset_config_description": "Скинути API ключі та root користувача.",
|
||||
"system_dark_theme_title": "Системна тема за замовчуванням",
|
||||
"system_dark_theme_description": "Використовуйте світлу або темну теми залежно від системних налаштувань",
|
||||
"dangerous_settings": "Небезпечні налаштування"
|
||||
"change_application_theme": "Змінити тему додатка",
|
||||
"language": "Мова",
|
||||
"dangerous_settings": "Небезпечні налаштування",
|
||||
"reset_config_title": "Скинути налаштування",
|
||||
"reset_config_description": "Скинути API ключі та root користувача."
|
||||
},
|
||||
"ssh": {
|
||||
"delete_confirm_question": "Ви впевнені, що хочете видалити SSH-ключ?",
|
||||
|
|
|
@ -475,12 +475,13 @@
|
|||
},
|
||||
"application_settings": {
|
||||
"title": "应用设置",
|
||||
"system_dark_theme_title": "系统默认主题",
|
||||
"system_theme_mode_title": "系统默认主题",
|
||||
"system_theme_mode_description": "根据系统设置自动使用明亮或暗色主题",
|
||||
"dark_theme_title": "暗色主题",
|
||||
"system_dark_theme_description": "根据系统设置自动使用明亮或暗色主题",
|
||||
"dark_theme_description": "切换应用主题",
|
||||
"change_application_theme": "切换应用主题",
|
||||
"dangerous_settings": "危险设置",
|
||||
"reset_config_title": "重置应用配置",
|
||||
"delete_server_title": "删除服务器",
|
||||
"reset_config_description": "重置API密钥和root用户。"
|
||||
},
|
||||
"ssh": {
|
||||
|
|
175
lib/config/app_controller/app_controller.dart
Normal file
175
lib/config/app_controller/app_controller.dart
Normal file
|
@ -0,0 +1,175 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:material_color_utilities/material_color_utilities.dart'
|
||||
as color_utils;
|
||||
import 'package:selfprivacy/config/get_it_config.dart';
|
||||
import 'package:selfprivacy/config/localization.dart';
|
||||
import 'package:selfprivacy/config/preferences_repository/preferences_repository.dart';
|
||||
|
||||
/// A class that many Widgets can interact with to read current app
|
||||
/// configuration, update it, or listen to its changes.
|
||||
///
|
||||
/// AppController uses repo to change persistent data.
|
||||
class AppController with ChangeNotifier {
|
||||
AppController(this._repo);
|
||||
|
||||
/// repo encapsulates retrieval and storage of preferences
|
||||
final PreferencesRepository _repo;
|
||||
|
||||
/// TODO: to be removed or changed
|
||||
late final ApiConfigModel _apiConfigModel = getIt.get<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;
|
||||
|
||||
late bool _shouldShowOnboarding;
|
||||
bool get shouldShowOnboarding => _shouldShowOnboarding;
|
||||
|
||||
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 = [
|
||||
Localization.systemLocale,
|
||||
...await _repo.getSupportedLocales(),
|
||||
];
|
||||
|
||||
_locale = await _repo.getActiveLocale();
|
||||
if (_locale != Localization.systemLocale) {
|
||||
// preset value to other state holders
|
||||
await _apiConfigModel.setLocaleCode(_locale.languageCode);
|
||||
await _repo.setDelegateLocale(_locale);
|
||||
}
|
||||
}(),
|
||||
|
||||
// load theme mode && initialize theme
|
||||
() async {
|
||||
_lightTheme = lightThemeData;
|
||||
_darkTheme = darkThemeData;
|
||||
_corePalette = colorPalette;
|
||||
_darkThemeModeActive = await _repo.getDarkThemeModeFlag();
|
||||
_systemThemeModeActive = await _repo.getSystemThemeModeFlag();
|
||||
}(),
|
||||
|
||||
// load onboarding flag
|
||||
() async {
|
||||
_shouldShowOnboarding = await _repo.getShouldShowOnboarding();
|
||||
}(),
|
||||
]);
|
||||
|
||||
_loaded = true;
|
||||
// Important! Inform listeners a change has occurred.
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// updateRepoReference
|
||||
Future<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);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
if (newLocale == Localization.systemLocale) {
|
||||
return resetLocale();
|
||||
}
|
||||
|
||||
/// update locale delegate, which in turn should update deps
|
||||
await _repo.setDelegateLocale(newLocale);
|
||||
|
||||
// Persist the change
|
||||
await _repo.setActiveLocale(newLocale);
|
||||
// Update other locale holders
|
||||
await _apiConfigModel.setLocaleCode(newLocale.languageCode);
|
||||
}
|
||||
|
||||
Future<void> resetLocale() async {
|
||||
/// update locale delegate, which in turn should update deps
|
||||
await _repo.resetDelegateLocale();
|
||||
|
||||
// Persist the change
|
||||
await _repo.resetActiveLocale();
|
||||
// Update other locale holders
|
||||
await _apiConfigModel.resetLocaleCode();
|
||||
}
|
||||
}
|
106
lib/config/app_controller/inherited_app_controller.dart
Normal file
106
lib/config/app_controller/inherited_app_controller.dart
Normal file
|
@ -0,0 +1,106 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:material_color_utilities/material_color_utilities.dart'
|
||||
as color_utils;
|
||||
import 'package:selfprivacy/config/app_controller/app_controller.dart';
|
||||
import 'package:selfprivacy/config/brand_colors.dart';
|
||||
import 'package:selfprivacy/config/preferences_repository/inherited_preferences_repository.dart';
|
||||
import 'package:selfprivacy/config/preferences_repository/preferences_repository.dart';
|
||||
import 'package:selfprivacy/theming/factory/app_theme_factory.dart';
|
||||
|
||||
class _AppControllerInjector extends InheritedNotifier<AppController> {
|
||||
const _AppControllerInjector({
|
||||
required super.child,
|
||||
required super.notifier,
|
||||
});
|
||||
}
|
||||
|
||||
class InheritedAppController extends StatefulWidget {
|
||||
const InheritedAppController({
|
||||
required this.child,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
State<InheritedAppController> createState() => _InheritedAppControllerState();
|
||||
|
||||
static AppController of(final BuildContext context) => context
|
||||
.dependOnInheritedWidgetOfExactType<_AppControllerInjector>()!
|
||||
.notifier!;
|
||||
}
|
||||
|
||||
class _InheritedAppControllerState extends State<InheritedAppController> {
|
||||
// actual state provider
|
||||
late AppController controller;
|
||||
// hold local reference to active repo
|
||||
late PreferencesRepository _repo;
|
||||
|
||||
bool initTriggerred = false;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
/// update reference on dependency change
|
||||
_repo = InheritedPreferencesRepository.of(context)!;
|
||||
|
||||
if (!initTriggerred) {
|
||||
/// hook controller repo to local reference
|
||||
controller = AppController(_repo);
|
||||
initialize();
|
||||
initTriggerred = true;
|
||||
}
|
||||
|
||||
super.didChangeDependencies();
|
||||
}
|
||||
|
||||
Future<void> initialize() async {
|
||||
late final ThemeData lightThemeData;
|
||||
late final ThemeData darkThemeData;
|
||||
late final color_utils.CorePalette colorPalette;
|
||||
|
||||
await Future.wait(
|
||||
<Future<void>>[
|
||||
() async {
|
||||
lightThemeData = await AppThemeFactory.create(
|
||||
isDark: false,
|
||||
fallbackColor: BrandColors.primary,
|
||||
);
|
||||
}(),
|
||||
() async {
|
||||
darkThemeData = await AppThemeFactory.create(
|
||||
isDark: true,
|
||||
fallbackColor: BrandColors.primary,
|
||||
);
|
||||
}(),
|
||||
() async {
|
||||
colorPalette = (await AppThemeFactory.getCorePalette()) ??
|
||||
color_utils.CorePalette.of(BrandColors.primary.value);
|
||||
}(),
|
||||
],
|
||||
);
|
||||
|
||||
await controller.init(
|
||||
colorPalette: colorPalette,
|
||||
lightThemeData: lightThemeData,
|
||||
darkThemeData: darkThemeData,
|
||||
);
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((final _) {
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(final BuildContext context) => _AppControllerInjector(
|
||||
notifier: controller,
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
|
@ -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,76 @@ import 'package:flutter/material.dart';
|
|||
|
||||
class Localization extends StatelessWidget {
|
||||
const Localization({
|
||||
required this.child,
|
||||
super.key,
|
||||
this.child,
|
||||
});
|
||||
|
||||
final Widget? child;
|
||||
/// value for resetting locale in settings to system default
|
||||
static const systemLocale = Locale('system');
|
||||
|
||||
// when adding new locale, add corresponding native language name to mapper
|
||||
// below
|
||||
static const supportedLocales = [
|
||||
Locale('ar'),
|
||||
Locale('az'),
|
||||
Locale('be'),
|
||||
Locale('cs'),
|
||||
Locale('de'),
|
||||
Locale('en'),
|
||||
Locale('es'),
|
||||
Locale('et'),
|
||||
Locale('fr'),
|
||||
Locale('he'),
|
||||
Locale('kk'),
|
||||
Locale('lv'),
|
||||
Locale('mk'),
|
||||
Locale('pl'),
|
||||
Locale('ru'),
|
||||
Locale('sk'),
|
||||
Locale('sl'),
|
||||
Locale('th'),
|
||||
Locale('uk'),
|
||||
Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hans'),
|
||||
];
|
||||
|
||||
// https://en.wikipedia.org/wiki/IETF_language_tag#List_of_common_primary_language_subtags
|
||||
static final _languageNames = {
|
||||
systemLocale: 'System default',
|
||||
const Locale('ar'): 'العربية',
|
||||
const Locale('az'): 'Azərbaycan',
|
||||
const Locale('be'): 'беларуская',
|
||||
const Locale('cs'): 'čeština',
|
||||
const Locale('de'): 'Deutsch',
|
||||
const Locale('en'): 'English',
|
||||
const Locale('es'): 'español',
|
||||
const Locale('et'): 'eesti',
|
||||
const Locale('fr'): 'français',
|
||||
const Locale('he'): 'עברית',
|
||||
const Locale('kk'): 'Қазақша',
|
||||
const Locale('lv'): 'latviešu',
|
||||
const Locale('mk'): 'македонски јазик',
|
||||
const Locale('pl'): 'polski',
|
||||
const Locale('ru'): 'русский',
|
||||
const Locale('sk'): 'slovenčina',
|
||||
const Locale('sl'): 'slovenski',
|
||||
const Locale('th'): 'ไทย',
|
||||
const Locale('uk'): 'Українська',
|
||||
const Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hans'): '中文',
|
||||
};
|
||||
|
||||
static String getLanguageName(final Locale locale) =>
|
||||
_languageNames[locale] ?? locale.languageCode;
|
||||
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(final BuildContext context) => EasyLocalization(
|
||||
supportedLocales: const [
|
||||
Locale('ar'),
|
||||
Locale('az'),
|
||||
Locale('be'),
|
||||
Locale('cs'),
|
||||
Locale('de'),
|
||||
Locale('en'),
|
||||
Locale('es'),
|
||||
Locale('et'),
|
||||
Locale('fr'),
|
||||
Locale('he'),
|
||||
Locale('kk'),
|
||||
Locale('lv'),
|
||||
Locale('mk'),
|
||||
Locale('pl'),
|
||||
Locale('ru'),
|
||||
Locale('sk'),
|
||||
Locale('sl'),
|
||||
Locale('th'),
|
||||
Locale('uk'),
|
||||
Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hans'),
|
||||
],
|
||||
supportedLocales: supportedLocales,
|
||||
path: 'assets/translations',
|
||||
fallbackLocale: const Locale('en'),
|
||||
useFallbackTranslations: true,
|
||||
saveLocale: false,
|
||||
useOnlyLangCode: false,
|
||||
child: child!,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
/// 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
|
||||
Future<void> setLocale(final String? newLocale);
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
import 'package:hive/hive.dart';
|
||||
import 'package:selfprivacy/config/hive_config.dart';
|
||||
import 'package:selfprivacy/config/preferences_repository/datasources/preferences_datasource.dart';
|
||||
|
||||
/// app preferences data source hive implementation
|
||||
class PreferencesHiveDataSource implements PreferencesDataSource {
|
||||
final Box _appSettingsBox = Hive.box(BNames.appSettingsBox);
|
||||
|
||||
@override
|
||||
Future<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 => newLocale == null
|
||||
? _appSettingsBox.delete(BNames.appLocale)
|
||||
: _appSettingsBox.put(BNames.appLocale, newLocale);
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:selfprivacy/config/preferences_repository/datasources/preferences_datasource.dart';
|
||||
import 'package:selfprivacy/config/preferences_repository/preferences_repository.dart';
|
||||
|
||||
class _PreferencesRepositoryInjector extends InheritedWidget {
|
||||
const _PreferencesRepositoryInjector({
|
||||
required this.settingsRepository,
|
||||
required super.child,
|
||||
});
|
||||
|
||||
final PreferencesRepository settingsRepository;
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(
|
||||
covariant final _PreferencesRepositoryInjector oldWidget,
|
||||
) =>
|
||||
oldWidget.settingsRepository != settingsRepository;
|
||||
}
|
||||
|
||||
/// Creates and injects app preferences repository inside widget tree.
|
||||
class InheritedPreferencesRepository extends StatefulWidget {
|
||||
const InheritedPreferencesRepository({
|
||||
required this.child,
|
||||
required this.dataSource,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final PreferencesDataSource dataSource;
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
State<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,
|
||||
resetDelegateLocale: EasyLocalization.of(context)!.resetLocale,
|
||||
getDelegateLocale: () => EasyLocalization.of(context)!.locale,
|
||||
getSupportedLocales: () => EasyLocalization.of(context)!.supportedLocales,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(final BuildContext context) => _PreferencesRepositoryInjector(
|
||||
settingsRepository: repo,
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:selfprivacy/config/localization.dart';
|
||||
import 'package:selfprivacy/config/preferences_repository/datasources/preferences_datasource.dart';
|
||||
|
||||
class PreferencesRepository {
|
||||
const PreferencesRepository({
|
||||
required this.dataSource,
|
||||
required this.getSupportedLocales,
|
||||
required this.getDelegateLocale,
|
||||
required this.setDelegateLocale,
|
||||
required this.resetDelegateLocale,
|
||||
});
|
||||
|
||||
final PreferencesDataSource dataSource;
|
||||
|
||||
/// easy localizations don't expose type of localization provider,
|
||||
/// so it needs to be this crutchy (I could've created one more class-wrapper,
|
||||
/// containing needed functions, but perceive it as boilerplate, because we
|
||||
/// don't need additional encapsulation level here)
|
||||
final FutureOr<void> Function(Locale) setDelegateLocale;
|
||||
final FutureOr<void> Function() resetDelegateLocale;
|
||||
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<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 ??= Localization.systemLocale;
|
||||
|
||||
return chosenLocale;
|
||||
}
|
||||
|
||||
Future<void> setActiveLocale(final Locale newLocale) async {
|
||||
await dataSource.setLocale(newLocale.toString());
|
||||
}
|
||||
|
||||
Future<void> resetActiveLocale() async {
|
||||
await dataSource.setLocale(null);
|
||||
}
|
||||
|
||||
/// 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;
|
||||
}
|
||||
}
|
||||
|
@ -113,14 +116,15 @@ abstract class GraphQLApiMap {
|
|||
);
|
||||
}
|
||||
|
||||
String get _locale => getIt.get<ApiConfigModel>().localeCode ?? 'en';
|
||||
String get _locale => getIt.get<ApiConfigModel>().localeCode;
|
||||
|
||||
String get _token {
|
||||
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];
|
||||
}
|
27
lib/logic/bloc/connection_status_bloc.dart
Normal file
27
lib/logic/bloc/connection_status_bloc.dart
Normal file
|
@ -0,0 +1,27 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:selfprivacy/config/get_it_config.dart';
|
||||
|
||||
/// basically, a bus for other blocs to listen to server status updates
|
||||
class ConnectionStatusBloc extends Bloc<ConnectionStatus, ConnectionStatus> {
|
||||
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,
|
||||
|
|
|
@ -9,7 +9,6 @@ class ApiConfigModel {
|
|||
final Box _box = Hive.box(BNames.serverInstallationBox);
|
||||
|
||||
ServerHostingDetails? get serverDetails => _serverDetails;
|
||||
String? get localeCode => _localeCode;
|
||||
String? get serverProviderKey => _serverProviderKey;
|
||||
String? get serverLocation => _serverLocation;
|
||||
String? get serverType => _serverType;
|
||||
|
@ -21,7 +20,12 @@ class ApiConfigModel {
|
|||
ServerDomain? get serverDomain => _serverDomain;
|
||||
BackblazeBucket? get backblazeBucket => _backblazeBucket;
|
||||
|
||||
static const localeCodeFallback = 'en';
|
||||
String? _localeCode;
|
||||
String get localeCode => _localeCode ?? localeCodeFallback;
|
||||
Future<void> setLocaleCode(final String value) async => _localeCode = value;
|
||||
Future<void> resetLocaleCode() async => _localeCode = null;
|
||||
|
||||
String? _serverProviderKey;
|
||||
String? _serverLocation;
|
||||
String? _dnsProviderKey;
|
||||
|
@ -33,10 +37,6 @@ class ApiConfigModel {
|
|||
ServerDomain? _serverDomain;
|
||||
BackblazeBucket? _backblazeBucket;
|
||||
|
||||
Future<void> setLocaleCode(final String value) async {
|
||||
_localeCode = value;
|
||||
}
|
||||
|
||||
Future<void> storeServerProviderType(final ServerProviderType value) async {
|
||||
await _box.put(BNames.serverProvider, value);
|
||||
_serverProvider = value;
|
||||
|
@ -88,7 +88,6 @@ class ApiConfigModel {
|
|||
}
|
||||
|
||||
void clear() {
|
||||
_localeCode = null;
|
||||
_serverProviderKey = null;
|
||||
_dnsProvider = null;
|
||||
_serverLocation = null;
|
||||
|
@ -101,8 +100,7 @@ class ApiConfigModel {
|
|||
_serverProvider = null;
|
||||
}
|
||||
|
||||
void init() {
|
||||
_localeCode = 'en';
|
||||
Future<void> init() async {
|
||||
_serverProviderKey = _box.get(BNames.hetznerKey);
|
||||
_serverLocation = _box.get(BNames.serverLocation);
|
||||
_dnsProviderKey = _box.get(BNames.cloudFlareKey);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
51
lib/logic/get_it/console_model.dart
Normal file
51
lib/logic/get_it/console_model.dart
Normal file
|
@ -0,0 +1,51 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:selfprivacy/logic/models/console_log.dart';
|
||||
|
||||
class ConsoleModel extends ChangeNotifier {
|
||||
/// limit for history, so logs won't affect memory and overflow
|
||||
static const logBufferLimit = 500;
|
||||
|
||||
/// differs from log buffer limit so as to not rearrange memory each time
|
||||
/// we add incoming log
|
||||
static const incomingBufferBreakpoint = 750;
|
||||
|
||||
final List<ConsoleLog> _logs = [];
|
||||
final List<ConsoleLog> _incomingQueue = [];
|
||||
|
||||
bool _paused = false;
|
||||
bool get paused => _paused;
|
||||
List<ConsoleLog> get logs => _logs;
|
||||
|
||||
void log(final ConsoleLog newLog) {
|
||||
if (paused) {
|
||||
_incomingQueue.add(newLog);
|
||||
if (_incomingQueue.length > incomingBufferBreakpoint) {
|
||||
logs.removeRange(0, _incomingQueue.length - logBufferLimit);
|
||||
}
|
||||
} else {
|
||||
logs.add(newLog);
|
||||
_updateQueue();
|
||||
}
|
||||
}
|
||||
|
||||
void play() {
|
||||
_logs.addAll(_incomingQueue);
|
||||
_paused = false;
|
||||
_updateQueue();
|
||||
_incomingQueue.clear();
|
||||
}
|
||||
|
||||
void pause() {
|
||||
_paused = true;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// drop logs over the limit and
|
||||
void _updateQueue() {
|
||||
// Make sure we don't have too many
|
||||
if (logs.length > logBufferLimit) {
|
||||
logs.removeRange(0, logs.length - logBufferLimit);
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
184
lib/logic/models/console_log.dart
Normal file
184
lib/logic/models/console_log.dart
Normal file
|
@ -0,0 +1,184 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:gql/language.dart' as gql;
|
||||
import 'package:graphql/client.dart' as gql_client;
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
enum ConsoleLogSeverity {
|
||||
normal,
|
||||
warning,
|
||||
}
|
||||
|
||||
/// Base entity for console logs.
|
||||
sealed class ConsoleLog {
|
||||
ConsoleLog({
|
||||
final String? customTitle,
|
||||
this.severity = ConsoleLogSeverity.normal,
|
||||
}) : title = customTitle ??
|
||||
(severity == ConsoleLogSeverity.warning ? 'Error' : 'Log'),
|
||||
time = DateTime.now();
|
||||
|
||||
final DateTime time;
|
||||
final ConsoleLogSeverity severity;
|
||||
bool get isError => severity == ConsoleLogSeverity.warning;
|
||||
|
||||
/// title for both in listing and in dialog
|
||||
final String title;
|
||||
|
||||
/// formatted data to be shown in listing
|
||||
String get content;
|
||||
|
||||
/// data available for copy in dialog
|
||||
String? get shareableData => '{"title": "$title",\n'
|
||||
'"timestamp": "$fullUTCString",\n'
|
||||
'"data":{\n$content\n}'
|
||||
'\n}';
|
||||
|
||||
static final DateFormat _formatter = DateFormat('hh:mm:ss');
|
||||
String get timeString => _formatter.format(time);
|
||||
|
||||
String get fullUTCString => time.toUtc().toIso8601String();
|
||||
}
|
||||
|
||||
abstract class LogWithRawResponse {
|
||||
String get rawResponse;
|
||||
}
|
||||
|
||||
/// entity for manually created logs, as opposed to automated ones coming
|
||||
/// from requests / responses
|
||||
class ManualConsoleLog extends ConsoleLog {
|
||||
ManualConsoleLog({
|
||||
required this.content,
|
||||
super.customTitle,
|
||||
super.severity,
|
||||
});
|
||||
|
||||
ManualConsoleLog.warning({
|
||||
required this.content,
|
||||
super.customTitle,
|
||||
}) : super(severity: ConsoleLogSeverity.warning);
|
||||
|
||||
@override
|
||||
String content;
|
||||
}
|
||||
|
||||
class RestApiRequestConsoleLog extends ConsoleLog {
|
||||
RestApiRequestConsoleLog({
|
||||
this.method,
|
||||
this.uri,
|
||||
this.headers,
|
||||
this.data,
|
||||
super.severity,
|
||||
});
|
||||
|
||||
/// headers thath should not be included into clipboard buffer, as opposed to
|
||||
/// `[[ConsoleLogItemDialog]]` `_KeyValueRow.hideList` which filters values,
|
||||
/// that should be accessible from UI, but hidden in screenshots
|
||||
static const blacklistedHeaders = ['Authorization'];
|
||||
|
||||
final String? method;
|
||||
final Uri? uri;
|
||||
final Map<String, dynamic>? headers;
|
||||
final String? data;
|
||||
|
||||
@override
|
||||
String get title => 'Rest API Request';
|
||||
|
||||
Map<String, dynamic> get filteredHeaders => Map.fromEntries(
|
||||
headers?.entries.where(
|
||||
(final entry) => !blacklistedHeaders.contains(entry.key),
|
||||
) ??
|
||||
const [],
|
||||
);
|
||||
|
||||
@override
|
||||
String get content => '"method": "$method",\n'
|
||||
'"uri": "$uri",\n'
|
||||
'"headers": ${jsonEncode(filteredHeaders)},\n' // censor header to not expose API keys
|
||||
'"data": $data';
|
||||
}
|
||||
|
||||
class RestApiResponseConsoleLog extends ConsoleLog {
|
||||
RestApiResponseConsoleLog({
|
||||
this.method,
|
||||
this.uri,
|
||||
this.statusCode,
|
||||
this.data,
|
||||
super.severity,
|
||||
});
|
||||
|
||||
final String? method;
|
||||
final Uri? uri;
|
||||
final int? statusCode;
|
||||
final String? data;
|
||||
|
||||
@override
|
||||
String get title => 'Rest API Response';
|
||||
@override
|
||||
String get content => '"method": "$method",\n'
|
||||
'"status_code": $statusCode,\n'
|
||||
'"uri": "$uri",\n'
|
||||
'"data": $data';
|
||||
}
|
||||
|
||||
/// there is no actual getter for context fields outside of its class
|
||||
/// one can extract unique entries by their type, which implements
|
||||
/// `ContextEntry` class, I'll leave the code here if in the future
|
||||
/// some entries will actually be needed.
|
||||
// extension ContextEncoder on gql_client.Context {
|
||||
// String get encode {
|
||||
// return '""';
|
||||
// }
|
||||
// }
|
||||
|
||||
class GraphQlRequestConsoleLog extends ConsoleLog {
|
||||
GraphQlRequestConsoleLog({
|
||||
required this.operationType,
|
||||
required this.operation,
|
||||
required this.variables,
|
||||
// this.context,
|
||||
super.severity,
|
||||
});
|
||||
|
||||
// final gql_client.Context? context;
|
||||
final String operationType;
|
||||
final gql_client.Operation? operation;
|
||||
String get operationDocument =>
|
||||
operation != null ? gql.printNode(operation!.document) : 'null';
|
||||
final Map<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
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;
|
||||
|
|
|
@ -39,6 +39,7 @@ class _BrandMarkdownState extends State<BrandMarkdown> {
|
|||
return MarkdownBody(
|
||||
shrinkWrap: true,
|
||||
styleSheet: markdown,
|
||||
selectable: true,
|
||||
onTapLink: (final String text, final String? href, final String title) {
|
||||
if (href != null) {
|
||||
canLaunchUrlString(href).then((final bool canLaunchURL) {
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -283,7 +283,7 @@ class JobsContent extends StatelessWidget {
|
|||
(final job) => job.uid == state.rebuildJobUid,
|
||||
);
|
||||
if (rebuildJob == null) {
|
||||
return const Gap(0);
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return Row(
|
||||
children: [
|
||||
|
|
|
@ -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,35 @@ 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(),
|
||||
),
|
||||
],
|
||||
_LanguagePicker(
|
||||
key: ValueKey('language_picker'.tr()),
|
||||
),
|
||||
const Gap(8),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Text(
|
||||
'application_settings.dangerous_settings'.tr(),
|
||||
style: Theme.of(context).textTheme.labelLarge!.copyWith(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const Gap(4),
|
||||
_ResetAppTile(
|
||||
key: ValueKey('reset_app'.tr()),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
62
lib/ui/pages/more/app_settings/language_picker.dart
Normal file
62
lib/ui/pages/more/app_settings/language_picker.dart
Normal file
|
@ -0,0 +1,62 @@
|
|||
part of 'app_settings.dart';
|
||||
|
||||
class _LanguagePicker extends StatelessWidget {
|
||||
const _LanguagePicker({super.key});
|
||||
|
||||
@override
|
||||
Widget build(final BuildContext context) => ListTile(
|
||||
title: Text(
|
||||
'application_settings.language'.tr(),
|
||||
),
|
||||
subtitle: Text('application_settings.click_to_change_locale'.tr()),
|
||||
trailing: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Text(
|
||||
Localization.getLanguageName(context.locale),
|
||||
style: Theme.of(context).textTheme.labelLarge,
|
||||
),
|
||||
),
|
||||
onTap: () async {
|
||||
final appController = InheritedAppController.of(context);
|
||||
final Locale? newLocale = await showDialog<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) {
|
||||
final appController = InheritedAppController.of(context);
|
||||
|
||||
return SimpleDialog(
|
||||
title: Text('application_settings.language'.tr()),
|
||||
children: [
|
||||
for (final locale in appController.supportedLocales)
|
||||
RadioMenuButton(
|
||||
groupValue: appController.locale,
|
||||
value: locale,
|
||||
child: Text(
|
||||
Localization.getLanguageName(locale),
|
||||
style: TextStyle(
|
||||
fontWeight: locale == appController.locale
|
||||
? FontWeight.w800
|
||||
: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
onChanged: (final newValue) => Navigator.of(context).pop(newValue),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
42
lib/ui/pages/more/app_settings/reset_app_button.dart
Normal file
42
lib/ui/pages/more/app_settings/reset_app_button.dart
Normal file
|
@ -0,0 +1,42 @@
|
|||
part of 'app_settings.dart';
|
||||
|
||||
class _ResetAppTile extends StatelessWidget {
|
||||
const _ResetAppTile({super.key});
|
||||
|
||||
@override
|
||||
Widget build(final BuildContext context) => ListTile(
|
||||
title: Text('application_settings.reset_config_title'.tr()),
|
||||
subtitle: Text('application_settings.reset_config_description'.tr()),
|
||||
onTap: () => showDialog(
|
||||
context: context,
|
||||
builder: (final context) => const _ResetAppDialog(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class _ResetAppDialog extends StatelessWidget {
|
||||
const _ResetAppDialog();
|
||||
|
||||
@override
|
||||
Widget build(final BuildContext context) => AlertDialog(
|
||||
title: Text('modals.are_you_sure'.tr()),
|
||||
content: Text('modals.purge_all_keys'.tr()),
|
||||
actions: [
|
||||
DialogActionButton(
|
||||
text: 'modals.purge_all_keys_confirm'.tr(),
|
||||
isRed: true,
|
||||
onPressed: () {
|
||||
context.read<ServerInstallationCubit>().clearAppConfig();
|
||||
|
||||
context.router.maybePop([
|
||||
const RootRoute(),
|
||||
]);
|
||||
context.resetLocale();
|
||||
},
|
||||
),
|
||||
DialogActionButton(
|
||||
text: 'basis.cancel'.tr(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
31
lib/ui/pages/more/app_settings/theme_picker.dart
Normal file
31
lib/ui/pages/more/app_settings/theme_picker.dart
Normal file
|
@ -0,0 +1,31 @@
|
|||
part of 'app_settings.dart';
|
||||
|
||||
class _ThemePicker extends StatelessWidget {
|
||||
const _ThemePicker({super.key});
|
||||
|
||||
@override
|
||||
Widget build(final BuildContext context) {
|
||||
final appController = InheritedAppController.of(context);
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
SwitchListTile.adaptive(
|
||||
title: Text('application_settings.system_theme_mode_title'.tr()),
|
||||
subtitle:
|
||||
Text('application_settings.system_theme_mode_description'.tr()),
|
||||
value: appController.systemThemeModeActive,
|
||||
onChanged: appController.setSystemThemeModeFlag,
|
||||
),
|
||||
SwitchListTile.adaptive(
|
||||
title: Text('application_settings.dark_theme_title'.tr()),
|
||||
subtitle: Text('application_settings.change_application_theme'.tr()),
|
||||
value: appController.darkThemeModeActive,
|
||||
onChanged: appController.systemThemeModeActive
|
||||
? null
|
||||
: appController.setDarkThemeModeFlag,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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(),
|
||||
],
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
304
lib/ui/pages/more/console/console_log_item_dialog.dart
Normal file
304
lib/ui/pages/more/console/console_log_item_dialog.dart
Normal file
|
@ -0,0 +1,304 @@
|
|||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:selfprivacy/logic/models/console_log.dart';
|
||||
import 'package:selfprivacy/utils/platform_adapter.dart';
|
||||
|
||||
extension on ConsoleLog {
|
||||
List<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) ...[
|
||||
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);
|
||||
|
||||
/// headers thath should be hidden in screenshots, but still accessible for
|
||||
/// user, as opposed to `[[ConsoleLog]]`
|
||||
/// `RestApiRequestConsoleLog.blacklistedHeaders` which need to be filtered
|
||||
/// out from clipboard content
|
||||
static const List<String> hideList = ['Authorization'];
|
||||
|
||||
final String title;
|
||||
final String? value;
|
||||
|
||||
@override
|
||||
Widget build(final BuildContext context) => hideList.contains(title)
|
||||
? _ObscuredKeyValueRow(title, value)
|
||||
: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: SelectableText.rich(
|
||||
TextSpan(
|
||||
style: DefaultTextStyle.of(context).style,
|
||||
children: <TextSpan>[
|
||||
TextSpan(
|
||||
text: '$title: ',
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
TextSpan(text: value ?? ''),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class _ObscuredKeyValueRow extends StatefulWidget {
|
||||
const _ObscuredKeyValueRow(this.title, this.value);
|
||||
|
||||
final String title;
|
||||
final String? value;
|
||||
|
||||
@override
|
||||
State<_ObscuredKeyValueRow> createState() => _ObscuredKeyValueRowState();
|
||||
}
|
||||
|
||||
class _ObscuredKeyValueRowState extends State<_ObscuredKeyValueRow> {
|
||||
static const obscuringCharacter = '•';
|
||||
bool _obscureValue = true;
|
||||
|
||||
@override
|
||||
Widget build(final BuildContext context) => Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SelectableText.rich(
|
||||
TextSpan(
|
||||
style: DefaultTextStyle.of(context).style,
|
||||
children: <TextSpan>[
|
||||
TextSpan(
|
||||
text: '${widget.title}: ',
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
TextSpan(
|
||||
text: _obscureValue
|
||||
? obscuringCharacter * (widget.value?.length ?? 4)
|
||||
: widget.value ?? '',
|
||||
style: const TextStyle(
|
||||
fontFeatures: [FontFeature.tabularFigures()],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
_obscureValue
|
||||
? Icons.visibility_outlined
|
||||
: Icons.visibility_off_outlined,
|
||||
),
|
||||
onPressed: () {
|
||||
_obscureValue ^= true; // toggle value
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// data row with only text
|
||||
class _DataRow extends StatelessWidget {
|
||||
const _DataRow(this.data);
|
||||
|
||||
final String? data;
|
||||
|
||||
@override
|
||||
Widget build(final BuildContext context) => Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: SelectableText(
|
||||
data ?? 'null',
|
||||
style: const TextStyle(fontWeight: FontWeight.w400),
|
||||
),
|
||||
);
|
||||
}
|
75
lib/ui/pages/more/console/console_log_item_widget.dart
Normal file
75
lib/ui/pages/more/console/console_log_item_widget.dart
Normal file
|
@ -0,0 +1,75 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:selfprivacy/logic/models/console_log.dart';
|
||||
import 'package:selfprivacy/ui/pages/more/console/console_log_item_dialog.dart';
|
||||
|
||||
extension on ConsoleLog {
|
||||
Color resolveColor(final BuildContext context) => isError
|
||||
? Theme.of(context).colorScheme.error
|
||||
: switch (this) {
|
||||
(final RestApiRequestConsoleLog _) =>
|
||||
Theme.of(context).colorScheme.secondary,
|
||||
(final RestApiResponseConsoleLog _) =>
|
||||
Theme.of(context).colorScheme.primary,
|
||||
(final GraphQlRequestConsoleLog _) =>
|
||||
Theme.of(context).colorScheme.secondary,
|
||||
(final GraphQlResponseConsoleLog _) =>
|
||||
Theme.of(context).colorScheme.tertiary,
|
||||
(final ManualConsoleLog _) => Theme.of(context).colorScheme.tertiary,
|
||||
};
|
||||
|
||||
IconData resolveIcon() => switch (this) {
|
||||
(final RestApiRequestConsoleLog _) => Icons.upload_outlined,
|
||||
(final RestApiResponseConsoleLog _) => Icons.download_outlined,
|
||||
(final GraphQlRequestConsoleLog _) => Icons.arrow_circle_up_outlined,
|
||||
(final GraphQlResponseConsoleLog _) => Icons.arrow_circle_down_outlined,
|
||||
(final ManualConsoleLog _) => Icons.read_more_outlined,
|
||||
};
|
||||
}
|
||||
|
||||
class ConsoleLogItemWidget extends StatelessWidget {
|
||||
const ConsoleLogItemWidget({
|
||||
required this.log,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final ConsoleLog log;
|
||||
|
||||
@override
|
||||
Widget build(final BuildContext context) => Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: ListTile(
|
||||
dense: true,
|
||||
title: Text.rich(
|
||||
TextSpan(
|
||||
style: DefaultTextStyle.of(context).style,
|
||||
children: <TextSpan>[
|
||||
TextSpan(
|
||||
text: '${log.timeString}: ',
|
||||
style: const TextStyle(
|
||||
fontFeatures: [FontFeature.tabularFigures()],
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: log.title,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
log.content,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 3,
|
||||
),
|
||||
leading: Icon(log.resolveIcon()),
|
||||
iconColor: log.resolveColor(context),
|
||||
onTap: () => showDialog(
|
||||
context: context,
|
||||
builder: (final BuildContext context) =>
|
||||
ConsoleItemDialog(log: log),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
143
lib/ui/pages/more/console/console_page.dart
Normal file
143
lib/ui/pages/more/console/console_page.dart
Normal file
|
@ -0,0 +1,143 @@
|
|||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:selfprivacy/config/get_it_config.dart';
|
||||
import 'package:selfprivacy/logic/models/console_log.dart';
|
||||
import 'package:selfprivacy/ui/pages/more/console/console_log_item_widget.dart';
|
||||
|
||||
/// listing with 500 latest app operations.
|
||||
@RoutePage()
|
||||
class ConsolePage extends StatefulWidget {
|
||||
const ConsolePage({super.key});
|
||||
|
||||
@override
|
||||
State<ConsolePage> createState() => _ConsolePageState();
|
||||
}
|
||||
|
||||
class _ConsolePageState extends State<ConsolePage> {
|
||||
ConsoleModel get console => getIt<ConsoleModel>();
|
||||
|
||||
/// should freeze logs state to properly read logs
|
||||
late final Future<void> future;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
future = getIt.allReady();
|
||||
console.addListener(update);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
console.removeListener(update);
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void update() {
|
||||
/// listener update could come at any time, like when widget is already
|
||||
/// unmounted or during frame build, adding as postframe callback ensures
|
||||
/// that element is marked for rebuild
|
||||
WidgetsBinding.instance.addPostFrameCallback((final _) {
|
||||
if (mounted) {
|
||||
setState(() => {});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(final BuildContext context) => SafeArea(
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('console_page.title'.tr()),
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () => Navigator.of(context).maybePop(),
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
console.paused
|
||||
? Icons.play_arrow_outlined
|
||||
: Icons.pause_outlined,
|
||||
),
|
||||
onPressed: console.paused ? console.play : console.pause,
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Scrollbar(
|
||||
child: FutureBuilder(
|
||||
future: future,
|
||||
builder: (
|
||||
final BuildContext context,
|
||||
final AsyncSnapshot<void> snapshot,
|
||||
) {
|
||||
if (snapshot.hasData) {
|
||||
final List<ConsoleLog> logs = console.logs;
|
||||
|
||||
return logs.isEmpty
|
||||
? const _ConsoleViewEmpty()
|
||||
: _ConsoleViewLoaded(logs: logs);
|
||||
}
|
||||
|
||||
return const _ConsoleViewLoading();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class _ConsoleViewLoading extends StatelessWidget {
|
||||
const _ConsoleViewLoading();
|
||||
|
||||
@override
|
||||
Widget build(final BuildContext context) => Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text('console_page.waiting'.tr()),
|
||||
const Gap(16),
|
||||
const Expanded(
|
||||
child: Center(
|
||||
child: CircularProgressIndicator.adaptive(),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
class _ConsoleViewEmpty extends StatelessWidget {
|
||||
const _ConsoleViewEmpty();
|
||||
|
||||
@override
|
||||
Widget build(final BuildContext context) => Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: Text('console_page.history_empty'.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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue