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:
Inex Code 2024-06-26 19:05:36 +03:00
commit 8318a7a7eb
112 changed files with 2618 additions and 1928 deletions

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

@ -0,0 +1,53 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "debug",
"request": "launch",
"type": "dart"
},
{
"name": "profile mode",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
},
{
"name": "release mode",
"request": "launch",
"type": "dart",
"flutterMode": "release"
},
{
"name": "debug (fdroid)",
"request": "launch",
"type": "dart",
"args": [
"--flavor",
"fdroid"
]
},
{
"name": "debug (production)",
"request": "launch",
"type": "dart",
"args": [
"--flavor",
"production"
]
},
{
"name": "debug (nightly)",
"request": "launch",
"type": "dart",
"args": [
"--flavor",
"nightly"
]
}
]
}

View file

@ -1,3 +1,10 @@
plugins {
id "com.android.application"
id "kotlin-android"
id "dev.flutter.flutter-gradle-plugin"
}
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
@ -6,10 +13,6 @@ if (localPropertiesFile.exists()) {
}
}
def flutterRoot = localProperties.getProperty('flutter.sdk')
if (flutterRoot == null) {
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
}
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
@ -21,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 {}

View file

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

View file

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

View file

@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-all.zip

View file

@ -1,11 +1,25 @@
include ':app'
pluginManagement {
def flutterSdkPath = {
def properties = new Properties()
file("local.properties").withInputStream { properties.load(it) }
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
return flutterSdkPath
}()
def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
def properties = new Properties()
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
assert localPropertiesFile.exists()
localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "7.3.0" apply false
id "org.jetbrains.kotlin.android" version "1.9.21" apply false
}
include ":app"

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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": "Уладальнікі паказаных тут ключоў атрымліваюць поўны доступ да дадзеных і налад сервера. Дадавайце выключна свае ключы.",

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 ומשתמש העל."

View file

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

View file

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

View file

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

View file

@ -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 ключи",

View file

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

View file

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

View file

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

View file

@ -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-ключ?",

View file

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

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

View file

@ -0,0 +1,106 @@
import 'package:flutter/material.dart';
import 'package:material_color_utilities/material_color_utilities.dart'
as color_utils;
import 'package:selfprivacy/config/app_controller/app_controller.dart';
import 'package:selfprivacy/config/brand_colors.dart';
import 'package:selfprivacy/config/preferences_repository/inherited_preferences_repository.dart';
import 'package:selfprivacy/config/preferences_repository/preferences_repository.dart';
import 'package:selfprivacy/theming/factory/app_theme_factory.dart';
class _AppControllerInjector extends InheritedNotifier<AppController> {
const _AppControllerInjector({
required super.child,
required super.notifier,
});
}
class InheritedAppController extends StatefulWidget {
const InheritedAppController({
required this.child,
super.key,
});
final Widget child;
@override
State<InheritedAppController> createState() => _InheritedAppControllerState();
static AppController of(final BuildContext context) => context
.dependOnInheritedWidgetOfExactType<_AppControllerInjector>()!
.notifier!;
}
class _InheritedAppControllerState extends State<InheritedAppController> {
// actual state provider
late AppController controller;
// hold local reference to active repo
late PreferencesRepository _repo;
bool initTriggerred = false;
@override
void didChangeDependencies() {
/// update reference on dependency change
_repo = InheritedPreferencesRepository.of(context)!;
if (!initTriggerred) {
/// hook controller repo to local reference
controller = AppController(_repo);
initialize();
initTriggerred = true;
}
super.didChangeDependencies();
}
Future<void> initialize() async {
late final ThemeData lightThemeData;
late final ThemeData darkThemeData;
late final color_utils.CorePalette colorPalette;
await Future.wait(
<Future<void>>[
() async {
lightThemeData = await AppThemeFactory.create(
isDark: false,
fallbackColor: BrandColors.primary,
);
}(),
() async {
darkThemeData = await AppThemeFactory.create(
isDark: true,
fallbackColor: BrandColors.primary,
);
}(),
() async {
colorPalette = (await AppThemeFactory.getCorePalette()) ??
color_utils.CorePalette.of(BrandColors.primary.value);
}(),
],
);
await controller.init(
colorPalette: colorPalette,
lightThemeData: lightThemeData,
darkThemeData: darkThemeData,
);
WidgetsBinding.instance.addPostFrameCallback((final _) {
if (mounted) {
setState(() {});
}
});
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(final BuildContext context) => _AppControllerInjector(
notifier: controller,
child: widget.child,
);
}

View file

@ -1,14 +1,13 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:selfprivacy/logic/bloc/backups/backups_bloc.dart';
import 'package:selfprivacy/logic/bloc/connection_status/connection_status_bloc.dart';
import 'package:selfprivacy/logic/bloc/connection_status_bloc.dart';
import 'package:selfprivacy/logic/bloc/devices/devices_bloc.dart';
import 'package:selfprivacy/logic/bloc/recovery_key/recovery_key_bloc.dart';
import 'package:selfprivacy/logic/bloc/server_jobs/server_jobs_bloc.dart';
import 'package:selfprivacy/logic/bloc/services/services_bloc.dart';
import 'package:selfprivacy/logic/bloc/users/users_bloc.dart';
import 'package:selfprivacy/logic/bloc/volumes/volumes_bloc.dart';
import 'package:selfprivacy/logic/cubit/app_settings/app_settings_cubit.dart';
import 'package:selfprivacy/logic/cubit/client_jobs/client_jobs_cubit.dart';
import 'package:selfprivacy/logic/cubit/dns_records/dns_records_cubit.dart';
import 'package:selfprivacy/logic/cubit/server_detailed_info/server_detailed_info_cubit.dart';
@ -56,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,
);
}

View file

@ -1,21 +1,23 @@
import 'package:get_it/get_it.dart';
import 'package:selfprivacy/logic/get_it/api_config.dart';
import 'package:selfprivacy/logic/get_it/api_connection_repository.dart';
import 'package:selfprivacy/logic/get_it/console.dart';
import 'package:selfprivacy/logic/get_it/console_model.dart';
import 'package:selfprivacy/logic/get_it/navigation.dart';
export 'package:selfprivacy/logic/get_it/api_config.dart';
export 'package:selfprivacy/logic/get_it/api_connection_repository.dart';
export 'package:selfprivacy/logic/get_it/console.dart';
export 'package:selfprivacy/logic/get_it/console_model.dart';
export 'package:selfprivacy/logic/get_it/navigation.dart';
final GetIt getIt = GetIt.instance;
Future<void> getItSetup() async {
getIt.registerSingleton<NavigationService>(NavigationService());
getIt.registerSingleton<ConsoleModel>(ConsoleModel());
getIt.registerSingleton<ApiConfigModel>(ApiConfigModel()..init());
final apiConfigModel = ApiConfigModel();
await apiConfigModel.init();
getIt.registerSingleton<ApiConfigModel>(apiConfigModel);
getIt.registerSingleton<ApiConnectionRepository>(
ApiConnectionRepository()..init(),

View file

@ -74,17 +74,20 @@ class HiveConfig {
/// Mappings for the different boxes and their keys
class BNames {
/// App settings box. Contains app settings like [isDarkModeOn], [isOnboardingShowing]
/// App settings box. Contains app settings like [darkThemeModeOn], [shouldShowOnboarding]
static String appSettingsBox = 'appSettings';
/// A boolean field of [appSettingsBox] box.
static String isDarkModeOn = 'isDarkModeOn';
static String darkThemeModeOn = 'isDarkModeOn';
/// A boolean field of [appSettingsBox] box.
static String isAutoDarkModeOn = 'isAutoDarkModeOn';
static String systemThemeModeOn = 'isAutoDarkModeOn';
/// A boolean field of [appSettingsBox] box.
static String isOnboardingShowing = 'isOnboardingShowing';
static String shouldShowOnboarding = 'isOnboardingShowing';
/// A string field
static String appLocale = 'appLocale';
/// Encryption key to decrypt [serverInstallationBox] and [usersBox] box.
static String serverInstallationEncryptionKey = 'key';

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,18 +1,14 @@
import 'dart:convert';
import 'dart:io';
import 'package:graphql_flutter/graphql_flutter.dart';
import 'package:http/io_client.dart';
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/api_maps/tls_options.dart';
import 'package:selfprivacy/logic/models/message.dart';
import 'package:selfprivacy/logic/models/console_log.dart';
void _logToAppConsole<T>(final T objectToLog) {
getIt.get<ConsoleModel>().addMessage(
Message(
text: objectToLog.toString(),
),
);
}
void _addConsoleLog(final ConsoleLog message) =>
getIt.get<ConsoleModel>().log(message);
class RequestLoggingLink extends Link {
@override
@ -20,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;
}

View file

@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:convert';
import 'dart:developer';
import 'dart:io';
@ -6,7 +7,7 @@ import 'package:dio/dio.dart';
import 'package:dio/io.dart';
import 'package:pretty_dio_logger/pretty_dio_logger.dart';
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/models/message.dart';
import 'package:selfprivacy/logic/models/console_log.dart';
abstract class RestApiMap {
Future<Dio> getClient({final BaseOptions? customOptions}) async {
@ -57,8 +58,8 @@ abstract class RestApiMap {
}
class ConsoleInterceptor extends InterceptorsWrapper {
void addMessage(final Message message) {
getIt.get<ConsoleModel>().addMessage(message);
void addConsoleLog(final ConsoleLog message) {
getIt.get<ConsoleModel>().log(message);
}
@override
@ -66,12 +67,12 @@ class ConsoleInterceptor extends InterceptorsWrapper {
final RequestOptions options,
final RequestInterceptorHandler handler,
) async {
addMessage(
RestApiRequestMessage(
method: options.method,
data: options.data.toString(),
headers: options.headers,
addConsoleLog(
RestApiRequestConsoleLog(
uri: options.uri,
method: options.method,
headers: options.headers,
data: jsonEncode(options.data),
),
);
return super.onRequest(options, handler);
@ -82,12 +83,12 @@ class ConsoleInterceptor extends InterceptorsWrapper {
final Response response,
final ResponseInterceptorHandler handler,
) async {
addMessage(
RestApiResponseMessage(
addConsoleLog(
RestApiResponseConsoleLog(
uri: response.realUri,
method: response.requestOptions.method,
statusCode: response.statusCode,
data: response.data.toString(),
uri: response.realUri,
data: jsonEncode(response.data),
),
);
return super.onResponse(
@ -103,10 +104,13 @@ class ConsoleInterceptor extends InterceptorsWrapper {
) async {
final Response? response = err.response;
log(err.toString());
addMessage(
Message.warn(
text:
'response-uri: ${response?.realUri}\ncode: ${response?.statusCode}\ndata: ${response?.toString()}\n',
addConsoleLog(
ManualConsoleLog.warning(
customTitle: 'RestAPI error',
content: '"uri": "${response?.realUri}",\n'
'"status_code": ${response?.statusCode},\n'
'"response": ${jsonEncode(response)}',
),
);
return super.onError(err, handler);

View file

@ -1,39 +0,0 @@
import 'dart:async';
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:selfprivacy/config/get_it_config.dart';
part 'connection_status_event.dart';
part 'connection_status_state.dart';
class ConnectionStatusBloc
extends Bloc<ConnectionStatusEvent, ConnectionStatusState> {
ConnectionStatusBloc()
: super(
const ConnectionStatusState(
connectionStatus: ConnectionStatus.nonexistent,
),
) {
on<ConnectionStatusChanged>((final event, final emit) {
emit(ConnectionStatusState(connectionStatus: event.connectionStatus));
});
final apiConnectionRepository = getIt<ApiConnectionRepository>();
_apiConnectionStatusSubscription =
apiConnectionRepository.connectionStatusStream.listen(
(final ConnectionStatus connectionStatus) {
add(
ConnectionStatusChanged(connectionStatus),
);
},
);
}
StreamSubscription? _apiConnectionStatusSubscription;
@override
Future<void> close() {
_apiConnectionStatusSubscription?.cancel();
return super.close();
}
}

View file

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

View file

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

View file

@ -0,0 +1,27 @@
import 'dart:async';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:selfprivacy/config/get_it_config.dart';
/// basically, a bus for other blocs to listen to server status updates
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();
}
}

View file

@ -1,66 +0,0 @@
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hive/hive.dart';
import 'package:material_color_utilities/material_color_utilities.dart'
as color_utils;
import 'package:selfprivacy/config/brand_colors.dart';
import 'package:selfprivacy/config/hive_config.dart';
import 'package:selfprivacy/theming/factory/app_theme_factory.dart';
export 'package:provider/provider.dart';
part 'app_settings_state.dart';
class AppSettingsCubit extends Cubit<AppSettingsState> {
AppSettingsCubit({
required final bool isDarkModeOn,
required final bool isAutoDarkModeOn,
required final bool isOnboardingShowing,
}) : super(
AppSettingsState(
isDarkModeOn: isDarkModeOn,
isAutoDarkModeOn: isAutoDarkModeOn,
isOnboardingShowing: isOnboardingShowing,
),
);
Box box = Hive.box(BNames.appSettingsBox);
void load() async {
final bool? isDarkModeOn = box.get(BNames.isDarkModeOn);
final bool? isAutoDarkModeOn = box.get(BNames.isAutoDarkModeOn);
final bool? isOnboardingShowing = box.get(BNames.isOnboardingShowing);
emit(
state.copyWith(
isDarkModeOn: isDarkModeOn,
isAutoDarkModeOn: isAutoDarkModeOn,
isOnboardingShowing: isOnboardingShowing,
),
);
WidgetsFlutterBinding.ensureInitialized();
final color_utils.CorePalette? colorPalette =
await AppThemeFactory.getCorePalette();
emit(
state.copyWith(
corePalette: colorPalette,
),
);
}
void updateDarkMode({required final bool isDarkModeOn}) {
box.put(BNames.isDarkModeOn, isDarkModeOn);
emit(state.copyWith(isDarkModeOn: isDarkModeOn));
}
void updateAutoDarkMode({required final bool isAutoDarkModeOn}) {
box.put(BNames.isAutoDarkModeOn, isAutoDarkModeOn);
emit(state.copyWith(isAutoDarkModeOn: isAutoDarkModeOn));
}
void turnOffOnboarding({final bool isOnboardingShowing = false}) {
box.put(BNames.isOnboardingShowing, isOnboardingShowing);
emit(state.copyWith(isOnboardingShowing: isOnboardingShowing));
}
}

View file

@ -1,35 +0,0 @@
part of 'app_settings_cubit.dart';
class AppSettingsState extends Equatable {
const AppSettingsState({
required this.isDarkModeOn,
required this.isAutoDarkModeOn,
required this.isOnboardingShowing,
this.corePalette,
});
final bool isDarkModeOn;
final bool isAutoDarkModeOn;
final bool isOnboardingShowing;
final color_utils.CorePalette? corePalette;
AppSettingsState copyWith({
final bool? isDarkModeOn,
final bool? isAutoDarkModeOn,
final bool? isOnboardingShowing,
final color_utils.CorePalette? corePalette,
}) =>
AppSettingsState(
isDarkModeOn: isDarkModeOn ?? this.isDarkModeOn,
isAutoDarkModeOn: isAutoDarkModeOn ?? this.isAutoDarkModeOn,
isOnboardingShowing: isOnboardingShowing ?? this.isOnboardingShowing,
corePalette: corePalette ?? this.corePalette,
);
color_utils.CorePalette get corePaletteOrDefault =>
corePalette ?? color_utils.CorePalette.of(BrandColors.primary.value);
@override
List<dynamic> get props =>
[isDarkModeOn, isAutoDarkModeOn, isOnboardingShowing, corePalette];
}

View file

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

View file

@ -49,9 +49,10 @@ abstract class ServerInstallationState extends Equatable {
bool get isPrimaryUserFilled => rootUser != null;
bool get isServerCreated => serverDetails != null;
bool get isFullyInitilized => _fulfilementList.every((final el) => el!);
bool get isFullyInitialized =>
_fulfillmentList.every((final el) => el ?? false);
ServerSetupProgress get progress => ServerSetupProgress
.values[_fulfilementList.where((final el) => el!).length];
.values[_fulfillmentList.where((final el) => el!).length];
int get porgressBar {
if (progress.index < 6) {
@ -63,7 +64,7 @@ abstract class ServerInstallationState extends Equatable {
}
}
List<bool?> get _fulfilementList {
List<bool?> get _fulfillmentList {
final List<bool> res = [
isServerProviderApiKeyFilled,
isServerTypeFilled,

View file

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

View file

@ -1,17 +0,0 @@
import 'package:flutter/material.dart';
import 'package:selfprivacy/logic/models/message.dart';
class ConsoleModel extends ChangeNotifier {
final List<Message> _messages = [];
List<Message> get messages => _messages;
void addMessage(final Message message) {
messages.add(message);
notifyListeners();
// Make sure we don't have too many messages
if (messages.length > 500) {
messages.removeAt(0);
}
}
}

View file

@ -0,0 +1,51 @@
import 'package:flutter/material.dart';
import 'package:selfprivacy/logic/models/console_log.dart';
class ConsoleModel extends ChangeNotifier {
/// limit for history, so logs won't affect memory and overflow
static const logBufferLimit = 500;
/// differs from log buffer limit so as to not rearrange memory each time
/// we add incoming log
static const incomingBufferBreakpoint = 750;
final List<ConsoleLog> _logs = [];
final List<ConsoleLog> _incomingQueue = [];
bool _paused = false;
bool get paused => _paused;
List<ConsoleLog> get logs => _logs;
void log(final ConsoleLog newLog) {
if (paused) {
_incomingQueue.add(newLog);
if (_incomingQueue.length > incomingBufferBreakpoint) {
logs.removeRange(0, _incomingQueue.length - logBufferLimit);
}
} else {
logs.add(newLog);
_updateQueue();
}
}
void play() {
_logs.addAll(_incomingQueue);
_paused = false;
_updateQueue();
_incomingQueue.clear();
}
void pause() {
_paused = true;
notifyListeners();
}
/// drop logs over the limit and
void _updateQueue() {
// Make sure we don't have too many
if (logs.length > logBufferLimit) {
logs.removeRange(0, logs.length - logBufferLimit);
}
notifyListeners();
}
}

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

View file

@ -77,46 +77,21 @@ class DigitalOceanLocation {
return emoji;
}
static const _townPrefixToCountryMap = {
'fra': 'germany',
'ams': 'netherlands',
'sgp': 'singapore',
'lon': 'united_kingdom',
'tor': 'canada',
'blr': 'india',
'syd': 'australia',
'nyc': 'united_states',
'sfo': 'united_states',
};
String get countryDisplayKey {
String displayKey = 'countries.';
switch (slug.substring(0, 3)) {
case 'fra':
displayKey += 'germany';
break;
case 'ams':
displayKey += 'netherlands';
break;
case 'sgp':
displayKey += 'singapore';
break;
case 'lon':
displayKey += 'united_kingdom';
break;
case 'tor':
displayKey += 'canada';
break;
case 'blr':
displayKey += 'india';
break;
case 'syd':
displayKey += 'australia';
break;
case 'nyc':
case 'sfo':
displayKey += 'united_states';
break;
default:
displayKey = slug;
}
return displayKey;
final countryName = _townPrefixToCountryMap[slug.substring(0, 3)] ?? slug;
return 'countries.$countryName';
}
}

View file

@ -10,8 +10,10 @@ DigitalOceanVolume _$DigitalOceanVolumeFromJson(Map<String, dynamic> json) =>
DigitalOceanVolume(
json['id'] as String,
json['name'] as String,
json['size_gigabytes'] as int,
(json['droplet_ids'] as List<dynamic>?)?.map((e) => e as int).toList(),
(json['size_gigabytes'] as num).toInt(),
(json['droplet_ids'] as List<dynamic>?)
?.map((e) => (e as num).toInt())
.toList(),
);
Map<String, dynamic> _$DigitalOceanVolumeToJson(DigitalOceanVolume instance) =>
@ -42,10 +44,10 @@ DigitalOceanServerType _$DigitalOceanServerTypeFromJson(
(json['regions'] as List<dynamic>).map((e) => e as String).toList(),
(json['memory'] as num).toDouble(),
json['description'] as String,
json['disk'] as int,
(json['disk'] as num).toInt(),
(json['price_monthly'] as num).toDouble(),
json['slug'] as String,
json['vcpus'] as int,
(json['vcpus'] as num).toInt(),
);
Map<String, dynamic> _$DigitalOceanServerTypeToJson(

View file

@ -24,8 +24,8 @@ CloudflareDnsRecord _$CloudflareDnsRecordFromJson(Map<String, dynamic> json) =>
name: json['name'] as String?,
content: json['content'] as String?,
zoneName: json['zone_name'] as String,
ttl: json['ttl'] as int? ?? 3600,
priority: json['priority'] as int? ?? 10,
ttl: (json['ttl'] as num?)?.toInt() ?? 3600,
priority: (json['priority'] as num?)?.toInt() ?? 10,
id: json['id'] as String?,
);

View file

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

View file

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

View file

@ -8,7 +8,7 @@ part of 'hetzner_server_info.dart';
HetznerServerInfo _$HetznerServerInfoFromJson(Map<String, dynamic> json) =>
HetznerServerInfo(
json['id'] as int,
(json['id'] as num).toInt(),
json['name'] as String,
$enumDecode(_$ServerStatusEnumMap, json['status']),
DateTime.parse(json['created'] as String),
@ -16,7 +16,9 @@ HetznerServerInfo _$HetznerServerInfoFromJson(Map<String, dynamic> json) =>
json['server_type'] as Map<String, dynamic>),
HetznerServerInfo.locationFromJson(json['datacenter'] as Map),
HetznerPublicNetInfo.fromJson(json['public_net'] as Map<String, dynamic>),
(json['volumes'] as List<dynamic>).map((e) => e as int).toList(),
(json['volumes'] as List<dynamic>)
.map((e) => (e as num).toInt())
.toList(),
);
Map<String, dynamic> _$HetznerServerInfoToJson(HetznerServerInfo instance) =>
@ -58,7 +60,7 @@ Map<String, dynamic> _$HetznerPublicNetInfoToJson(
};
HetznerIp4 _$HetznerIp4FromJson(Map<String, dynamic> json) => HetznerIp4(
json['id'] as int,
(json['id'] as num).toInt(),
json['ip'] as String,
json['blocked'] as bool,
json['dns_ptr'] as String,
@ -75,9 +77,9 @@ Map<String, dynamic> _$HetznerIp4ToJson(HetznerIp4 instance) =>
HetznerServerTypeInfo _$HetznerServerTypeInfoFromJson(
Map<String, dynamic> json) =>
HetznerServerTypeInfo(
json['cores'] as int,
(json['cores'] as num).toInt(),
json['memory'] as num,
json['disk'] as int,
(json['disk'] as num).toInt(),
(json['prices'] as List<dynamic>)
.map((e) => HetznerPriceInfo.fromJson(e as Map<String, dynamic>))
.toList(),
@ -132,9 +134,9 @@ Map<String, dynamic> _$HetznerLocationToJson(HetznerLocation instance) =>
HetznerVolume _$HetznerVolumeFromJson(Map<String, dynamic> json) =>
HetznerVolume(
json['id'] as int,
json['size'] as int,
json['serverId'] as int?,
(json['id'] as num).toInt(),
(json['size'] as num).toInt(),
(json['serverId'] as num?)?.toInt(),
json['name'] as String,
json['linux_device'] as String?,
);

View file

@ -15,7 +15,7 @@ RecoveryKeyStatus _$RecoveryKeyStatusFromJson(Map<String, dynamic> json) =>
expiration: json['expiration'] == null
? null
: DateTime.parse(json['expiration'] as String),
usesLeft: json['uses_left'] as int?,
usesLeft: (json['uses_left'] as num?)?.toInt(),
);
Map<String, dynamic> _$RecoveryKeyStatusToJson(RecoveryKeyStatus instance) =>

View file

@ -15,7 +15,7 @@ ServerJob _$ServerJobFromJson(Map<String, dynamic> json) => ServerJob(
updatedAt: DateTime.parse(json['updatedAt'] as String),
createdAt: DateTime.parse(json['createdAt'] as String),
error: json['error'] as String?,
progress: json['progress'] as int?,
progress: (json['progress'] as num?)?.toInt(),
result: json['result'] as String?,
statusText: json['statusText'] as String?,
finishedAt: json['finishedAt'] == null

View file

@ -1,75 +0,0 @@
import 'package:graphql/client.dart';
import 'package:intl/intl.dart';
/// TODO(misterfourtytwo): add equality override
class Message {
Message({this.text, this.severity = MessageSeverity.normal})
: time = DateTime.now();
Message.warn({this.text})
: severity = MessageSeverity.warning,
time = DateTime.now();
final String? text;
final DateTime time;
final MessageSeverity severity;
static final DateFormat _formatter = DateFormat('hh:mm');
String get timeString => _formatter.format(time);
}
enum MessageSeverity {
normal,
warning,
}
class RestApiRequestMessage extends Message {
RestApiRequestMessage({
this.method,
this.uri,
this.data,
this.headers,
}) : super(text: 'request-uri: $uri\nheaders: $headers\ndata: $data');
final String? method;
final Uri? uri;
final String? data;
final Map<String, dynamic>? headers;
}
class RestApiResponseMessage extends Message {
RestApiResponseMessage({
this.method,
this.uri,
this.statusCode,
this.data,
}) : super(text: 'response-uri: $uri\ncode: $statusCode\ndata: $data');
final String? method;
final Uri? uri;
final int? statusCode;
final String? data;
}
class GraphQlResponseMessage extends Message {
GraphQlResponseMessage({
this.data,
this.errors,
this.context,
}) : super(text: 'GraphQL Response\ndata: $data');
final Map<String, dynamic>? data;
final List<GraphQLError>? errors;
final Context? context;
}
class GraphQlRequestMessage extends Message {
GraphQlRequestMessage({
this.operation,
this.variables,
this.context,
}) : super(text: 'GraphQL Request\noperation: $operation');
final Operation? operation;
final Map<String, dynamic>? variables;
final Context? context;
}

View file

@ -27,9 +27,9 @@ class ApiAdapter {
class CloudflareDnsProvider extends DnsProvider {
CloudflareDnsProvider() : _adapter = ApiAdapter();
CloudflareDnsProvider.load(
final bool isAuthotized,
final bool isAuthorized,
) : _adapter = ApiAdapter(
isWithToken: isAuthotized,
isWithToken: isAuthorized,
);
ApiAdapter _adapter;

View file

@ -22,9 +22,9 @@ class ApiAdapter {
class DesecDnsProvider extends DnsProvider {
DesecDnsProvider() : _adapter = ApiAdapter();
DesecDnsProvider.load(
final bool isAuthotized,
final bool isAuthorized,
) : _adapter = ApiAdapter(
isWithToken: isAuthotized,
isWithToken: isAuthorized,
);
ApiAdapter _adapter;

View file

@ -22,9 +22,9 @@ class ApiAdapter {
class DigitalOceanDnsProvider extends DnsProvider {
DigitalOceanDnsProvider() : _adapter = ApiAdapter();
DigitalOceanDnsProvider.load(
final bool isAuthotized,
final bool isAuthorized,
) : _adapter = ApiAdapter(
isWithToken: isAuthotized,
isWithToken: isAuthorized,
);
ApiAdapter _adapter;

View file

@ -38,9 +38,9 @@ class DigitalOceanServerProvider extends ServerProvider {
DigitalOceanServerProvider() : _adapter = ApiAdapter();
DigitalOceanServerProvider.load(
final String? location,
final bool isAuthotized,
final bool isAuthorized,
) : _adapter = ApiAdapter(
isWithToken: isAuthotized,
isWithToken: isAuthorized,
region: location,
);

View file

@ -38,9 +38,9 @@ class HetznerServerProvider extends ServerProvider {
HetznerServerProvider() : _adapter = ApiAdapter();
HetznerServerProvider.load(
final String? location,
final bool isAuthotized,
final bool isAuthorized,
) : _adapter = ApiAdapter(
isWithToken: isAuthotized,
isWithToken: isAuthorized,
region: location,
);

View file

@ -2,28 +2,20 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:selfprivacy/config/app_controller/inherited_app_controller.dart';
import 'package:selfprivacy/config/bloc_config.dart';
import 'package:selfprivacy/config/bloc_observer.dart';
import 'package:selfprivacy/config/brand_colors.dart';
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/config/hive_config.dart';
import 'package:selfprivacy/config/localization.dart';
import 'package:selfprivacy/logic/cubit/app_settings/app_settings_cubit.dart';
import 'package:selfprivacy/theming/factory/app_theme_factory.dart';
import 'package:selfprivacy/config/preferences_repository/datasources/preferences_hive_datasource.dart';
import 'package:selfprivacy/config/preferences_repository/inherited_preferences_repository.dart';
import 'package:selfprivacy/ui/pages/errors/failed_to_init_secure_storage.dart';
import 'package:selfprivacy/ui/router/router.dart';
// import 'package:wakelock/wakelock.dart';
import 'package:timezone/data/latest.dart' as tz;
void main() async {
WidgetsFlutterBinding.ensureInitialized();
try {
await HiveConfig.init();
} on PlatformException catch (e) {
runApp(
FailedToInitSecureStorageScreen(e: e),
);
}
// await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
// try {
@ -34,85 +26,117 @@ void main() async {
// print(e);
// }
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;
}
}

View file

@ -42,6 +42,11 @@ abstract class AppThemeFactory {
typography: appTypography,
useMaterial3: true,
scaffoldBackgroundColor: colorScheme.background,
listTileTheme: ListTileThemeData(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
);
return materialThemeData;
@ -50,7 +55,8 @@ abstract class AppThemeFactory {
static Future<ColorScheme?> _getDynamicColors(final Brightness brightness) {
try {
return DynamicColorPlugin.getCorePalette().then(
(final corePallet) => corePallet?.toColorScheme(brightness: brightness),
(final corePallete) =>
corePallete?.toColorScheme(brightness: brightness),
);
} on PlatformException {
return Future.value(null);

View file

@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
class BrandHeader extends StatelessWidget {
class BrandHeader extends StatelessWidget implements PreferredSizeWidget {
const BrandHeader({
super.key,
this.title = '',
@ -8,6 +8,9 @@ class BrandHeader extends StatelessWidget {
this.onBackButtonPressed,
});
@override
Size get preferredSize => const Size.fromHeight(52.0);
final String title;
final bool hasBackButton;
final VoidCallback? onBackButtonPressed;

View file

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

View file

@ -11,15 +11,16 @@ class InfoBox extends StatelessWidget {
final bool isWarning;
@override
Widget build(final BuildContext context) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
Widget build(final BuildContext context) => Wrap(
spacing: 8.0,
runSpacing: 16.0,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
Icon(
isWarning ? Icons.warning_amber_outlined : Icons.info_outline,
size: 24,
color: Theme.of(context).colorScheme.onBackground,
),
const SizedBox(height: 16),
Text(
text,
style: Theme.of(context).textTheme.bodyMedium!.copyWith(

View file

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

View file

@ -1,304 +0,0 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:selfprivacy/logic/models/message.dart';
import 'package:selfprivacy/utils/platform_adapter.dart';
class LogListItem extends StatelessWidget {
const LogListItem({
required this.message,
super.key,
});
final Message message;
@override
Widget build(final BuildContext context) {
final messageItem = message;
if (messageItem is RestApiRequestMessage) {
return _RestApiRequestMessageItem(message: messageItem);
} else if (messageItem is RestApiResponseMessage) {
return _RestApiResponseMessageItem(message: messageItem);
} else if (messageItem is GraphQlResponseMessage) {
return _GraphQlResponseMessageItem(message: messageItem);
} else if (messageItem is GraphQlRequestMessage) {
return _GraphQlRequestMessageItem(message: messageItem);
} else {
return _DefaultMessageItem(message: messageItem);
}
}
}
class _RestApiRequestMessageItem extends StatelessWidget {
const _RestApiRequestMessageItem({required this.message});
final RestApiRequestMessage message;
@override
Widget build(final BuildContext context) => ListTile(
title: Text(
'${message.method}\n${message.uri}',
),
subtitle: Text(message.timeString),
leading: const Icon(Icons.upload_outlined),
iconColor: Theme.of(context).colorScheme.secondary,
onTap: () => showDialog(
context: context,
builder: (final BuildContext context) => AlertDialog(
scrollable: true,
title: Text(
'${message.method}\n${message.uri}',
),
content: Column(
children: [
Text(message.timeString),
const SizedBox(height: 16),
// Headers is a map of key-value pairs
if (message.headers != null) const Text('Headers'),
if (message.headers != null)
Text(
message.headers!.entries
.map((final entry) => '${entry.key}: ${entry.value}')
.join('\n'),
),
if (message.data != null && message.data != 'null')
const Text('Data'),
if (message.data != null && message.data != 'null')
Text(message.data!),
],
),
actions: [
// A button to copy the request to the clipboard
if (message.text != null)
TextButton(
onPressed: () {
PlatformAdapter.setClipboard(message.text ?? '');
},
child: Text('console_page.copy'.tr()),
),
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text('basis.close'.tr()),
),
],
),
),
);
}
class _RestApiResponseMessageItem extends StatelessWidget {
const _RestApiResponseMessageItem({required this.message});
final RestApiResponseMessage message;
@override
Widget build(final BuildContext context) => ListTile(
title: Text(
'${message.statusCode} ${message.method}\n${message.uri}',
),
subtitle: Text(message.timeString),
leading: const Icon(Icons.download_outlined),
iconColor: Theme.of(context).colorScheme.primary,
onTap: () => showDialog(
context: context,
builder: (final BuildContext context) => AlertDialog(
scrollable: true,
title: Text(
'${message.statusCode} ${message.method}\n${message.uri}',
),
content: Column(
children: [
Text(message.timeString),
const SizedBox(height: 16),
// Headers is a map of key-value pairs
if (message.data != null && message.data != 'null')
const Text('Data'),
if (message.data != null && message.data != 'null')
Text(message.data!),
],
),
actions: [
// A button to copy the request to the clipboard
if (message.text != null)
TextButton(
onPressed: () {
PlatformAdapter.setClipboard(message.text ?? '');
},
child: Text('console_page.copy'.tr()),
),
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text('basis.close'.tr()),
),
],
),
),
);
}
class _GraphQlResponseMessageItem extends StatelessWidget {
const _GraphQlResponseMessageItem({required this.message});
final GraphQlResponseMessage message;
@override
Widget build(final BuildContext context) => ListTile(
title: Text(
'GraphQL Response at ${message.timeString}',
),
subtitle: Text(
message.data.toString(),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
leading: const Icon(Icons.arrow_circle_down_outlined),
iconColor: Theme.of(context).colorScheme.tertiary,
onTap: () => showDialog(
context: context,
builder: (final BuildContext context) => AlertDialog(
scrollable: true,
title: Text(
'GraphQL Response at ${message.timeString}',
),
content: Column(
children: [
Text(message.timeString),
const Divider(),
if (message.data != null) const Text('Data'),
// Data is a map of key-value pairs
if (message.data != null)
Text(
message.data!.entries
.map((final entry) => '${entry.key}: ${entry.value}')
.join('\n'),
),
const Divider(),
if (message.errors != null) const Text('Errors'),
if (message.errors != null)
Text(
message.errors!
.map(
(final entry) =>
'${entry.message} at ${entry.locations}',
)
.join('\n'),
),
const Divider(),
if (message.context != null) const Text('Context'),
if (message.context != null)
Text(
message.context!.toString(),
),
],
),
actions: [
// A button to copy the request to the clipboard
if (message.text != null)
TextButton(
onPressed: () {
PlatformAdapter.setClipboard(message.text ?? '');
},
child: Text('console_page.copy'.tr()),
),
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text('basis.close'.tr()),
),
],
),
),
);
}
class _GraphQlRequestMessageItem extends StatelessWidget {
const _GraphQlRequestMessageItem({required this.message});
final GraphQlRequestMessage message;
@override
Widget build(final BuildContext context) => ListTile(
title: Text(
'GraphQL Request at ${message.timeString}',
),
subtitle: Text(
message.operation.toString(),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
leading: const Icon(Icons.arrow_circle_up_outlined),
iconColor: Theme.of(context).colorScheme.secondary,
onTap: () => showDialog(
context: context,
builder: (final BuildContext context) => AlertDialog(
scrollable: true,
title: Text(
'GraphQL Response at ${message.timeString}',
),
content: Column(
children: [
Text(message.timeString),
const Divider(),
if (message.operation != null) const Text('Operation'),
// Data is a map of key-value pairs
if (message.operation != null)
Text(
message.operation!.toString(),
),
const Divider(),
if (message.variables != null) const Text('Variables'),
if (message.variables != null)
Text(
message.variables!.entries
.map((final entry) => '${entry.key}: ${entry.value}')
.join('\n'),
),
const Divider(),
if (message.context != null) const Text('Context'),
if (message.context != null)
Text(
message.context!.toString(),
),
],
),
actions: [
// A button to copy the request to the clipboard
if (message.text != null)
TextButton(
onPressed: () {
PlatformAdapter.setClipboard(message.text ?? '');
},
child: Text('console_page.copy'.tr()),
),
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text('basis.close'.tr()),
),
],
),
),
);
}
class _DefaultMessageItem extends StatelessWidget {
const _DefaultMessageItem({required this.message});
final Message message;
@override
Widget build(final BuildContext context) => Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: RichText(
text: TextSpan(
style: DefaultTextStyle.of(context).style,
children: <TextSpan>[
TextSpan(
text: '${message.timeString}: \n',
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
TextSpan(text: message.text),
],
),
),
);
}

View file

@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:selfprivacy/ui/components/not_ready_card/not_ready_card.dart';
class EmptyPagePlaceholder extends StatelessWidget {
@ -10,50 +11,72 @@ class EmptyPagePlaceholder extends StatelessWidget {
super.key,
});
final bool showReadyCard;
final IconData iconData;
final String title;
final String description;
final IconData iconData;
final bool showReadyCard;
@override
Widget build(final BuildContext context) => !showReadyCard
? _expandedContent(context)
: Column(
Widget build(final BuildContext context) => showReadyCard
? Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
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,

View file

@ -1,295 +0,0 @@
import 'dart:io';
import 'package:auto_route/auto_route.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:selfprivacy/ui/components/drawers/support_drawer.dart';
import 'package:selfprivacy/ui/components/pre_styled_buttons/flash_fab.dart';
import 'package:selfprivacy/ui/router/root_destinations.dart';
import 'package:selfprivacy/utils/breakpoints.dart';
class RootScaffoldWithNavigation extends StatelessWidget {
const RootScaffoldWithNavigation({
required this.child,
required this.title,
required this.destinations,
this.showBottomBar = true,
this.showFab = true,
super.key,
});
final Widget child;
final String title;
final bool showBottomBar;
final List<RouteDestination> destinations;
final bool showFab;
@override
// ignore: prefer_expression_function_bodies
Widget build(final BuildContext context) {
return Scaffold(
appBar: Breakpoints.mediumAndUp.isActive(context)
? PreferredSize(
preferredSize: const Size.fromHeight(52),
child: _RootAppBar(title: title),
)
: null,
endDrawer: const SupportDrawer(),
endDrawerEnableOpenDragGesture: false,
body: Row(
children: [
if (Breakpoints.medium.isActive(context))
_MainScreenNavigationRail(
destinations: destinations,
showFab: showFab,
),
if (Breakpoints.large.isActive(context))
_MainScreenNavigationDrawer(
destinations: destinations,
showFab: showFab,
),
Expanded(child: child),
],
),
bottomNavigationBar: _BottomBar(
destinations: destinations,
hidden: !(Breakpoints.small.isActive(context) && showBottomBar),
key: const Key('bottomBar'),
),
floatingActionButton:
showFab && Breakpoints.small.isActive(context) && showBottomBar
? const BrandFab()
: null,
);
}
}
class _RootAppBar extends StatelessWidget {
const _RootAppBar({
required this.title,
});
final String title;
@override
Widget build(final BuildContext context) => AppBar(
title: AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
transitionBuilder:
(final Widget child, final Animation<double> animation) =>
SlideTransition(
position: animation.drive(
Tween<Offset>(
begin: const Offset(0.0, 0.2),
end: Offset.zero,
),
),
child: FadeTransition(
opacity: animation,
child: child,
),
),
child: SizedBox(
key: ValueKey<String>(title),
width: double.infinity,
child: Text(
title,
),
),
),
leading: context.router.pageCount > 1
? IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => context.router.maybePop(),
)
: null,
actions: const [
SizedBox.shrink(),
],
);
}
class _MainScreenNavigationRail extends StatelessWidget {
const _MainScreenNavigationRail({
required this.destinations,
this.showFab = true,
});
final List<RouteDestination> destinations;
final bool showFab;
@override
Widget build(final BuildContext context) {
int? activeIndex = destinations.indexWhere(
(final destination) =>
context.router.isRouteActive(destination.route.routeName),
);
final prevActiveIndex = destinations.indexWhere(
(final destination) => context.router.stack
.any((final route) => route.name == destination.route.routeName),
);
if (activeIndex == -1) {
if (prevActiveIndex != -1) {
activeIndex = prevActiveIndex;
} else {
activeIndex = 0;
}
}
final isExtended = Breakpoints.large.isActive(context);
return LayoutBuilder(
builder: (final context, final constraints) => SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(minHeight: constraints.maxHeight),
child: IntrinsicHeight(
child: NavigationRail(
backgroundColor: Colors.transparent,
labelType: isExtended
? NavigationRailLabelType.none
: NavigationRailLabelType.all,
extended: isExtended,
leading: showFab
? const BrandFab(
extended: false,
)
: null,
groupAlignment: 0.0,
destinations: destinations
.map(
(final destination) => NavigationRailDestination(
icon: Icon(destination.icon),
label: Text(destination.label),
),
)
.toList(),
selectedIndex: activeIndex,
onDestinationSelected: (final index) {
context.router.replaceAll([destinations[index].route]);
},
),
),
),
),
);
}
}
class _BottomBar extends StatelessWidget {
const _BottomBar({
required this.destinations,
required this.hidden,
super.key,
});
final List<RouteDestination> destinations;
final bool hidden;
@override
Widget build(final BuildContext context) {
final prevActiveIndex = destinations.indexWhere(
(final destination) => context.router.stack
.any((final route) => route.name == destination.route.routeName),
);
return AnimatedContainer(
duration: const Duration(milliseconds: 500),
height: hidden ? 0 : 80,
curve: Curves.easeInOutCubicEmphasized,
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
color: Theme.of(context).scaffoldBackgroundColor,
),
child: Platform.isIOS
? CupertinoTabBar(
currentIndex: prevActiveIndex == -1 ? 0 : prevActiveIndex,
onTap: (final index) {
context.router.replaceAll([destinations[index].route]);
},
items: destinations
.map(
(final destination) => BottomNavigationBarItem(
icon: Icon(destination.icon),
label: destination.label,
),
)
.toList(),
)
: NavigationBar(
selectedIndex: prevActiveIndex == -1 ? 0 : prevActiveIndex,
labelBehavior: NavigationDestinationLabelBehavior.alwaysShow,
onDestinationSelected: (final index) {
context.router.replaceAll([destinations[index].route]);
},
destinations: destinations
.map(
(final destination) => NavigationDestination(
icon: Icon(destination.icon),
label: destination.label,
),
)
.toList(),
),
);
}
}
class _MainScreenNavigationDrawer extends StatelessWidget {
const _MainScreenNavigationDrawer({
required this.destinations,
this.showFab = true,
});
final List<RouteDestination> destinations;
final bool showFab;
@override
Widget build(final BuildContext context) {
int? activeIndex = destinations.indexWhere(
(final destination) =>
context.router.isRouteActive(destination.route.routeName),
);
final prevActiveIndex = destinations.indexWhere(
(final destination) => context.router.stack
.any((final route) => route.name == destination.route.routeName),
);
if (activeIndex == -1) {
if (prevActiveIndex != -1) {
activeIndex = prevActiveIndex;
} else {
activeIndex = 0;
}
}
return SizedBox(
height: MediaQuery.of(context).size.height,
width: 296,
child: NavigationDrawer(
key: const Key('PrimaryNavigationDrawer'),
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
surfaceTintColor: Colors.transparent,
selectedIndex: activeIndex,
onDestinationSelected: (final index) {
context.router.replaceAll([destinations[index].route]);
},
children: [
const Padding(
padding: EdgeInsets.all(16.0),
child: BrandFab(extended: true),
),
const SizedBox(height: 16),
...destinations.map(
(final destination) => NavigationDrawerDestination(
icon: Icon(destination.icon),
label: Text(destination.label),
),
),
],
),
);
}
}

View file

@ -0,0 +1,50 @@
part of 'root_scaffold_with_subroute_selector.dart';
class _BottomTabBar extends SubrouteSelector {
const _BottomTabBar({
required super.subroutes,
required this.hidden,
super.key,
});
final bool hidden;
@override
Widget build(final BuildContext context) {
final int activeIndex = getActiveIndex(context);
return AnimatedContainer(
duration: const Duration(milliseconds: 500),
height: hidden ? 0 : 80,
curve: Curves.easeInOutCubicEmphasized,
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
color: Theme.of(context).scaffoldBackgroundColor,
),
child: Platform.isIOS
? CupertinoTabBar(
currentIndex: activeIndex,
onTap: openSubpage(context),
items: [
for (final destination in subroutes)
BottomNavigationBarItem(
icon: Icon(destination.icon),
label: destination.label.tr(),
),
],
)
: NavigationBar(
selectedIndex: activeIndex,
labelBehavior: NavigationDestinationLabelBehavior.alwaysShow,
onDestinationSelected: openSubpage(context),
destinations: [
for (final destination in subroutes)
NavigationDestination(
icon: Icon(destination.icon),
label: destination.label.tr(),
),
].toList(),
),
);
}
}

View file

@ -0,0 +1,35 @@
part of 'root_scaffold_with_subroute_selector.dart';
class _NavigationDrawer extends SubrouteSelector {
const _NavigationDrawer({
required super.subroutes,
this.showFab = true,
});
final bool showFab;
@override
Widget build(final BuildContext context) => SizedBox(
height: MediaQuery.of(context).size.height,
width: 296,
child: NavigationDrawer(
key: const Key('PrimaryNavigationDrawer'),
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
surfaceTintColor: Colors.transparent,
selectedIndex: getActiveIndex(context),
onDestinationSelected: openSubpage(context),
children: [
const Padding(
padding: EdgeInsets.all(16.0),
child: BrandFab(extended: true),
),
const SizedBox(height: 16),
for (final destination in subroutes)
NavigationDrawerDestination(
icon: Icon(destination.icon),
label: Text(destination.label.tr()),
),
],
),
);
}

View file

@ -0,0 +1,47 @@
part of 'root_scaffold_with_subroute_selector.dart';
class _NavigationRail extends SubrouteSelector {
const _NavigationRail({
required super.subroutes,
this.showFab = true,
});
final bool showFab;
@override
Widget build(final BuildContext context) {
final isExtended = Breakpoints.large.isActive(context);
return LayoutBuilder(
builder: (final context, final constraints) => SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(minHeight: constraints.maxHeight),
child: IntrinsicHeight(
child: NavigationRail(
backgroundColor: Colors.transparent,
labelType: isExtended
? NavigationRailLabelType.none
: NavigationRailLabelType.all,
extended: isExtended,
leading: showFab
? const BrandFab(
extended: false,
)
: null,
groupAlignment: 0.0,
destinations: [
for (final destination in subroutes)
NavigationRailDestination(
icon: Icon(destination.icon),
label: Text(destination.label.tr()),
),
],
selectedIndex: getActiveIndex(context),
onDestinationSelected: openSubpage(context),
),
),
),
),
);
}
}

View file

@ -0,0 +1,50 @@
part of 'root_scaffold_with_subroute_selector.dart';
class _RootAppBar extends StatelessWidget implements PreferredSizeWidget {
const _RootAppBar({
required this.title,
});
final String title;
@override
Size get preferredSize => const Size.fromHeight(52);
@override
Widget build(final BuildContext context) => AppBar(
title: AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
transitionBuilder:
(final Widget child, final Animation<double> animation) =>
SlideTransition(
position: animation.drive(
Tween<Offset>(
begin: const Offset(0.0, 0.2),
end: Offset.zero,
),
),
child: FadeTransition(
opacity: animation,
child: child,
),
),
child: SizedBox(
key: ValueKey<String>(title),
width: double.infinity,
child: Text(
title,
maxLines: 1,
textAlign: TextAlign.start,
overflow: TextOverflow.fade,
),
),
),
leading: context.router.pageCount > 1
? IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => context.router.maybePop(),
)
: null,
actions: const [SizedBox.shrink()],
);
}

View file

@ -0,0 +1,67 @@
import 'dart:io';
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:selfprivacy/ui/components/drawers/support_drawer.dart';
import 'package:selfprivacy/ui/components/pre_styled_buttons/flash_fab.dart';
import 'package:selfprivacy/ui/router/root_destinations.dart';
import 'package:selfprivacy/ui/router/router.dart';
import 'package:selfprivacy/utils/breakpoints.dart';
part 'bottom_tab_bar.dart';
part 'navigation_drawer.dart';
part 'navigation_rail.dart';
part 'root_app_bar.dart';
part 'subroute_selector.dart';
class RootScaffoldWithSubrouteSelector extends StatelessWidget {
const RootScaffoldWithSubrouteSelector({
required this.child,
required this.destinations,
this.showBottomBar = true,
this.showFab = true,
super.key,
});
final Widget child;
final bool showBottomBar;
final List<RouteDestination> destinations;
final bool showFab;
@override
Widget build(final BuildContext context) => Scaffold(
appBar: Breakpoints.mediumAndUp.isActive(context)
? _RootAppBar(
title: getRouteTitle(context.router.current.name).tr(),
)
: null,
endDrawer: const SupportDrawer(),
endDrawerEnableOpenDragGesture: false,
body: Row(
children: [
if (Breakpoints.medium.isActive(context))
_NavigationRail(
subroutes: destinations,
showFab: showFab,
)
else if (Breakpoints.large.isActive(context))
_NavigationDrawer(
subroutes: destinations,
showFab: showFab,
),
Expanded(child: child),
],
),
bottomNavigationBar: _BottomTabBar(
key: const ValueKey('bottomBar'),
subroutes: destinations,
hidden: !(Breakpoints.small.isActive(context) && showBottomBar),
),
floatingActionButton:
showFab && Breakpoints.small.isActive(context) && showBottomBar
? const BrandFab()
: null,
);
}

View file

@ -0,0 +1,33 @@
part of 'root_scaffold_with_subroute_selector.dart';
abstract class SubrouteSelector extends StatelessWidget {
const SubrouteSelector({
required this.subroutes,
super.key,
});
final List<RouteDestination> subroutes;
int getActiveIndex(final BuildContext context) {
int activeIndex = subroutes.indexWhere(
(final destination) =>
context.router.isRouteActive(destination.route.routeName),
);
final prevActiveIndex = subroutes.indexWhere(
(final destination) => context.router.stack.any(
(final route) => route.name == destination.route.routeName,
),
);
if (activeIndex == -1) {
activeIndex = prevActiveIndex != -1 ? prevActiveIndex : 0;
}
return activeIndex;
}
ValueSetter<int> openSubpage(final BuildContext context) => (final index) {
context.router.replaceAll([subroutes[index].route]);
};
}

View file

@ -39,6 +39,7 @@ class NewDeviceScreen extends StatelessWidget {
class _KeyDisplay extends StatelessWidget {
const _KeyDisplay({required this.newDeviceKey});
final String newDeviceKey;
@override
@ -47,7 +48,7 @@ class _KeyDisplay extends StatelessWidget {
children: [
const Divider(),
const SizedBox(height: 16),
Text(
SelectableText(
newDeviceKey,
style: Theme.of(context).textTheme.bodyLarge!.copyWith(
fontSize: 24,

View file

@ -3,7 +3,7 @@ import 'dart:io';
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:package_info/package_info.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/ui/components/list_tiles/section_title.dart';
import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart';

View file

@ -1,10 +1,17 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:selfprivacy/logic/cubit/app_settings/app_settings_cubit.dart';
import 'package:gap/gap.dart';
import 'package:selfprivacy/config/app_controller/inherited_app_controller.dart';
import 'package:selfprivacy/config/localization.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
import 'package:selfprivacy/ui/components/buttons/dialog_action_button.dart';
import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart';
import 'package:selfprivacy/ui/router/router.dart';
part 'language_picker.dart';
part 'reset_app_button.dart';
part 'theme_picker.dart';
@RoutePage()
class AppSettingsPage extends StatefulWidget {
@ -16,82 +23,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()),
),
],
);
}

View file

@ -1,11 +1,12 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:selfprivacy/config/app_controller/inherited_app_controller.dart';
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/api_maps/tls_options.dart';
import 'package:selfprivacy/logic/bloc/services/services_bloc.dart';
import 'package:selfprivacy/logic/bloc/volumes/volumes_bloc.dart';
import 'package:selfprivacy/logic/cubit/app_settings/app_settings_cubit.dart';
import 'package:selfprivacy/ui/components/list_tiles/section_title.dart';
import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart';
import 'package:selfprivacy/ui/router/router.dart';
@ -60,17 +61,14 @@ class _DeveloperSettingsPageState extends State<DeveloperSettingsPage> {
title: Text('developer_settings.reset_onboarding'.tr()),
subtitle:
Text('developer_settings.reset_onboarding_description'.tr()),
enabled:
!context.watch<AppSettingsCubit>().state.isOnboardingShowing,
onTap: () => context
.read<AppSettingsCubit>()
.turnOffOnboarding(isOnboardingShowing: true),
enabled: !InheritedAppController.of(context).shouldShowOnboarding,
onTap: () => InheritedAppController.of(context)
.setShouldShowOnboarding(true),
),
ListTile(
title: Text('storage.start_migration_button'.tr()),
subtitle: Text('storage.data_migration_notice'.tr()),
enabled:
!context.watch<AppSettingsCubit>().state.isOnboardingShowing,
enabled: InheritedAppController.of(context).shouldShowOnboarding,
onTap: () => context.pushRoute(
ServicesMigrationRoute(
diskStatus: context.read<VolumesBloc>().state.diskStatus,

View file

@ -0,0 +1,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),
),
],
);
}
}

View file

@ -0,0 +1,42 @@
part of 'app_settings.dart';
class _ResetAppTile extends StatelessWidget {
const _ResetAppTile({super.key});
@override
Widget build(final BuildContext context) => ListTile(
title: Text('application_settings.reset_config_title'.tr()),
subtitle: Text('application_settings.reset_config_description'.tr()),
onTap: () => showDialog(
context: context,
builder: (final context) => const _ResetAppDialog(),
),
);
}
class _ResetAppDialog extends StatelessWidget {
const _ResetAppDialog();
@override
Widget build(final BuildContext context) => AlertDialog(
title: Text('modals.are_you_sure'.tr()),
content: Text('modals.purge_all_keys'.tr()),
actions: [
DialogActionButton(
text: 'modals.purge_all_keys_confirm'.tr(),
isRed: true,
onPressed: () {
context.read<ServerInstallationCubit>().clearAppConfig();
context.router.maybePop([
const RootRoute(),
]);
context.resetLocale();
},
),
DialogActionButton(
text: 'basis.cancel'.tr(),
),
],
);
}

View file

@ -0,0 +1,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,
),
],
);
}
}

View file

@ -1,107 +0,0 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/models/message.dart';
import 'package:selfprivacy/ui/components/list_tiles/log_list_tile.dart';
@RoutePage()
class ConsolePage extends StatefulWidget {
const ConsolePage({super.key});
@override
State<ConsolePage> createState() => _ConsolePageState();
}
class _ConsolePageState extends State<ConsolePage> {
bool paused = false;
@override
void initState() {
super.initState();
getIt<ConsoleModel>().addListener(update);
}
@override
void dispose() {
getIt<ConsoleModel>().removeListener(update);
super.dispose();
}
void update() {
/// listener update could come at any time, like when widget is already
/// unmounted or during frame build, adding as postframe callback ensures
/// that element is marked for rebuild
WidgetsBinding.instance.addPostFrameCallback((final _) {
if (!paused && mounted) {
setState(() => {});
}
});
}
void togglePause() {
paused ^= true;
setState(() {});
}
@override
Widget build(final BuildContext context) => SafeArea(
child: Scaffold(
appBar: AppBar(
title: Text('console_page.title'.tr()),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => Navigator.of(context).pop(),
),
actions: [
IconButton(
icon: Icon(
paused ? Icons.play_arrow_outlined : Icons.pause_outlined,
),
onPressed: togglePause,
),
],
),
body: FutureBuilder(
future: getIt.allReady(),
builder: (
final BuildContext context,
final AsyncSnapshot<void> snapshot,
) {
if (snapshot.hasData) {
final List<Message> messages =
getIt.get<ConsoleModel>().messages;
return ListView(
reverse: true,
shrinkWrap: true,
children: [
const Gap(20),
...messages.reversed.map(
(final message) => LogListItem(
key: ValueKey(message),
message: message,
),
),
],
);
} else {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text('console_page.waiting'.tr()),
const Gap(16),
const CircularProgressIndicator.adaptive(),
],
);
}
},
),
),
);
}

View file

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

View file

@ -0,0 +1,75 @@
import 'package:flutter/material.dart';
import 'package:selfprivacy/logic/models/console_log.dart';
import 'package:selfprivacy/ui/pages/more/console/console_log_item_dialog.dart';
extension on ConsoleLog {
Color resolveColor(final BuildContext context) => isError
? Theme.of(context).colorScheme.error
: switch (this) {
(final RestApiRequestConsoleLog _) =>
Theme.of(context).colorScheme.secondary,
(final RestApiResponseConsoleLog _) =>
Theme.of(context).colorScheme.primary,
(final GraphQlRequestConsoleLog _) =>
Theme.of(context).colorScheme.secondary,
(final GraphQlResponseConsoleLog _) =>
Theme.of(context).colorScheme.tertiary,
(final ManualConsoleLog _) => Theme.of(context).colorScheme.tertiary,
};
IconData resolveIcon() => switch (this) {
(final RestApiRequestConsoleLog _) => Icons.upload_outlined,
(final RestApiResponseConsoleLog _) => Icons.download_outlined,
(final GraphQlRequestConsoleLog _) => Icons.arrow_circle_up_outlined,
(final GraphQlResponseConsoleLog _) => Icons.arrow_circle_down_outlined,
(final ManualConsoleLog _) => Icons.read_more_outlined,
};
}
class ConsoleLogItemWidget extends StatelessWidget {
const ConsoleLogItemWidget({
required this.log,
super.key,
});
final ConsoleLog log;
@override
Widget build(final BuildContext context) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: ListTile(
dense: true,
title: Text.rich(
TextSpan(
style: DefaultTextStyle.of(context).style,
children: <TextSpan>[
TextSpan(
text: '${log.timeString}: ',
style: const TextStyle(
fontFeatures: [FontFeature.tabularFigures()],
),
),
TextSpan(
text: log.title,
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
],
),
),
subtitle: Text(
log.content,
overflow: TextOverflow.ellipsis,
maxLines: 3,
),
leading: Icon(log.resolveIcon()),
iconColor: log.resolveColor(context),
onTap: () => showDialog(
context: context,
builder: (final BuildContext context) =>
ConsoleItemDialog(log: log),
),
),
);
}

View file

@ -0,0 +1,143 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/models/console_log.dart';
import 'package:selfprivacy/ui/pages/more/console/console_log_item_widget.dart';
/// listing with 500 latest app operations.
@RoutePage()
class ConsolePage extends StatefulWidget {
const ConsolePage({super.key});
@override
State<ConsolePage> createState() => _ConsolePageState();
}
class _ConsolePageState extends State<ConsolePage> {
ConsoleModel get console => getIt<ConsoleModel>();
/// should freeze logs state to properly read logs
late final Future<void> future;
@override
void initState() {
super.initState();
future = getIt.allReady();
console.addListener(update);
}
@override
void dispose() {
console.removeListener(update);
super.dispose();
}
void update() {
/// listener update could come at any time, like when widget is already
/// unmounted or during frame build, adding as postframe callback ensures
/// that element is marked for rebuild
WidgetsBinding.instance.addPostFrameCallback((final _) {
if (mounted) {
setState(() => {});
}
});
}
@override
Widget build(final BuildContext context) => SafeArea(
child: Scaffold(
appBar: AppBar(
title: Text('console_page.title'.tr()),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => Navigator.of(context).maybePop(),
),
actions: [
IconButton(
icon: Icon(
console.paused
? Icons.play_arrow_outlined
: Icons.pause_outlined,
),
onPressed: console.paused ? console.play : console.pause,
),
],
),
body: Scrollbar(
child: FutureBuilder(
future: future,
builder: (
final BuildContext context,
final AsyncSnapshot<void> snapshot,
) {
if (snapshot.hasData) {
final List<ConsoleLog> logs = console.logs;
return logs.isEmpty
? const _ConsoleViewEmpty()
: _ConsoleViewLoaded(logs: logs);
}
return const _ConsoleViewLoading();
},
),
),
),
);
}
class _ConsoleViewLoading extends StatelessWidget {
const _ConsoleViewLoading();
@override
Widget build(final BuildContext context) => Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text('console_page.waiting'.tr()),
const Gap(16),
const Expanded(
child: Center(
child: CircularProgressIndicator.adaptive(),
),
),
],
);
}
class _ConsoleViewEmpty extends StatelessWidget {
const _ConsoleViewEmpty();
@override
Widget build(final BuildContext context) => Align(
alignment: Alignment.topCenter,
child: Text('console_page.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(),
);
}

View file

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

View file

@ -1,6 +1,6 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:selfprivacy/logic/cubit/app_settings/app_settings_cubit.dart';
import 'package:selfprivacy/config/app_controller/inherited_app_controller.dart';
import 'package:selfprivacy/ui/pages/onboarding/views/views.dart';
import 'package:selfprivacy/ui/router/router.dart';
@ -37,7 +37,8 @@ class _OnboardingPageState extends State<OnboardingPage> {
),
OnboardingSecondView(
onProceed: () {
context.read<AppSettingsCubit>().turnOffOnboarding();
InheritedAppController.of(context)
.setShouldShowOnboarding(false);
context.router.replaceAll([
const RootRoute(),
const InitializingRoute(),

View file

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

View file

@ -1,9 +1,8 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:selfprivacy/logic/cubit/app_settings/app_settings_cubit.dart';
import 'package:selfprivacy/config/app_controller/inherited_app_controller.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
import 'package:selfprivacy/ui/layouts/root_scaffold_with_navigation.dart';
import 'package:selfprivacy/ui/layouts/root_scaffold_with_subroute_selector/root_scaffold_with_subroute_selector.dart';
import 'package:selfprivacy/ui/router/root_destinations.dart';
import 'package:selfprivacy/ui/router/router.dart';
@ -19,31 +18,31 @@ class RootPage extends StatefulWidget implements AutoRouteWrapper {
}
class _RootPageState extends State<RootPage> with TickerProviderStateMixin {
bool shouldUseSplitView() => false;
@override
void didChangeDependencies() {
if (InheritedAppController.of(context).shouldShowOnboarding) {
context.router.replace(const OnboardingRoute());
}
final destinations = rootDestinations;
super.didChangeDependencies();
}
@override
Widget build(final BuildContext context) {
final bool isReady = context.watch<ServerInstallationCubit>().state
is ServerInstallationFinished;
if (context.read<AppSettingsCubit>().state.isOnboardingShowing) {
context.router.replace(const OnboardingRoute());
}
return AutoRouter(
builder: (final context, final child) {
final currentDestinationIndex = destinations.indexWhere(
final currentDestinationIndex = rootDestinations.indexWhere(
(final destination) =>
context.router.isRouteActive(destination.route.routeName),
);
final isOtherRouterActive =
context.router.root.current.name != RootRoute.name;
final routeName = getRouteTitle(context.router.current.name).tr();
return RootScaffoldWithNavigation(
title: routeName,
destinations: destinations,
return RootScaffoldWithSubrouteSelector(
destinations: rootDestinations,
showBottomBar:
!(currentDestinationIndex == -1 && !isOtherRouterActive),
showFab: isReady,
@ -53,99 +52,3 @@ class _RootPageState extends State<RootPage> with TickerProviderStateMixin {
);
}
}
class MainScreenNavigationRail extends StatelessWidget {
const MainScreenNavigationRail({
required this.destinations,
super.key,
});
final List<RouteDestination> destinations;
@override
Widget build(final BuildContext context) {
int? activeIndex = destinations.indexWhere(
(final destination) =>
context.router.isRouteActive(destination.route.routeName),
);
if (activeIndex == -1) {
activeIndex = null;
}
return Padding(
padding: const EdgeInsets.all(8.0),
child: SizedBox(
height: MediaQuery.of(context).size.height,
width: 72,
child: LayoutBuilder(
builder: (final context, final constraints) => SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(minHeight: constraints.maxHeight),
child: IntrinsicHeight(
child: NavigationRail(
backgroundColor: Colors.transparent,
labelType: NavigationRailLabelType.all,
destinations: destinations
.map(
(final destination) => NavigationRailDestination(
icon: Icon(destination.icon),
label: Text(destination.label),
),
)
.toList(),
selectedIndex: activeIndex,
onDestinationSelected: (final index) {
context.router.replaceAll([destinations[index].route]);
},
),
),
),
),
),
),
);
}
}
class MainScreenNavigationDrawer extends StatelessWidget {
const MainScreenNavigationDrawer({
required this.destinations,
super.key,
});
final List<RouteDestination> destinations;
@override
Widget build(final BuildContext context) {
int? activeIndex = destinations.indexWhere(
(final destination) =>
context.router.isRouteActive(destination.route.routeName),
);
if (activeIndex == -1) {
activeIndex = null;
}
return SizedBox(
height: MediaQuery.of(context).size.height,
width: 296,
child: LayoutBuilder(
builder: (final context, final constraints) => NavigationDrawer(
key: const Key('PrimaryNavigationDrawer'),
selectedIndex: activeIndex,
onDestinationSelected: (final index) {
context.router.replaceAll([destinations[index].route]);
},
children: [
const SizedBox(height: 18),
...destinations.map(
(final destination) => NavigationDrawerDestination(
icon: Icon(destination.icon),
label: Text(destination.label),
),
),
],
),
),
);
}
}

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