mirror of
https://git.selfprivacy.org/kherel/selfprivacy.org.app.git
synced 2024-11-09 18:33:11 +00:00
Merge branch 'master' into translations
# Conflicts: # assets/markdown/how_fallback_ssh-ja.md # assets/translations/ar.json # assets/translations/az.json # assets/translations/be.json # assets/translations/cs.json # assets/translations/de.json # assets/translations/es.json # assets/translations/et.json # assets/translations/fr.json # assets/translations/kk.json # assets/translations/pl.json # assets/translations/ru.json # assets/translations/sk.json # assets/translations/th.json # assets/translations/uk.json # assets/translations/zh-Hans.json
This commit is contained in:
commit
8318a7a7eb
53
.vscode/launch.json
vendored
Normal file
53
.vscode/launch.json
vendored
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
{
|
||||||
|
// Use IntelliSense to learn about possible attributes.
|
||||||
|
// Hover to view descriptions of existing attributes.
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "debug",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "dart"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "profile mode",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "dart",
|
||||||
|
"flutterMode": "profile"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "release mode",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "dart",
|
||||||
|
"flutterMode": "release"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "debug (fdroid)",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "dart",
|
||||||
|
"args": [
|
||||||
|
"--flavor",
|
||||||
|
"fdroid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "debug (production)",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "dart",
|
||||||
|
"args": [
|
||||||
|
"--flavor",
|
||||||
|
"production"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "debug (nightly)",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "dart",
|
||||||
|
"args": [
|
||||||
|
"--flavor",
|
||||||
|
"nightly"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -1,3 +1,10 @@
|
||||||
|
plugins {
|
||||||
|
id "com.android.application"
|
||||||
|
id "kotlin-android"
|
||||||
|
id "dev.flutter.flutter-gradle-plugin"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def localProperties = new Properties()
|
def localProperties = new Properties()
|
||||||
def localPropertiesFile = rootProject.file('local.properties')
|
def localPropertiesFile = rootProject.file('local.properties')
|
||||||
if (localPropertiesFile.exists()) {
|
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')
|
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
|
||||||
if (flutterVersionCode == null) {
|
if (flutterVersionCode == null) {
|
||||||
|
@ -21,14 +24,9 @@ if (flutterVersionName == null) {
|
||||||
flutterVersionName = '1.0'
|
flutterVersionName = '1.0'
|
||||||
}
|
}
|
||||||
|
|
||||||
apply plugin: 'com.android.application'
|
|
||||||
apply plugin: 'kotlin-android'
|
|
||||||
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
|
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace 'org.selfprivacy.app'
|
namespace 'org.selfprivacy.app'
|
||||||
|
compileSdkVersion flutter.compileSdkVersion
|
||||||
compileSdkVersion flutter.compileSdkVersion
|
|
||||||
ndkVersion flutter.ndkVersion
|
ndkVersion flutter.ndkVersion
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
|
@ -43,13 +41,16 @@ android {
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = '1.8'
|
jvmTarget = '1.8'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
main.java.srcDirs += 'src/main/kotlin'
|
||||||
|
}
|
||||||
|
|
||||||
lintOptions {
|
lintOptions {
|
||||||
disable 'InvalidPackage'
|
disable 'InvalidPackage'
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
|
||||||
applicationId "org.selfprivacy.app"
|
applicationId "org.selfprivacy.app"
|
||||||
minSdkVersion 21
|
minSdkVersion 21
|
||||||
targetSdkVersion 34
|
targetSdkVersion 34
|
||||||
|
@ -57,31 +58,33 @@ android {
|
||||||
versionCode flutterVersionCode.toInteger()
|
versionCode flutterVersionCode.toInteger()
|
||||||
versionName flutterVersionName
|
versionName flutterVersionName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
debug {
|
||||||
|
|
||||||
flavorDimensions "default"
|
|
||||||
productFlavors {
|
|
||||||
fdroid {
|
|
||||||
applicationId "pro.kherel.selfprivacy"
|
|
||||||
}
|
}
|
||||||
production {
|
profile {
|
||||||
applicationIdSuffix ""
|
|
||||||
}
|
}
|
||||||
nightly {
|
release {
|
||||||
applicationIdSuffix ".nightly"
|
|
||||||
versionCode project.getVersionCode()
|
|
||||||
versionName "nightly-" + project.getVersionCode()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
buildFeatures {
|
||||||
|
flavorDimensions = ["default"]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
flavorDimensions "default"
|
|
||||||
productFlavors {
|
productFlavors {
|
||||||
fdroid {
|
fdroid {
|
||||||
|
dimension 'default'
|
||||||
applicationId "pro.kherel.selfprivacy"
|
applicationId "pro.kherel.selfprivacy"
|
||||||
}
|
}
|
||||||
production {
|
production {
|
||||||
applicationIdSuffix ""
|
dimension 'default'
|
||||||
}
|
}
|
||||||
nightly {
|
nightly {
|
||||||
|
dimension 'default'
|
||||||
applicationIdSuffix ".nightly"
|
applicationIdSuffix ".nightly"
|
||||||
versionCode project.getVersionCode()
|
versionCode project.getVersionCode()
|
||||||
versionName "nightly-" + project.getVersionCode()
|
versionName "nightly-" + project.getVersionCode()
|
||||||
|
@ -93,6 +96,5 @@ flutter {
|
||||||
source '../..'
|
source '../..'
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {}
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
buildscript {
|
buildscript {
|
||||||
ext.kotlin_version = '1.9.21'
|
ext.getVersionCode = { ->
|
||||||
ext.getVersionCode = { ->
|
|
||||||
try {
|
try {
|
||||||
def stdout = new ByteArrayOutputStream()
|
def stdout = new ByteArrayOutputStream()
|
||||||
exec {
|
exec {
|
||||||
|
@ -13,15 +12,6 @@ buildscript {
|
||||||
return -1
|
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 {
|
allprojects {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
org.gradle.jvmargs=-Xmx1536M
|
org.gradle.jvmargs=-Xmx4G
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
android.enableJetifier=true
|
android.enableJetifier=true
|
||||||
android.bundle.enableUncompressedNativeLibs=false
|
android.bundle.enableUncompressedNativeLibs=false
|
||||||
|
|
|
@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-all.zip
|
||||||
|
|
|
@ -1,11 +1,25 @@
|
||||||
include ':app'
|
pluginManagement {
|
||||||
|
def flutterSdkPath = {
|
||||||
|
def properties = new Properties()
|
||||||
|
file("local.properties").withInputStream { properties.load(it) }
|
||||||
|
def flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||||
|
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
|
||||||
|
return flutterSdkPath
|
||||||
|
}()
|
||||||
|
|
||||||
def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
|
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
|
||||||
def properties = new Properties()
|
|
||||||
|
|
||||||
assert localPropertiesFile.exists()
|
repositories {
|
||||||
localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
gradlePluginPortal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
def flutterSdkPath = properties.getProperty("flutter.sdk")
|
plugins {
|
||||||
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
|
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||||
apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
|
id "com.android.application" version "7.3.0" apply false
|
||||||
|
id "org.jetbrains.kotlin.android" version "1.9.21" apply false
|
||||||
|
}
|
||||||
|
|
||||||
|
include ":app"
|
|
@ -1,19 +1,7 @@
|
||||||
Login as root user to your server and look at the contents of the file `/etc/nixos/userdata/tokens.json`
|
[Login as root user to your server](https://selfprivacy.org/docs/how-to-guides/root_ssh/) and enter this command:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cat /etc/nixos/userdata/tokens.json
|
sp-print-api-token
|
||||||
```
|
```
|
||||||
|
|
||||||
This file will have a similar construction:
|
Copy the token from the terminal and paste it in the next window.
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"tokens": [
|
|
||||||
{
|
|
||||||
"token": "token_to_copy",
|
|
||||||
"name": "device_name",
|
|
||||||
"date": "date"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Copy the token from the file and paste it in the next window.
|
|
||||||
|
|
|
@ -1,19 +1,7 @@
|
||||||
Login as root user to your server and look at the contents of the file `/etc/nixos/userdata/tokens.json`
|
[Login as root user to your server](https://selfprivacy.org/docs/how-to-guides/root_ssh/) and enter this command:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cat /etc/nixos/userdata/tokens.json
|
sp-print-api-token
|
||||||
```
|
```
|
||||||
|
|
||||||
This file will have a similar construction:
|
Copy the token from the terminal and paste it in the next window.
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"tokens": [
|
|
||||||
{
|
|
||||||
"token": "token_to_copy",
|
|
||||||
"name": "device_name",
|
|
||||||
"date": "date"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Copy the token from the file and paste it in the next window.
|
|
||||||
|
|
|
@ -1,19 +1,7 @@
|
||||||
Login as root user to your server and look at the contents of the file `/etc/nixos/userdata/tokens.json`
|
[Login as root user to your server](https://selfprivacy.org/docs/how-to-guides/root_ssh/) and enter this command:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cat /etc/nixos/userdata/tokens.json
|
sp-print-api-token
|
||||||
```
|
```
|
||||||
|
|
||||||
This file will have a similar construction:
|
Copy the token from the terminal and paste it in the next window.
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"tokens": [
|
|
||||||
{
|
|
||||||
"token": "token_to_copy",
|
|
||||||
"name": "device_name",
|
|
||||||
"date": "date"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Copy the token from the file and paste it in the next window.
|
|
||||||
|
|
|
@ -1,19 +1,7 @@
|
||||||
Login as root user to your server and look at the contents of the file `/etc/nixos/userdata/tokens.json`
|
[Login as root user to your server](https://selfprivacy.org/docs/how-to-guides/root_ssh/) and enter this command:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cat /etc/nixos/userdata/tokens.json
|
sp-print-api-token
|
||||||
```
|
```
|
||||||
|
|
||||||
This file will have a similar construction:
|
Copy the token from the terminal and paste it in the next window.
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"tokens": [
|
|
||||||
{
|
|
||||||
"token": "token_to_copy",
|
|
||||||
"name": "device_name",
|
|
||||||
"date": "date"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Copy the token from the file and paste it in the next window.
|
|
||||||
|
|
|
@ -1,19 +1,7 @@
|
||||||
Login as root user to your server and look at the contents of the file `/etc/nixos/userdata/tokens.json`
|
[Login as root user to your server](https://selfprivacy.org/docs/how-to-guides/root_ssh/) and enter this command:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cat /etc/nixos/userdata/tokens.json
|
sp-print-api-token
|
||||||
```
|
```
|
||||||
|
|
||||||
This file will have a similar construction:
|
Copy the token from the terminal and paste it in the next window.
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"tokens": [
|
|
||||||
{
|
|
||||||
"token": "token_to_copy",
|
|
||||||
"name": "device_name",
|
|
||||||
"date": "date"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Copy the token from the file and paste it in the next window.
|
|
||||||
|
|
|
@ -1,19 +1,7 @@
|
||||||
Login as root user to your server and look at the contents of the file `/etc/nixos/userdata/tokens.json`
|
[Login as root user to your server](https://selfprivacy.org/docs/how-to-guides/root_ssh/) and enter this command:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cat /etc/nixos/userdata/tokens.json
|
sp-print-api-token
|
||||||
```
|
```
|
||||||
|
|
||||||
This file will have a similar construction:
|
Copy the token from the terminal and paste it in the next window.
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"tokens": [
|
|
||||||
{
|
|
||||||
"token": "token_to_copy",
|
|
||||||
"name": "device_name",
|
|
||||||
"date": "date"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Copy the token from the file and paste it in the next window.
|
|
||||||
|
|
|
@ -1,19 +1,7 @@
|
||||||
Login as root user to your server and look at the contents of the file `/etc/nixos/userdata/tokens.json`
|
[Login as root user to your server](https://selfprivacy.org/docs/how-to-guides/root_ssh/) and enter this command:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cat /etc/nixos/userdata/tokens.json
|
sp-print-api-token
|
||||||
```
|
```
|
||||||
|
|
||||||
This file will have a similar construction:
|
Copy the token from the terminal and paste it in the next window.
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"tokens": [
|
|
||||||
{
|
|
||||||
"token": "token_to_copy",
|
|
||||||
"name": "device_name",
|
|
||||||
"date": "date"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Copy the token from the file and paste it in the next window.
|
|
||||||
|
|
|
@ -173,6 +173,7 @@
|
||||||
"destroy_server": "هل تريد إنهاء هذا الخادم وإنشاء واحد جديد؟",
|
"destroy_server": "هل تريد إنهاء هذا الخادم وإنشاء واحد جديد؟",
|
||||||
"try_again": "هل تريد المحاولة مرة أخرى؟",
|
"try_again": "هل تريد المحاولة مرة أخرى؟",
|
||||||
"purge_all_keys": "هل تريد محو جميع مفاتيح الّتي مُنحت الموافقة؟",
|
"purge_all_keys": "هل تريد محو جميع مفاتيح الّتي مُنحت الموافقة؟",
|
||||||
|
"delete_server_volume": "هل تريد حذف الخادم ووحدة التخزين؟",
|
||||||
"reboot": "قم بإعادة التشغيل",
|
"reboot": "قم بإعادة التشغيل",
|
||||||
"yes": "نعم",
|
"yes": "نعم",
|
||||||
"no": "لا"
|
"no": "لا"
|
||||||
|
@ -332,12 +333,12 @@
|
||||||
},
|
},
|
||||||
"application_settings": {
|
"application_settings": {
|
||||||
"title": "إعدادات التطبيق",
|
"title": "إعدادات التطبيق",
|
||||||
"system_dark_theme_title": "الوضع الافتراضي للنظام",
|
"system_theme_mode_title": "الوضع الافتراضي للنظام",
|
||||||
"system_dark_theme_description": "قم بتطبيق الوضع الفاتح أو الداكن حسب إعدادات النظام",
|
"system_theme_mode_description": "قم بتطبيق الوضع الفاتح أو الداكن حسب إعدادات النظام",
|
||||||
"dark_theme_title": "الوضع الداكن",
|
"dark_theme_title": "الوضع الداكن",
|
||||||
|
"change_application_theme": "قم بتبديل وضع التطبيق",
|
||||||
"dangerous_settings": "إعدادات خطرة",
|
"dangerous_settings": "إعدادات خطرة",
|
||||||
"reset_config_title": "قم بإعادة ضبط إعدادات التطبيق",
|
"reset_config_title": "قم بإعادة ضبط إعدادات التطبيق",
|
||||||
"dark_theme_description": "قم بتبديل وضع التطبيق",
|
|
||||||
"reset_config_description": "قم بإعادة ضبط مفاتيح API والمستخدم المميز."
|
"reset_config_description": "قم بإعادة ضبط مفاتيح API والمستخدم المميز."
|
||||||
},
|
},
|
||||||
"ssh": {
|
"ssh": {
|
||||||
|
|
|
@ -54,13 +54,15 @@
|
||||||
},
|
},
|
||||||
"application_settings": {
|
"application_settings": {
|
||||||
"title": "Tətbiq parametrləri",
|
"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",
|
"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_title": "Tətbiq Sıfırlayın",
|
||||||
"reset_config_description": "API və Super İstifadəçi Açarlarını 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"
|
|
||||||
},
|
},
|
||||||
"ssh": {
|
"ssh": {
|
||||||
"title": "SSH açarları",
|
"title": "SSH açarları",
|
||||||
|
@ -379,6 +381,7 @@
|
||||||
"are_you_sure": "Sən əminsən?",
|
"are_you_sure": "Sən əminsən?",
|
||||||
"purge_all_keys": "Bütün avtorizasiya açarları silinsin?",
|
"purge_all_keys": "Bütün avtorizasiya açarları silinsin?",
|
||||||
"purge_all_keys_confirm": "Bəli, bütün düymələri silin",
|
"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",
|
"reboot": "Yenidən yükləyin",
|
||||||
"yes": "Bəli",
|
"yes": "Bəli",
|
||||||
"no": "Yox"
|
"no": "Yox"
|
||||||
|
|
|
@ -51,7 +51,7 @@
|
||||||
"connect_to_server_provider": "Аўтарызавацца ў ",
|
"connect_to_server_provider": "Аўтарызавацца ў ",
|
||||||
"connect_to_server_provider_text": "З дапамогай API токена праграма SelfPrivacy зможа ад вашага імя замовіць і наладзіць сервер",
|
"connect_to_server_provider_text": "З дапамогай API токена праграма SelfPrivacy зможа ад вашага імя замовіць і наладзіць сервер",
|
||||||
"steps": {
|
"steps": {
|
||||||
"nixos_installation": "Ўстаноўка NixOS",
|
"nixos_installation": "Ўсталёўка NixOS",
|
||||||
"hosting": "Хостынг",
|
"hosting": "Хостынг",
|
||||||
"server_type": "Тып сервера",
|
"server_type": "Тып сервера",
|
||||||
"dns_provider": "DNS правайдэр",
|
"dns_provider": "DNS правайдэр",
|
||||||
|
@ -59,7 +59,7 @@
|
||||||
"domain": "Дамен",
|
"domain": "Дамен",
|
||||||
"master_account": "Майстар акаўнт",
|
"master_account": "Майстар акаўнт",
|
||||||
"server": "Сервер",
|
"server": "Сервер",
|
||||||
"dns_setup": "Устаноўка DNS",
|
"dns_setup": "Усталёўка DNS",
|
||||||
"server_reboot": "Перазагрузка сервера",
|
"server_reboot": "Перазагрузка сервера",
|
||||||
"final_checks": "Фінальныя праверкі"
|
"final_checks": "Фінальныя праверкі"
|
||||||
},
|
},
|
||||||
|
@ -100,7 +100,7 @@
|
||||||
"modal_confirmation_dns_invalid": "Зваротны DNS паказвае на іншы дамен",
|
"modal_confirmation_dns_invalid": "Зваротны DNS паказвае на іншы дамен",
|
||||||
"modal_confirmation_ip_invalid": "IP не супадае з паказаным у DNS запісу",
|
"modal_confirmation_ip_invalid": "IP не супадае з паказаным у DNS запісу",
|
||||||
"fallback_select_provider_console": "Доступ да кансолі хостынгу.",
|
"fallback_select_provider_console": "Доступ да кансолі хостынгу.",
|
||||||
"provider_connected_description": "Сувязь устаноўлена. Увядзіце свой токен з доступам да {}:",
|
"provider_connected_description": "Сувязь наладжана. Увядзіце свой токен з доступам да {}:",
|
||||||
"choose_server": "Выберыце сервер",
|
"choose_server": "Выберыце сервер",
|
||||||
"no_servers": "На вашым акаўнце няма даступных сэрвэраў.",
|
"no_servers": "На вашым акаўнце няма даступных сэрвэраў.",
|
||||||
"modal_confirmation_description": "Падлучэнне да няправільнага сервера можа прывесці да дэструктыўных наступстваў.",
|
"modal_confirmation_description": "Падлучэнне да няправільнага сервера можа прывесці да дэструктыўных наступстваў.",
|
||||||
|
@ -114,7 +114,7 @@
|
||||||
"authorize_new_device": "Аўтарызаваць новую прыладу",
|
"authorize_new_device": "Аўтарызаваць новую прыладу",
|
||||||
"access_granted_on": "Доступ выдадзены {}",
|
"access_granted_on": "Доступ выдадзены {}",
|
||||||
"tip": "Націсніце на прыладу, каб адклікаць доступ.",
|
"tip": "Націсніце на прыладу, каб адклікаць доступ.",
|
||||||
"description": "Гэтыя прылады маюць поўны доступ да кіравання серверам праз прыкладанне SelfPrivacy."
|
"description": "Гэтыя прылады маюць поўны доступ да кіравання серверам праз прыладу SelfPrivacy."
|
||||||
},
|
},
|
||||||
"add_new_device_screen": {
|
"add_new_device_screen": {
|
||||||
"description": "Увядзіце гэты ключ на новай прыладзе:",
|
"description": "Увядзіце гэты ключ на новай прыладзе:",
|
||||||
|
@ -127,7 +127,7 @@
|
||||||
"revoke_device_alert": {
|
"revoke_device_alert": {
|
||||||
"header": "Адклікаць доступ?",
|
"header": "Адклікаць доступ?",
|
||||||
"yes": "Адклікаць",
|
"yes": "Адклікаць",
|
||||||
"no": "Адменіць",
|
"no": "Адхіліць",
|
||||||
"description": "Прылада {} больш не зможа кіраваць серверам."
|
"description": "Прылада {} больш не зможа кіраваць серверам."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -143,7 +143,7 @@
|
||||||
"later": "Прапусціць і наладзіць потым",
|
"later": "Прапусціць і наладзіць потым",
|
||||||
"no_data": "Няма дадзеных",
|
"no_data": "Няма дадзеных",
|
||||||
"services": "Сэрвісы",
|
"services": "Сэрвісы",
|
||||||
"users": "Ужыткоўнікі",
|
"users": "Карыстальнікі",
|
||||||
"more": "Дадаткова",
|
"more": "Дадаткова",
|
||||||
"got_it": "Зразумеў",
|
"got_it": "Зразумеў",
|
||||||
"settings": "Налады",
|
"settings": "Налады",
|
||||||
|
@ -205,6 +205,7 @@
|
||||||
"dns_removal_error": "Немагчыма выдаліць запісы DNS.",
|
"dns_removal_error": "Немагчыма выдаліць запісы DNS.",
|
||||||
"server_deletion_error": "Немагчыма выдаліць сервер.",
|
"server_deletion_error": "Немагчыма выдаліць сервер.",
|
||||||
"unexpected_error": "Непрадбачаная памылка з боку правайдэра.",
|
"unexpected_error": "Непрадбачаная памылка з боку правайдэра.",
|
||||||
|
"delete_server_volume": "Выдаліць сервер і сховішча?",
|
||||||
"volume_creation_error": "Не ўдалося стварыць том."
|
"volume_creation_error": "Не ўдалося стварыць том."
|
||||||
},
|
},
|
||||||
"timer": {
|
"timer": {
|
||||||
|
@ -235,7 +236,7 @@
|
||||||
},
|
},
|
||||||
"more_page": {
|
"more_page": {
|
||||||
"configuration_wizard": "Майстар наладкі",
|
"configuration_wizard": "Майстар наладкі",
|
||||||
"onboarding": "Прівітанне",
|
"onboarding": "Прывітанне",
|
||||||
"create_ssh_key": "SSH ключы адміністратара"
|
"create_ssh_key": "SSH ключы адміністратара"
|
||||||
},
|
},
|
||||||
"about_application_page": {
|
"about_application_page": {
|
||||||
|
@ -265,12 +266,15 @@
|
||||||
"application_settings": {
|
"application_settings": {
|
||||||
"reset_config_description": "Скінуць API ключы i суперкарыстальніка.",
|
"reset_config_description": "Скінуць API ключы i суперкарыстальніка.",
|
||||||
"title": "Налады праграмы",
|
"title": "Налады праграмы",
|
||||||
|
"system_theme_mode_title": "Сістэмная тэма па-змаўчанні",
|
||||||
|
"system_theme_mode_description": "Выкарыстоўвайце светлую ці цёмную тэмы ў залежнасці ад сістэмных налад",
|
||||||
"dark_theme_title": "Цёмная тэма",
|
"dark_theme_title": "Цёмная тэма",
|
||||||
"dark_theme_description": "Змяніць каляровую тэму",
|
"change_application_theme": "Змяніць каляровую тэму",
|
||||||
|
"language": "Мова",
|
||||||
|
"click_to_change_locale": "Націсніце, каб адчыніць меню выбару мовы",
|
||||||
|
"dangerous_settings": "Небяспечныя налады",
|
||||||
"reset_config_title": "Скід налад",
|
"reset_config_title": "Скід налад",
|
||||||
"system_dark_theme_title": "Сістэмная тэма па-змаўчанні",
|
"reset_config_description": "Скінуць API ключы i суперкарыстальніка."
|
||||||
"system_dark_theme_description": "Выкарыстоўвайце светлую ці цёмную тэмы ў залежнасці ад сістэмных налад",
|
|
||||||
"dangerous_settings": "Небяспечныя наладкі"
|
|
||||||
},
|
},
|
||||||
"ssh": {
|
"ssh": {
|
||||||
"root_subtitle": "Уладальнікі паказаных тут ключоў атрымліваюць поўны доступ да дадзеных і налад сервера. Дадавайце выключна свае ключы.",
|
"root_subtitle": "Уладальнікі паказаных тут ключоў атрымліваюць поўны доступ да дадзеных і налад сервера. Дадавайце выключна свае ключы.",
|
||||||
|
|
|
@ -54,13 +54,13 @@
|
||||||
},
|
},
|
||||||
"application_settings": {
|
"application_settings": {
|
||||||
"title": "Nastavení aplikace",
|
"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",
|
"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_title": "Obnovení konfigurace aplikace",
|
||||||
"reset_config_description": "Obnovení klíčů API a uživatele root.",
|
"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í"
|
|
||||||
},
|
},
|
||||||
"ssh": {
|
"ssh": {
|
||||||
"title": "Klíče SSH",
|
"title": "Klíče SSH",
|
||||||
|
|
|
@ -57,13 +57,13 @@
|
||||||
},
|
},
|
||||||
"application_settings": {
|
"application_settings": {
|
||||||
"title": "Anwendungseinstellungen",
|
"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_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_title": "Anwendungseinstellungen zurücksetzen",
|
||||||
"reset_config_description": "API Sclüssel und root Benutzer 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"
|
|
||||||
},
|
},
|
||||||
"ssh": {
|
"ssh": {
|
||||||
"title": "SSH Schlüssel",
|
"title": "SSH Schlüssel",
|
||||||
|
|
|
@ -47,7 +47,29 @@
|
||||||
"console_page": {
|
"console_page": {
|
||||||
"title": "Console",
|
"title": "Console",
|
||||||
"waiting": "Waiting for initialization…",
|
"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": {
|
"about_application_page": {
|
||||||
"title": "About & support",
|
"title": "About & support",
|
||||||
|
@ -75,10 +97,12 @@
|
||||||
},
|
},
|
||||||
"application_settings": {
|
"application_settings": {
|
||||||
"title": "Application settings",
|
"title": "Application settings",
|
||||||
"system_dark_theme_title": "System default theme",
|
"system_theme_mode_title": "System default theme",
|
||||||
"system_dark_theme_description": "Use light or dark theme depending on system settings",
|
"system_theme_mode_description": "Use light or dark theme depending on system settings",
|
||||||
"dark_theme_title": "Dark theme",
|
"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",
|
"dangerous_settings": "Dangerous settings",
|
||||||
"reset_config_title": "Reset application config",
|
"reset_config_title": "Reset application config",
|
||||||
"reset_config_description": "Resets API keys and root user."
|
"reset_config_description": "Resets API keys and root user."
|
||||||
|
|
|
@ -39,14 +39,14 @@
|
||||||
"test": "es-test",
|
"test": "es-test",
|
||||||
"locale": "es",
|
"locale": "es",
|
||||||
"application_settings": {
|
"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",
|
"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",
|
"dark_theme_title": "Tema oscuro",
|
||||||
"system_dark_theme_title": "Tema del sistema",
|
"change_application_theme": "Cambia el tema de tu aplicación",
|
||||||
"system_dark_theme_description": "Utiliza un tema claro u oscuro de la configuración del sistema",
|
"dangerous_settings": "Configuraciones peligrosas",
|
||||||
"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": {
|
"ssh": {
|
||||||
"delete_confirm_question": "¿Está seguro de que desea eliminar la clave SSH?",
|
"delete_confirm_question": "¿Está seguro de que desea eliminar la clave SSH?",
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
{
|
{
|
||||||
"application_settings": {
|
"application_settings": {
|
||||||
"system_dark_theme_description": "Kasutage valgus- või tumeteemat sõltuvalt süsteemi seadetest",
|
|
||||||
"title": "Rakenduse seaded",
|
"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_title": "Tume teema",
|
||||||
"dark_theme_description": "Vaheta oma rakenduse teemat",
|
"change_application_theme": "Vaheta oma rakenduse teemat",
|
||||||
"dangerous_settings": "Ohtlikud seaded",
|
"dangerous_settings": "Ohtlikud seaded",
|
||||||
"reset_config_title": "Lähtesta rakenduse konfiguratsioon",
|
"reset_config_title": "Lähtesta rakenduse konfiguratsioon",
|
||||||
"reset_config_description": "Lähtestab API võtmed ja juurkasutaja."
|
"reset_config_description": "Lähtestab API võtmed ja juurkasutaja."
|
||||||
|
|
|
@ -56,13 +56,13 @@
|
||||||
},
|
},
|
||||||
"application_settings": {
|
"application_settings": {
|
||||||
"title": "Paramètres de l'application",
|
"title": "Paramètres de l'application",
|
||||||
"dark_theme_description": "Changer le thème de l'application",
|
"system_theme_mode_title": "Thème par défaut du système",
|
||||||
"reset_config_title": "Réinitialiser la configuration de l'application",
|
"system_theme_mode_description": "Affichage de jour ou de nuit en fonction du paramétrage système",
|
||||||
"dark_theme_title": "Thème sombre",
|
"dark_theme_title": "Thème sombre",
|
||||||
"reset_config_description": "Réinitialiser les clés API et l'utilisateur root.",
|
"change_application_theme": "Changer le thème de l'application",
|
||||||
"system_dark_theme_title": "Thème par défaut du système",
|
"dangerous_settings": "Paramètres dangereux",
|
||||||
"system_dark_theme_description": "Affichage de jour ou de nuit en fonction du paramétrage système",
|
"reset_config_title": "Réinitialiser la configuration de l'application",
|
||||||
"dangerous_settings": "Paramètres dangereux"
|
"reset_config_description": "Réinitialiser les clés API et l'utilisateur root."
|
||||||
},
|
},
|
||||||
"ssh": {
|
"ssh": {
|
||||||
"title": "Clés SSH",
|
"title": "Clés SSH",
|
||||||
|
|
|
@ -81,10 +81,10 @@
|
||||||
},
|
},
|
||||||
"application_settings": {
|
"application_settings": {
|
||||||
"title": "הגדרות יישום",
|
"title": "הגדרות יישום",
|
||||||
"system_dark_theme_title": "ערכת העיצוב כברירת המחדל של המערכת",
|
"system_theme_mode_title": "ערכת העיצוב כברירת המחדל של המערכת",
|
||||||
"system_dark_theme_description": "להשתמש בערכות עיצוב בהירה או כהה בהתאם להגדרות המערכת שלך",
|
"system_theme_mode_description": "להשתמש בערכות עיצוב בהירה או כהה בהתאם להגדרות המערכת שלך",
|
||||||
"dark_theme_title": "ערכת עיצוב כהה",
|
"dark_theme_title": "ערכת עיצוב כהה",
|
||||||
"dark_theme_description": "החלפת ערכת העיצוב של המערכת שלך",
|
"change_application_theme": "החלפת ערכת העיצוב של המערכת שלך",
|
||||||
"dangerous_settings": "הגדרות מסוכנות",
|
"dangerous_settings": "הגדרות מסוכנות",
|
||||||
"reset_config_title": "איפוס הגדרות היישומון",
|
"reset_config_title": "איפוס הגדרות היישומון",
|
||||||
"reset_config_description": "איפוס מפתחות ה־API ומשתמש העל."
|
"reset_config_description": "איפוס מפתחות ה־API ומשתמש העל."
|
||||||
|
|
|
@ -92,13 +92,13 @@
|
||||||
"bug_report_subtitle": "Спамға байланысты есептік жазбаны қолмен растау қажет. Тіркелгіні белсендіру үшін Қолдау чатында бізге хабарласыңыз."
|
"bug_report_subtitle": "Спамға байланысты есептік жазбаны қолмен растау қажет. Тіркелгіні белсендіру үшін Қолдау чатында бізге хабарласыңыз."
|
||||||
},
|
},
|
||||||
"application_settings": {
|
"application_settings": {
|
||||||
|
"title": "Қосымша параметрлері",
|
||||||
|
"system_theme_mode_title": "Системалық қараңғы тақырып",
|
||||||
|
"system_theme_mode_description": "Системалық қараңғы тақырып сипаттамасы",
|
||||||
|
"dark_theme_title": "Қараңғы тақырып",
|
||||||
|
"change_application_theme": "Қараңғы тақырып сипаттамасы",
|
||||||
"dangerous_settings": "Қауіпті параметрлер",
|
"dangerous_settings": "Қауіпті параметрлер",
|
||||||
"reset_config_title": "Конфигурацияны қалпына келтіру",
|
"reset_config_title": "Конфигурацияны қалпына келтіру",
|
||||||
"title": "Қосымша параметрлері",
|
|
||||||
"system_dark_theme_title": "Системалық қараңғы тақырып",
|
|
||||||
"system_dark_theme_description": "Системалық қараңғы тақырып сипаттамасы",
|
|
||||||
"dark_theme_title": "Қараңғы тақырып",
|
|
||||||
"dark_theme_description": "Қараңғы тақырып сипаттамасы",
|
|
||||||
"reset_config_description": "Конфигурацияны қалпына келтіру сипаттамасы."
|
"reset_config_description": "Конфигурацияны қалпына келтіру сипаттамасы."
|
||||||
},
|
},
|
||||||
"resource_chart": {
|
"resource_chart": {
|
||||||
|
|
|
@ -52,11 +52,11 @@
|
||||||
"privacy_policy": "Privātuma politika"
|
"privacy_policy": "Privātuma politika"
|
||||||
},
|
},
|
||||||
"application_settings": {
|
"application_settings": {
|
||||||
"system_dark_theme_title": "Sistēmas noklusējuma dizains",
|
|
||||||
"dark_theme_title": "Tumšs dizains",
|
|
||||||
"title": "Aplikācijas iestatījumi",
|
"title": "Aplikācijas iestatījumi",
|
||||||
"system_dark_theme_description": "Izmantojiet gaišu vai tumšu dizainu atkarībā no sistēmas iestatījumiem",
|
"system_theme_mode_title": "Sistēmas noklusējuma dizains",
|
||||||
"dark_theme_description": "Lietojumprogrammas dizaina pārslēgšana",
|
"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",
|
"dangerous_settings": "Bīstamie iestatījumi",
|
||||||
"reset_config_title": "Atiestatīt lietojumprogrammas konfigurāciju",
|
"reset_config_title": "Atiestatīt lietojumprogrammas konfigurāciju",
|
||||||
"reset_config_description": "Atiestatīt API atslēgas un saknes lietotāju."
|
"reset_config_description": "Atiestatīt API atslēgas un saknes lietotāju."
|
||||||
|
|
|
@ -56,13 +56,13 @@
|
||||||
},
|
},
|
||||||
"application_settings": {
|
"application_settings": {
|
||||||
"title": "Ustawienia aplikacji",
|
"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_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_title": "Resetowanie",
|
||||||
"reset_config_description": "Zresetuj klucze API i użytkownika root.",
|
"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"
|
|
||||||
},
|
},
|
||||||
"ssh": {
|
"ssh": {
|
||||||
"title": "klucze SSH",
|
"title": "klucze SSH",
|
||||||
|
|
|
@ -75,13 +75,15 @@
|
||||||
},
|
},
|
||||||
"application_settings": {
|
"application_settings": {
|
||||||
"title": "Настройки приложения",
|
"title": "Настройки приложения",
|
||||||
|
"system_theme_mode_title": "Системная тема",
|
||||||
|
"system_theme_mode_description": "Будет использована светлая или тёмная тема в зависимости от системных настроек",
|
||||||
"dark_theme_title": "Тёмная тема",
|
"dark_theme_title": "Тёмная тема",
|
||||||
"dark_theme_description": "Сменить цветовую тему",
|
"change_application_theme": "Сменить цветовую тему",
|
||||||
|
"language": "Язык",
|
||||||
|
"click_to_change_locale": "Нажмите, чтобы открыть список языков",
|
||||||
|
"dangerous_settings": "Опасные настройки",
|
||||||
"reset_config_title": "Сброс настроек",
|
"reset_config_title": "Сброс настроек",
|
||||||
"reset_config_description": "Сбросить API ключи и root пользователя.",
|
"reset_config_description": "Сбросить API ключи и root пользователя."
|
||||||
"system_dark_theme_title": "Системная тема",
|
|
||||||
"system_dark_theme_description": "Будет использована светлая или тёмная тема в зависимости от системных настроек",
|
|
||||||
"dangerous_settings": "Опасные настройки"
|
|
||||||
},
|
},
|
||||||
"ssh": {
|
"ssh": {
|
||||||
"title": "SSH ключи",
|
"title": "SSH ключи",
|
||||||
|
|
|
@ -102,13 +102,13 @@
|
||||||
},
|
},
|
||||||
"application_settings": {
|
"application_settings": {
|
||||||
"title": "Nastavenia aplikácie",
|
"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_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_title": "Resetovať nastavenia aplikácie",
|
||||||
"reset_config_description": "Resetovať kľúče API a užívateľa root.",
|
"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"
|
|
||||||
},
|
},
|
||||||
"ssh": {
|
"ssh": {
|
||||||
"title": "Kľúče SSH",
|
"title": "Kľúče SSH",
|
||||||
|
|
|
@ -53,11 +53,11 @@
|
||||||
"application_version_text": "Različica aplikacije"
|
"application_version_text": "Različica aplikacije"
|
||||||
},
|
},
|
||||||
"application_settings": {
|
"application_settings": {
|
||||||
"dark_theme_title": "Temna tema",
|
|
||||||
"title": "Nastavitve aplikacije",
|
"title": "Nastavitve aplikacije",
|
||||||
"system_dark_theme_title": "Privzeta tema sistema",
|
"system_theme_mode_title": "Privzeta tema sistema",
|
||||||
"system_dark_theme_description": "Uporaba svetle ali temne teme glede na sistemske nastavitve",
|
"system_theme_mode_description": "Uporaba svetle ali temne teme glede na sistemske nastavitve",
|
||||||
"dark_theme_description": "Spreminjanje barvne teme",
|
"dark_theme_title": "Temna tema",
|
||||||
|
"change_application_theme": "Spreminjanje barvne teme",
|
||||||
"dangerous_settings": "Nevarne nastavitve",
|
"dangerous_settings": "Nevarne nastavitve",
|
||||||
"reset_config_title": "Ponastavitev konfiguracije aplikacije"
|
"reset_config_title": "Ponastavitev konfiguracije aplikacije"
|
||||||
},
|
},
|
||||||
|
|
|
@ -47,9 +47,9 @@
|
||||||
"privacy_policy": "นโยบายความเป็นส่วนตัว"
|
"privacy_policy": "นโยบายความเป็นส่วนตัว"
|
||||||
},
|
},
|
||||||
"application_settings": {
|
"application_settings": {
|
||||||
"dark_theme_description": "สลับธีมแอปพลิเคชั่นของคุณ",
|
|
||||||
"title": "การตั้งค่าแอปพลิเคชัน",
|
"title": "การตั้งค่าแอปพลิเคชัน",
|
||||||
"dark_theme_title": "ธีมมืด",
|
"dark_theme_title": "ธีมมืด",
|
||||||
|
"change_application_theme": "สลับธีมแอปพลิเคชั่นของคุณ",
|
||||||
"reset_config_title": "รีเซ็ตค่าดั้งเดิมการตั้งค่าของแอปพลิเคชั่น",
|
"reset_config_title": "รีเซ็ตค่าดั้งเดิมการตั้งค่าของแอปพลิเคชั่น",
|
||||||
"reset_config_description": "รีเซ็ต API key และผู้ใช้งาน root"
|
"reset_config_description": "รีเซ็ต API key และผู้ใช้งาน root"
|
||||||
},
|
},
|
||||||
|
|
|
@ -41,13 +41,14 @@
|
||||||
"locale": "ua",
|
"locale": "ua",
|
||||||
"application_settings": {
|
"application_settings": {
|
||||||
"title": "Налаштування додатка",
|
"title": "Налаштування додатка",
|
||||||
"reset_config_title": "Скинути налаштування",
|
"system_theme_mode_title": "Системна тема за замовчуванням",
|
||||||
|
"system_theme_mode_description": "Використовуйте світлу або темну теми залежно від системних налаштувань",
|
||||||
"dark_theme_title": "Темна тема",
|
"dark_theme_title": "Темна тема",
|
||||||
"dark_theme_description": "Змінити тему додатка",
|
"change_application_theme": "Змінити тему додатка",
|
||||||
"reset_config_description": "Скинути API ключі та root користувача.",
|
"language": "Мова",
|
||||||
"system_dark_theme_title": "Системна тема за замовчуванням",
|
"dangerous_settings": "Небезпечні налаштування",
|
||||||
"system_dark_theme_description": "Використовуйте світлу або темну теми залежно від системних налаштувань",
|
"reset_config_title": "Скинути налаштування",
|
||||||
"dangerous_settings": "Небезпечні налаштування"
|
"reset_config_description": "Скинути API ключі та root користувача."
|
||||||
},
|
},
|
||||||
"ssh": {
|
"ssh": {
|
||||||
"delete_confirm_question": "Ви впевнені, що хочете видалити SSH-ключ?",
|
"delete_confirm_question": "Ви впевнені, що хочете видалити SSH-ключ?",
|
||||||
|
|
|
@ -475,12 +475,13 @@
|
||||||
},
|
},
|
||||||
"application_settings": {
|
"application_settings": {
|
||||||
"title": "应用设置",
|
"title": "应用设置",
|
||||||
"system_dark_theme_title": "系统默认主题",
|
"system_theme_mode_title": "系统默认主题",
|
||||||
|
"system_theme_mode_description": "根据系统设置自动使用明亮或暗色主题",
|
||||||
"dark_theme_title": "暗色主题",
|
"dark_theme_title": "暗色主题",
|
||||||
"system_dark_theme_description": "根据系统设置自动使用明亮或暗色主题",
|
"change_application_theme": "切换应用主题",
|
||||||
"dark_theme_description": "切换应用主题",
|
|
||||||
"dangerous_settings": "危险设置",
|
"dangerous_settings": "危险设置",
|
||||||
"reset_config_title": "重置应用配置",
|
"reset_config_title": "重置应用配置",
|
||||||
|
"delete_server_title": "删除服务器",
|
||||||
"reset_config_description": "重置API密钥和root用户。"
|
"reset_config_description": "重置API密钥和root用户。"
|
||||||
},
|
},
|
||||||
"ssh": {
|
"ssh": {
|
||||||
|
|
175
lib/config/app_controller/app_controller.dart
Normal file
175
lib/config/app_controller/app_controller.dart
Normal file
|
@ -0,0 +1,175 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:material_color_utilities/material_color_utilities.dart'
|
||||||
|
as color_utils;
|
||||||
|
import 'package:selfprivacy/config/get_it_config.dart';
|
||||||
|
import 'package:selfprivacy/config/localization.dart';
|
||||||
|
import 'package:selfprivacy/config/preferences_repository/preferences_repository.dart';
|
||||||
|
|
||||||
|
/// A class that many Widgets can interact with to read current app
|
||||||
|
/// configuration, update it, or listen to its changes.
|
||||||
|
///
|
||||||
|
/// AppController uses repo to change persistent data.
|
||||||
|
class AppController with ChangeNotifier {
|
||||||
|
AppController(this._repo);
|
||||||
|
|
||||||
|
/// repo encapsulates retrieval and storage of preferences
|
||||||
|
final PreferencesRepository _repo;
|
||||||
|
|
||||||
|
/// TODO: to be removed or changed
|
||||||
|
late final ApiConfigModel _apiConfigModel = getIt.get<ApiConfigModel>();
|
||||||
|
|
||||||
|
bool _loaded = false;
|
||||||
|
bool get loaded => _loaded;
|
||||||
|
|
||||||
|
// localization
|
||||||
|
late Locale _locale;
|
||||||
|
Locale get locale => _locale;
|
||||||
|
late List<Locale> _supportedLocales;
|
||||||
|
List<Locale> get supportedLocales => _supportedLocales;
|
||||||
|
|
||||||
|
// theme
|
||||||
|
late ThemeData _lightTheme;
|
||||||
|
ThemeData get lightTheme => _lightTheme;
|
||||||
|
late ThemeData _darkTheme;
|
||||||
|
ThemeData get darkTheme => _darkTheme;
|
||||||
|
late color_utils.CorePalette _corePalette;
|
||||||
|
color_utils.CorePalette get corePalette => _corePalette;
|
||||||
|
|
||||||
|
late bool _systemThemeModeActive;
|
||||||
|
bool get systemThemeModeActive => _systemThemeModeActive;
|
||||||
|
|
||||||
|
late bool _darkThemeModeActive;
|
||||||
|
bool get darkThemeModeActive => _darkThemeModeActive;
|
||||||
|
|
||||||
|
ThemeMode get themeMode => systemThemeModeActive
|
||||||
|
? ThemeMode.system
|
||||||
|
: darkThemeModeActive
|
||||||
|
? ThemeMode.dark
|
||||||
|
: ThemeMode.light;
|
||||||
|
|
||||||
|
late bool _shouldShowOnboarding;
|
||||||
|
bool get shouldShowOnboarding => _shouldShowOnboarding;
|
||||||
|
|
||||||
|
Future<void> init({
|
||||||
|
// required final AppPreferencesRepository repo,
|
||||||
|
required final ThemeData lightThemeData,
|
||||||
|
required final ThemeData darkThemeData,
|
||||||
|
required final color_utils.CorePalette colorPalette,
|
||||||
|
}) async {
|
||||||
|
// _repo = repo;
|
||||||
|
|
||||||
|
await Future.wait(<Future>[
|
||||||
|
// load locale
|
||||||
|
() async {
|
||||||
|
_supportedLocales = [
|
||||||
|
Localization.systemLocale,
|
||||||
|
...await _repo.getSupportedLocales(),
|
||||||
|
];
|
||||||
|
|
||||||
|
_locale = await _repo.getActiveLocale();
|
||||||
|
if (_locale != Localization.systemLocale) {
|
||||||
|
// preset value to other state holders
|
||||||
|
await _apiConfigModel.setLocaleCode(_locale.languageCode);
|
||||||
|
await _repo.setDelegateLocale(_locale);
|
||||||
|
}
|
||||||
|
}(),
|
||||||
|
|
||||||
|
// load theme mode && initialize theme
|
||||||
|
() async {
|
||||||
|
_lightTheme = lightThemeData;
|
||||||
|
_darkTheme = darkThemeData;
|
||||||
|
_corePalette = colorPalette;
|
||||||
|
_darkThemeModeActive = await _repo.getDarkThemeModeFlag();
|
||||||
|
_systemThemeModeActive = await _repo.getSystemThemeModeFlag();
|
||||||
|
}(),
|
||||||
|
|
||||||
|
// load onboarding flag
|
||||||
|
() async {
|
||||||
|
_shouldShowOnboarding = await _repo.getShouldShowOnboarding();
|
||||||
|
}(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
_loaded = true;
|
||||||
|
// Important! Inform listeners a change has occurred.
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateRepoReference
|
||||||
|
Future<void> setShouldShowOnboarding(final bool newValue) async {
|
||||||
|
// Do not perform any work if new and old flag values are identical
|
||||||
|
if (newValue == shouldShowOnboarding) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the flag in memory
|
||||||
|
_shouldShowOnboarding = newValue;
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
// Persist the change
|
||||||
|
await _repo.setShouldShowOnboarding(newValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setSystemThemeModeFlag(final bool newValue) async {
|
||||||
|
// Do not perform any work if new and old ThemeMode are identical
|
||||||
|
if (systemThemeModeActive == newValue) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the new ThemeMode in memory
|
||||||
|
_systemThemeModeActive = newValue;
|
||||||
|
|
||||||
|
// Inform listeners a change has occurred.
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
// Persist the change
|
||||||
|
await _repo.setSystemModeFlag(newValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setDarkThemeModeFlag(final bool newValue) async {
|
||||||
|
// Do not perform any work if new and old ThemeMode are identical
|
||||||
|
if (darkThemeModeActive == newValue) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the new ThemeMode in memory
|
||||||
|
_darkThemeModeActive = newValue;
|
||||||
|
|
||||||
|
// Inform listeners a change has occurred.
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
// Persist the change
|
||||||
|
await _repo.setDarkThemeModeFlag(newValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setLocale(final Locale newLocale) async {
|
||||||
|
// Do not perform any work if new and old Locales are identical
|
||||||
|
if (newLocale == _locale) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the new Locale in memory
|
||||||
|
_locale = newLocale;
|
||||||
|
|
||||||
|
if (newLocale == Localization.systemLocale) {
|
||||||
|
return resetLocale();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// update locale delegate, which in turn should update deps
|
||||||
|
await _repo.setDelegateLocale(newLocale);
|
||||||
|
|
||||||
|
// Persist the change
|
||||||
|
await _repo.setActiveLocale(newLocale);
|
||||||
|
// Update other locale holders
|
||||||
|
await _apiConfigModel.setLocaleCode(newLocale.languageCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> resetLocale() async {
|
||||||
|
/// update locale delegate, which in turn should update deps
|
||||||
|
await _repo.resetDelegateLocale();
|
||||||
|
|
||||||
|
// Persist the change
|
||||||
|
await _repo.resetActiveLocale();
|
||||||
|
// Update other locale holders
|
||||||
|
await _apiConfigModel.resetLocaleCode();
|
||||||
|
}
|
||||||
|
}
|
106
lib/config/app_controller/inherited_app_controller.dart
Normal file
106
lib/config/app_controller/inherited_app_controller.dart
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:material_color_utilities/material_color_utilities.dart'
|
||||||
|
as color_utils;
|
||||||
|
import 'package:selfprivacy/config/app_controller/app_controller.dart';
|
||||||
|
import 'package:selfprivacy/config/brand_colors.dart';
|
||||||
|
import 'package:selfprivacy/config/preferences_repository/inherited_preferences_repository.dart';
|
||||||
|
import 'package:selfprivacy/config/preferences_repository/preferences_repository.dart';
|
||||||
|
import 'package:selfprivacy/theming/factory/app_theme_factory.dart';
|
||||||
|
|
||||||
|
class _AppControllerInjector extends InheritedNotifier<AppController> {
|
||||||
|
const _AppControllerInjector({
|
||||||
|
required super.child,
|
||||||
|
required super.notifier,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class InheritedAppController extends StatefulWidget {
|
||||||
|
const InheritedAppController({
|
||||||
|
required this.child,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<InheritedAppController> createState() => _InheritedAppControllerState();
|
||||||
|
|
||||||
|
static AppController of(final BuildContext context) => context
|
||||||
|
.dependOnInheritedWidgetOfExactType<_AppControllerInjector>()!
|
||||||
|
.notifier!;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _InheritedAppControllerState extends State<InheritedAppController> {
|
||||||
|
// actual state provider
|
||||||
|
late AppController controller;
|
||||||
|
// hold local reference to active repo
|
||||||
|
late PreferencesRepository _repo;
|
||||||
|
|
||||||
|
bool initTriggerred = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
/// update reference on dependency change
|
||||||
|
_repo = InheritedPreferencesRepository.of(context)!;
|
||||||
|
|
||||||
|
if (!initTriggerred) {
|
||||||
|
/// hook controller repo to local reference
|
||||||
|
controller = AppController(_repo);
|
||||||
|
initialize();
|
||||||
|
initTriggerred = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
super.didChangeDependencies();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> initialize() async {
|
||||||
|
late final ThemeData lightThemeData;
|
||||||
|
late final ThemeData darkThemeData;
|
||||||
|
late final color_utils.CorePalette colorPalette;
|
||||||
|
|
||||||
|
await Future.wait(
|
||||||
|
<Future<void>>[
|
||||||
|
() async {
|
||||||
|
lightThemeData = await AppThemeFactory.create(
|
||||||
|
isDark: false,
|
||||||
|
fallbackColor: BrandColors.primary,
|
||||||
|
);
|
||||||
|
}(),
|
||||||
|
() async {
|
||||||
|
darkThemeData = await AppThemeFactory.create(
|
||||||
|
isDark: true,
|
||||||
|
fallbackColor: BrandColors.primary,
|
||||||
|
);
|
||||||
|
}(),
|
||||||
|
() async {
|
||||||
|
colorPalette = (await AppThemeFactory.getCorePalette()) ??
|
||||||
|
color_utils.CorePalette.of(BrandColors.primary.value);
|
||||||
|
}(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
await controller.init(
|
||||||
|
colorPalette: colorPalette,
|
||||||
|
lightThemeData: lightThemeData,
|
||||||
|
darkThemeData: darkThemeData,
|
||||||
|
);
|
||||||
|
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((final _) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(final BuildContext context) => _AppControllerInjector(
|
||||||
|
notifier: controller,
|
||||||
|
child: widget.child,
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,14 +1,13 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:selfprivacy/logic/bloc/backups/backups_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/devices/devices_bloc.dart';
|
||||||
import 'package:selfprivacy/logic/bloc/recovery_key/recovery_key_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/server_jobs/server_jobs_bloc.dart';
|
||||||
import 'package:selfprivacy/logic/bloc/services/services_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/users/users_bloc.dart';
|
||||||
import 'package:selfprivacy/logic/bloc/volumes/volumes_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/client_jobs/client_jobs_cubit.dart';
|
||||||
import 'package:selfprivacy/logic/cubit/dns_records/dns_records_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';
|
import 'package:selfprivacy/logic/cubit/server_detailed_info/server_detailed_info_cubit.dart';
|
||||||
|
@ -56,58 +55,46 @@ class BlocAndProviderConfigState extends State<BlocAndProviderConfig> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(final BuildContext context) {
|
Widget build(final BuildContext context) => MultiProvider(
|
||||||
const isDark = false;
|
providers: [
|
||||||
const isAutoDark = true;
|
BlocProvider(
|
||||||
|
create: (final _) => supportSystemCubit,
|
||||||
return MultiProvider(
|
),
|
||||||
providers: [
|
BlocProvider(
|
||||||
BlocProvider(
|
create: (final _) => serverInstallationCubit,
|
||||||
create: (final _) => AppSettingsCubit(
|
lazy: false,
|
||||||
isDarkModeOn: isDark,
|
),
|
||||||
isAutoDarkModeOn: isAutoDark,
|
BlocProvider(
|
||||||
isOnboardingShowing: true,
|
create: (final _) => usersBloc,
|
||||||
)..load(),
|
lazy: false,
|
||||||
),
|
),
|
||||||
BlocProvider(
|
BlocProvider(
|
||||||
create: (final _) => supportSystemCubit,
|
create: (final _) => servicesBloc,
|
||||||
),
|
),
|
||||||
BlocProvider(
|
BlocProvider(
|
||||||
create: (final _) => serverInstallationCubit,
|
create: (final _) => backupsBloc,
|
||||||
lazy: false,
|
),
|
||||||
),
|
BlocProvider(
|
||||||
BlocProvider(
|
create: (final _) => dnsRecordsCubit,
|
||||||
create: (final _) => usersBloc,
|
),
|
||||||
lazy: false,
|
BlocProvider(
|
||||||
),
|
create: (final _) => recoveryKeyBloc,
|
||||||
BlocProvider(
|
),
|
||||||
create: (final _) => servicesBloc,
|
BlocProvider(
|
||||||
),
|
create: (final _) => devicesBloc,
|
||||||
BlocProvider(
|
),
|
||||||
create: (final _) => backupsBloc,
|
BlocProvider(
|
||||||
),
|
create: (final _) => serverJobsBloc,
|
||||||
BlocProvider(
|
),
|
||||||
create: (final _) => dnsRecordsCubit,
|
BlocProvider(create: (final _) => connectionStatusBloc),
|
||||||
),
|
BlocProvider(
|
||||||
BlocProvider(
|
create: (final _) => serverDetailsCubit,
|
||||||
create: (final _) => recoveryKeyBloc,
|
),
|
||||||
),
|
BlocProvider(create: (final _) => volumesBloc),
|
||||||
BlocProvider(
|
BlocProvider(
|
||||||
create: (final _) => devicesBloc,
|
create: (final _) => JobsCubit(),
|
||||||
),
|
),
|
||||||
BlocProvider(
|
],
|
||||||
create: (final _) => serverJobsBloc,
|
child: widget.child,
|
||||||
),
|
);
|
||||||
BlocProvider(create: (final _) => connectionStatusBloc),
|
|
||||||
BlocProvider(
|
|
||||||
create: (final _) => serverDetailsCubit,
|
|
||||||
),
|
|
||||||
BlocProvider(create: (final _) => volumesBloc),
|
|
||||||
BlocProvider(
|
|
||||||
create: (final _) => JobsCubit(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
child: widget.child,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,21 +1,23 @@
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
import 'package:selfprivacy/logic/get_it/api_config.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/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';
|
import 'package:selfprivacy/logic/get_it/navigation.dart';
|
||||||
|
|
||||||
export 'package:selfprivacy/logic/get_it/api_config.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/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';
|
export 'package:selfprivacy/logic/get_it/navigation.dart';
|
||||||
|
|
||||||
final GetIt getIt = GetIt.instance;
|
final GetIt getIt = GetIt.instance;
|
||||||
|
|
||||||
Future<void> getItSetup() async {
|
Future<void> getItSetup() async {
|
||||||
getIt.registerSingleton<NavigationService>(NavigationService());
|
getIt.registerSingleton<NavigationService>(NavigationService());
|
||||||
|
|
||||||
getIt.registerSingleton<ConsoleModel>(ConsoleModel());
|
getIt.registerSingleton<ConsoleModel>(ConsoleModel());
|
||||||
getIt.registerSingleton<ApiConfigModel>(ApiConfigModel()..init());
|
|
||||||
|
final apiConfigModel = ApiConfigModel();
|
||||||
|
await apiConfigModel.init();
|
||||||
|
getIt.registerSingleton<ApiConfigModel>(apiConfigModel);
|
||||||
|
|
||||||
getIt.registerSingleton<ApiConnectionRepository>(
|
getIt.registerSingleton<ApiConnectionRepository>(
|
||||||
ApiConnectionRepository()..init(),
|
ApiConnectionRepository()..init(),
|
||||||
|
|
|
@ -74,17 +74,20 @@ class HiveConfig {
|
||||||
|
|
||||||
/// Mappings for the different boxes and their keys
|
/// Mappings for the different boxes and their keys
|
||||||
class BNames {
|
class BNames {
|
||||||
/// App settings box. Contains app settings like [isDarkModeOn], [isOnboardingShowing]
|
/// App settings box. Contains app settings like [darkThemeModeOn], [shouldShowOnboarding]
|
||||||
static String appSettingsBox = 'appSettings';
|
static String appSettingsBox = 'appSettings';
|
||||||
|
|
||||||
/// A boolean field of [appSettingsBox] box.
|
/// A boolean field of [appSettingsBox] box.
|
||||||
static String isDarkModeOn = 'isDarkModeOn';
|
static String darkThemeModeOn = 'isDarkModeOn';
|
||||||
|
|
||||||
/// A boolean field of [appSettingsBox] box.
|
/// A boolean field of [appSettingsBox] box.
|
||||||
static String isAutoDarkModeOn = 'isAutoDarkModeOn';
|
static String systemThemeModeOn = 'isAutoDarkModeOn';
|
||||||
|
|
||||||
/// A boolean field of [appSettingsBox] box.
|
/// 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.
|
/// Encryption key to decrypt [serverInstallationBox] and [usersBox] box.
|
||||||
static String serverInstallationEncryptionKey = 'key';
|
static String serverInstallationEncryptionKey = 'key';
|
||||||
|
|
|
@ -3,40 +3,76 @@ import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class Localization extends StatelessWidget {
|
class Localization extends StatelessWidget {
|
||||||
const Localization({
|
const Localization({
|
||||||
|
required this.child,
|
||||||
super.key,
|
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
|
@override
|
||||||
Widget build(final BuildContext context) => EasyLocalization(
|
Widget build(final BuildContext context) => EasyLocalization(
|
||||||
supportedLocales: const [
|
supportedLocales: 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'),
|
|
||||||
],
|
|
||||||
path: 'assets/translations',
|
path: 'assets/translations',
|
||||||
fallbackLocale: const Locale('en'),
|
fallbackLocale: const Locale('en'),
|
||||||
useFallbackTranslations: true,
|
useFallbackTranslations: true,
|
||||||
saveLocale: false,
|
saveLocale: false,
|
||||||
useOnlyLangCode: false,
|
useOnlyLangCode: false,
|
||||||
child: child!,
|
child: child,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
/// abstraction for manipulation of stored app preferences
|
||||||
|
abstract class PreferencesDataSource {
|
||||||
|
/// should onboarding be shown
|
||||||
|
Future<bool> getOnboardingFlag();
|
||||||
|
|
||||||
|
/// should onboarding be shown
|
||||||
|
Future<void> setOnboardingFlag(final bool newValue);
|
||||||
|
|
||||||
|
// TODO: should probably deprecate the following, instead add the
|
||||||
|
// getThemeMode and setThemeMode methods, which store one value instead of
|
||||||
|
// flags.
|
||||||
|
|
||||||
|
/// should system theme mode be enabled
|
||||||
|
Future<bool?> getSystemThemeModeFlag();
|
||||||
|
|
||||||
|
/// should system theme mode be enabled
|
||||||
|
Future<void> setSystemThemeModeFlag(final bool newValue);
|
||||||
|
|
||||||
|
/// should dark theme be enabled
|
||||||
|
Future<bool?> getDarkThemeModeFlag();
|
||||||
|
|
||||||
|
/// should dark theme be enabled
|
||||||
|
Future<void> setDarkThemeModeFlag(final bool newValue);
|
||||||
|
|
||||||
|
/// locale, as set by user
|
||||||
|
///
|
||||||
|
///
|
||||||
|
/// when null, app takes system locale
|
||||||
|
Future<String?> getLocale();
|
||||||
|
|
||||||
|
/// locale, as set by user
|
||||||
|
Future<void> setLocale(final String? newLocale);
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
import 'package:hive/hive.dart';
|
||||||
|
import 'package:selfprivacy/config/hive_config.dart';
|
||||||
|
import 'package:selfprivacy/config/preferences_repository/datasources/preferences_datasource.dart';
|
||||||
|
|
||||||
|
/// app preferences data source hive implementation
|
||||||
|
class PreferencesHiveDataSource implements PreferencesDataSource {
|
||||||
|
final Box _appSettingsBox = Hive.box(BNames.appSettingsBox);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> getOnboardingFlag() async =>
|
||||||
|
_appSettingsBox.get(BNames.shouldShowOnboarding, defaultValue: true);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> setOnboardingFlag(final bool newValue) async =>
|
||||||
|
_appSettingsBox.put(BNames.shouldShowOnboarding, newValue);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool?> getSystemThemeModeFlag() async =>
|
||||||
|
_appSettingsBox.get(BNames.systemThemeModeOn);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> setSystemThemeModeFlag(final bool newValue) async =>
|
||||||
|
_appSettingsBox.put(BNames.systemThemeModeOn, newValue);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool?> getDarkThemeModeFlag() async =>
|
||||||
|
_appSettingsBox.get(BNames.darkThemeModeOn);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> setDarkThemeModeFlag(final bool newValue) async =>
|
||||||
|
_appSettingsBox.put(BNames.darkThemeModeOn, newValue);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String?> getLocale() async => _appSettingsBox.get(BNames.appLocale);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> setLocale(final String? newLocale) async => newLocale == null
|
||||||
|
? _appSettingsBox.delete(BNames.appLocale)
|
||||||
|
: _appSettingsBox.put(BNames.appLocale, newLocale);
|
||||||
|
}
|
|
@ -0,0 +1,64 @@
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:selfprivacy/config/preferences_repository/datasources/preferences_datasource.dart';
|
||||||
|
import 'package:selfprivacy/config/preferences_repository/preferences_repository.dart';
|
||||||
|
|
||||||
|
class _PreferencesRepositoryInjector extends InheritedWidget {
|
||||||
|
const _PreferencesRepositoryInjector({
|
||||||
|
required this.settingsRepository,
|
||||||
|
required super.child,
|
||||||
|
});
|
||||||
|
|
||||||
|
final PreferencesRepository settingsRepository;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool updateShouldNotify(
|
||||||
|
covariant final _PreferencesRepositoryInjector oldWidget,
|
||||||
|
) =>
|
||||||
|
oldWidget.settingsRepository != settingsRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates and injects app preferences repository inside widget tree.
|
||||||
|
class InheritedPreferencesRepository extends StatefulWidget {
|
||||||
|
const InheritedPreferencesRepository({
|
||||||
|
required this.child,
|
||||||
|
required this.dataSource,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final PreferencesDataSource dataSource;
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<InheritedPreferencesRepository> createState() =>
|
||||||
|
_InheritedPreferencesRepositoryState();
|
||||||
|
|
||||||
|
static PreferencesRepository? of(final BuildContext context) => context
|
||||||
|
.dependOnInheritedWidgetOfExactType<_PreferencesRepositoryInjector>()
|
||||||
|
?.settingsRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _InheritedPreferencesRepositoryState
|
||||||
|
extends State<InheritedPreferencesRepository> {
|
||||||
|
late PreferencesRepository repo;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
super.didChangeDependencies();
|
||||||
|
|
||||||
|
/// recreate repo each time dependencies change
|
||||||
|
repo = PreferencesRepository(
|
||||||
|
dataSource: widget.dataSource,
|
||||||
|
setDelegateLocale: EasyLocalization.of(context)!.setLocale,
|
||||||
|
resetDelegateLocale: EasyLocalization.of(context)!.resetLocale,
|
||||||
|
getDelegateLocale: () => EasyLocalization.of(context)!.locale,
|
||||||
|
getSupportedLocales: () => EasyLocalization.of(context)!.supportedLocales,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(final BuildContext context) => _PreferencesRepositoryInjector(
|
||||||
|
settingsRepository: repo,
|
||||||
|
child: widget.child,
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,73 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:selfprivacy/config/localization.dart';
|
||||||
|
import 'package:selfprivacy/config/preferences_repository/datasources/preferences_datasource.dart';
|
||||||
|
|
||||||
|
class PreferencesRepository {
|
||||||
|
const PreferencesRepository({
|
||||||
|
required this.dataSource,
|
||||||
|
required this.getSupportedLocales,
|
||||||
|
required this.getDelegateLocale,
|
||||||
|
required this.setDelegateLocale,
|
||||||
|
required this.resetDelegateLocale,
|
||||||
|
});
|
||||||
|
|
||||||
|
final PreferencesDataSource dataSource;
|
||||||
|
|
||||||
|
/// easy localizations don't expose type of localization provider,
|
||||||
|
/// so it needs to be this crutchy (I could've created one more class-wrapper,
|
||||||
|
/// containing needed functions, but perceive it as boilerplate, because we
|
||||||
|
/// don't need additional encapsulation level here)
|
||||||
|
final FutureOr<void> Function(Locale) setDelegateLocale;
|
||||||
|
final FutureOr<void> Function() resetDelegateLocale;
|
||||||
|
final FutureOr<List<Locale>> Function() getSupportedLocales;
|
||||||
|
final FutureOr<Locale> Function() getDelegateLocale;
|
||||||
|
|
||||||
|
Future<bool> getSystemThemeModeFlag() async =>
|
||||||
|
(await dataSource.getSystemThemeModeFlag()) ?? true;
|
||||||
|
|
||||||
|
Future<void> setSystemThemeModeFlag(final bool newValue) async =>
|
||||||
|
dataSource.setSystemThemeModeFlag(newValue);
|
||||||
|
|
||||||
|
Future<bool> getDarkThemeModeFlag() async =>
|
||||||
|
(await dataSource.getDarkThemeModeFlag()) ?? false;
|
||||||
|
|
||||||
|
Future<void> setDarkThemeModeFlag(final bool newValue) async =>
|
||||||
|
dataSource.setDarkThemeModeFlag(newValue);
|
||||||
|
|
||||||
|
Future<void> setSystemModeFlag(final bool newValue) async =>
|
||||||
|
dataSource.setSystemThemeModeFlag(newValue);
|
||||||
|
|
||||||
|
Future<List<Locale>> supportedLocales() async => getSupportedLocales();
|
||||||
|
|
||||||
|
Future<Locale> getActiveLocale() async {
|
||||||
|
Locale? chosenLocale;
|
||||||
|
|
||||||
|
final String? storedLocaleCode = await dataSource.getLocale();
|
||||||
|
if (storedLocaleCode != null) {
|
||||||
|
chosenLocale = Locale(storedLocaleCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
// when it's null fallback on delegate locale
|
||||||
|
chosenLocale ??= Localization.systemLocale;
|
||||||
|
|
||||||
|
return chosenLocale;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setActiveLocale(final Locale newLocale) async {
|
||||||
|
await dataSource.setLocale(newLocale.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> resetActiveLocale() async {
|
||||||
|
await dataSource.setLocale(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// true when we need to show onboarding
|
||||||
|
Future<bool> getShouldShowOnboarding() async =>
|
||||||
|
dataSource.getOnboardingFlag();
|
||||||
|
|
||||||
|
/// true when we need to show onboarding
|
||||||
|
Future<void> setShouldShowOnboarding(final bool newValue) =>
|
||||||
|
dataSource.setOnboardingFlag(newValue);
|
||||||
|
}
|
|
@ -1,18 +1,14 @@
|
||||||
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:graphql_flutter/graphql_flutter.dart';
|
import 'package:graphql_flutter/graphql_flutter.dart';
|
||||||
import 'package:http/io_client.dart';
|
import 'package:http/io_client.dart';
|
||||||
import 'package:selfprivacy/config/get_it_config.dart';
|
import 'package:selfprivacy/config/get_it_config.dart';
|
||||||
import 'package:selfprivacy/logic/api_maps/tls_options.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) {
|
void _addConsoleLog(final ConsoleLog message) =>
|
||||||
getIt.get<ConsoleModel>().addMessage(
|
getIt.get<ConsoleModel>().log(message);
|
||||||
Message(
|
|
||||||
text: objectToLog.toString(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
class RequestLoggingLink extends Link {
|
class RequestLoggingLink extends Link {
|
||||||
@override
|
@override
|
||||||
|
@ -20,13 +16,14 @@ class RequestLoggingLink extends Link {
|
||||||
final Request request, [
|
final Request request, [
|
||||||
final NextLink? forward,
|
final NextLink? forward,
|
||||||
]) async* {
|
]) async* {
|
||||||
getIt.get<ConsoleModel>().addMessage(
|
_addConsoleLog(
|
||||||
GraphQlRequestMessage(
|
GraphQlRequestConsoleLog(
|
||||||
operation: request.operation,
|
// context: request.context,
|
||||||
variables: request.variables,
|
operationType: request.type.name,
|
||||||
context: request.context,
|
operation: request.operation,
|
||||||
),
|
variables: request.variables,
|
||||||
);
|
),
|
||||||
|
);
|
||||||
yield* forward!(request);
|
yield* forward!(request);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -35,20 +32,26 @@ class ResponseLoggingParser extends ResponseParser {
|
||||||
@override
|
@override
|
||||||
Response parseResponse(final Map<String, dynamic> body) {
|
Response parseResponse(final Map<String, dynamic> body) {
|
||||||
final response = super.parseResponse(body);
|
final response = super.parseResponse(body);
|
||||||
getIt.get<ConsoleModel>().addMessage(
|
_addConsoleLog(
|
||||||
GraphQlResponseMessage(
|
GraphQlResponseConsoleLog(
|
||||||
data: response.data,
|
// context: response.context,
|
||||||
errors: response.errors,
|
data: response.data,
|
||||||
context: response.context,
|
errors: response.errors,
|
||||||
),
|
rawResponse: jsonEncode(response.response),
|
||||||
);
|
),
|
||||||
|
);
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
GraphQLError parseError(final Map<String, dynamic> error) {
|
GraphQLError parseError(final Map<String, dynamic> error) {
|
||||||
final graphQlError = super.parseError(error);
|
final graphQlError = super.parseError(error);
|
||||||
_logToAppConsole(graphQlError);
|
_addConsoleLog(
|
||||||
|
ManualConsoleLog.warning(
|
||||||
|
customTitle: 'GraphQL Error',
|
||||||
|
content: graphQlError.toString(),
|
||||||
|
),
|
||||||
|
);
|
||||||
return graphQlError;
|
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 get _token {
|
||||||
String token = '';
|
String token = '';
|
||||||
final serverDetails = getIt<ApiConfigModel>().serverDetails;
|
final serverDetails = getIt<ApiConfigModel>().serverDetails;
|
||||||
if (serverDetails != null) {
|
if (serverDetails != null) {
|
||||||
token = getIt<ApiConfigModel>().serverDetails!.apiToken;
|
token = serverDetails.apiToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
import 'dart:developer';
|
import 'dart:developer';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
@ -6,7 +7,7 @@ import 'package:dio/dio.dart';
|
||||||
import 'package:dio/io.dart';
|
import 'package:dio/io.dart';
|
||||||
import 'package:pretty_dio_logger/pretty_dio_logger.dart';
|
import 'package:pretty_dio_logger/pretty_dio_logger.dart';
|
||||||
import 'package:selfprivacy/config/get_it_config.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 {
|
abstract class RestApiMap {
|
||||||
Future<Dio> getClient({final BaseOptions? customOptions}) async {
|
Future<Dio> getClient({final BaseOptions? customOptions}) async {
|
||||||
|
@ -57,8 +58,8 @@ abstract class RestApiMap {
|
||||||
}
|
}
|
||||||
|
|
||||||
class ConsoleInterceptor extends InterceptorsWrapper {
|
class ConsoleInterceptor extends InterceptorsWrapper {
|
||||||
void addMessage(final Message message) {
|
void addConsoleLog(final ConsoleLog message) {
|
||||||
getIt.get<ConsoleModel>().addMessage(message);
|
getIt.get<ConsoleModel>().log(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -66,12 +67,12 @@ class ConsoleInterceptor extends InterceptorsWrapper {
|
||||||
final RequestOptions options,
|
final RequestOptions options,
|
||||||
final RequestInterceptorHandler handler,
|
final RequestInterceptorHandler handler,
|
||||||
) async {
|
) async {
|
||||||
addMessage(
|
addConsoleLog(
|
||||||
RestApiRequestMessage(
|
RestApiRequestConsoleLog(
|
||||||
method: options.method,
|
|
||||||
data: options.data.toString(),
|
|
||||||
headers: options.headers,
|
|
||||||
uri: options.uri,
|
uri: options.uri,
|
||||||
|
method: options.method,
|
||||||
|
headers: options.headers,
|
||||||
|
data: jsonEncode(options.data),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return super.onRequest(options, handler);
|
return super.onRequest(options, handler);
|
||||||
|
@ -82,12 +83,12 @@ class ConsoleInterceptor extends InterceptorsWrapper {
|
||||||
final Response response,
|
final Response response,
|
||||||
final ResponseInterceptorHandler handler,
|
final ResponseInterceptorHandler handler,
|
||||||
) async {
|
) async {
|
||||||
addMessage(
|
addConsoleLog(
|
||||||
RestApiResponseMessage(
|
RestApiResponseConsoleLog(
|
||||||
|
uri: response.realUri,
|
||||||
method: response.requestOptions.method,
|
method: response.requestOptions.method,
|
||||||
statusCode: response.statusCode,
|
statusCode: response.statusCode,
|
||||||
data: response.data.toString(),
|
data: jsonEncode(response.data),
|
||||||
uri: response.realUri,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return super.onResponse(
|
return super.onResponse(
|
||||||
|
@ -103,10 +104,13 @@ class ConsoleInterceptor extends InterceptorsWrapper {
|
||||||
) async {
|
) async {
|
||||||
final Response? response = err.response;
|
final Response? response = err.response;
|
||||||
log(err.toString());
|
log(err.toString());
|
||||||
addMessage(
|
|
||||||
Message.warn(
|
addConsoleLog(
|
||||||
text:
|
ManualConsoleLog.warning(
|
||||||
'response-uri: ${response?.realUri}\ncode: ${response?.statusCode}\ndata: ${response?.toString()}\n',
|
customTitle: 'RestAPI error',
|
||||||
|
content: '"uri": "${response?.realUri}",\n'
|
||||||
|
'"status_code": ${response?.statusCode},\n'
|
||||||
|
'"response": ${jsonEncode(response)}',
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return super.onError(err, handler);
|
return super.onError(err, handler);
|
||||||
|
|
|
@ -1,39 +0,0 @@
|
||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:equatable/equatable.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
import 'package:selfprivacy/config/get_it_config.dart';
|
|
||||||
|
|
||||||
part 'connection_status_event.dart';
|
|
||||||
part 'connection_status_state.dart';
|
|
||||||
|
|
||||||
class ConnectionStatusBloc
|
|
||||||
extends Bloc<ConnectionStatusEvent, ConnectionStatusState> {
|
|
||||||
ConnectionStatusBloc()
|
|
||||||
: super(
|
|
||||||
const ConnectionStatusState(
|
|
||||||
connectionStatus: ConnectionStatus.nonexistent,
|
|
||||||
),
|
|
||||||
) {
|
|
||||||
on<ConnectionStatusChanged>((final event, final emit) {
|
|
||||||
emit(ConnectionStatusState(connectionStatus: event.connectionStatus));
|
|
||||||
});
|
|
||||||
final apiConnectionRepository = getIt<ApiConnectionRepository>();
|
|
||||||
_apiConnectionStatusSubscription =
|
|
||||||
apiConnectionRepository.connectionStatusStream.listen(
|
|
||||||
(final ConnectionStatus connectionStatus) {
|
|
||||||
add(
|
|
||||||
ConnectionStatusChanged(connectionStatus),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
StreamSubscription? _apiConnectionStatusSubscription;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> close() {
|
|
||||||
_apiConnectionStatusSubscription?.cancel();
|
|
||||||
return super.close();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
part of 'connection_status_bloc.dart';
|
|
||||||
|
|
||||||
sealed class ConnectionStatusEvent extends Equatable {
|
|
||||||
const ConnectionStatusEvent();
|
|
||||||
}
|
|
||||||
|
|
||||||
class ConnectionStatusChanged extends ConnectionStatusEvent {
|
|
||||||
const ConnectionStatusChanged(this.connectionStatus);
|
|
||||||
|
|
||||||
final ConnectionStatus connectionStatus;
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Object?> get props => [connectionStatus];
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
part of 'connection_status_bloc.dart';
|
|
||||||
|
|
||||||
class ConnectionStatusState extends Equatable {
|
|
||||||
const ConnectionStatusState({
|
|
||||||
required this.connectionStatus,
|
|
||||||
});
|
|
||||||
|
|
||||||
final ConnectionStatus connectionStatus;
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Object> get props => [connectionStatus];
|
|
||||||
}
|
|
27
lib/logic/bloc/connection_status_bloc.dart
Normal file
27
lib/logic/bloc/connection_status_bloc.dart
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:selfprivacy/config/get_it_config.dart';
|
||||||
|
|
||||||
|
/// basically, a bus for other blocs to listen to server status updates
|
||||||
|
class ConnectionStatusBloc extends Bloc<ConnectionStatus, ConnectionStatus> {
|
||||||
|
ConnectionStatusBloc() : super(ConnectionStatus.nonexistent) {
|
||||||
|
on<ConnectionStatus>(
|
||||||
|
(final newStatus, final emit) => emit(newStatus),
|
||||||
|
);
|
||||||
|
|
||||||
|
final apiConnectionRepository = getIt<ApiConnectionRepository>();
|
||||||
|
_apiConnectionStatusSubscription =
|
||||||
|
apiConnectionRepository.connectionStatusStream.listen(
|
||||||
|
(final ConnectionStatus newStatus) => add(newStatus),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
StreamSubscription? _apiConnectionStatusSubscription;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> close() {
|
||||||
|
_apiConnectionStatusSubscription?.cancel();
|
||||||
|
return super.close();
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,66 +0,0 @@
|
||||||
import 'package:equatable/equatable.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
import 'package:hive/hive.dart';
|
|
||||||
import 'package:material_color_utilities/material_color_utilities.dart'
|
|
||||||
as color_utils;
|
|
||||||
import 'package:selfprivacy/config/brand_colors.dart';
|
|
||||||
import 'package:selfprivacy/config/hive_config.dart';
|
|
||||||
import 'package:selfprivacy/theming/factory/app_theme_factory.dart';
|
|
||||||
|
|
||||||
export 'package:provider/provider.dart';
|
|
||||||
|
|
||||||
part 'app_settings_state.dart';
|
|
||||||
|
|
||||||
class AppSettingsCubit extends Cubit<AppSettingsState> {
|
|
||||||
AppSettingsCubit({
|
|
||||||
required final bool isDarkModeOn,
|
|
||||||
required final bool isAutoDarkModeOn,
|
|
||||||
required final bool isOnboardingShowing,
|
|
||||||
}) : super(
|
|
||||||
AppSettingsState(
|
|
||||||
isDarkModeOn: isDarkModeOn,
|
|
||||||
isAutoDarkModeOn: isAutoDarkModeOn,
|
|
||||||
isOnboardingShowing: isOnboardingShowing,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
Box box = Hive.box(BNames.appSettingsBox);
|
|
||||||
|
|
||||||
void load() async {
|
|
||||||
final bool? isDarkModeOn = box.get(BNames.isDarkModeOn);
|
|
||||||
final bool? isAutoDarkModeOn = box.get(BNames.isAutoDarkModeOn);
|
|
||||||
final bool? isOnboardingShowing = box.get(BNames.isOnboardingShowing);
|
|
||||||
emit(
|
|
||||||
state.copyWith(
|
|
||||||
isDarkModeOn: isDarkModeOn,
|
|
||||||
isAutoDarkModeOn: isAutoDarkModeOn,
|
|
||||||
isOnboardingShowing: isOnboardingShowing,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
|
||||||
final color_utils.CorePalette? colorPalette =
|
|
||||||
await AppThemeFactory.getCorePalette();
|
|
||||||
emit(
|
|
||||||
state.copyWith(
|
|
||||||
corePalette: colorPalette,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void updateDarkMode({required final bool isDarkModeOn}) {
|
|
||||||
box.put(BNames.isDarkModeOn, isDarkModeOn);
|
|
||||||
emit(state.copyWith(isDarkModeOn: isDarkModeOn));
|
|
||||||
}
|
|
||||||
|
|
||||||
void updateAutoDarkMode({required final bool isAutoDarkModeOn}) {
|
|
||||||
box.put(BNames.isAutoDarkModeOn, isAutoDarkModeOn);
|
|
||||||
emit(state.copyWith(isAutoDarkModeOn: isAutoDarkModeOn));
|
|
||||||
}
|
|
||||||
|
|
||||||
void turnOffOnboarding({final bool isOnboardingShowing = false}) {
|
|
||||||
box.put(BNames.isOnboardingShowing, isOnboardingShowing);
|
|
||||||
|
|
||||||
emit(state.copyWith(isOnboardingShowing: isOnboardingShowing));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,35 +0,0 @@
|
||||||
part of 'app_settings_cubit.dart';
|
|
||||||
|
|
||||||
class AppSettingsState extends Equatable {
|
|
||||||
const AppSettingsState({
|
|
||||||
required this.isDarkModeOn,
|
|
||||||
required this.isAutoDarkModeOn,
|
|
||||||
required this.isOnboardingShowing,
|
|
||||||
this.corePalette,
|
|
||||||
});
|
|
||||||
|
|
||||||
final bool isDarkModeOn;
|
|
||||||
final bool isAutoDarkModeOn;
|
|
||||||
final bool isOnboardingShowing;
|
|
||||||
final color_utils.CorePalette? corePalette;
|
|
||||||
|
|
||||||
AppSettingsState copyWith({
|
|
||||||
final bool? isDarkModeOn,
|
|
||||||
final bool? isAutoDarkModeOn,
|
|
||||||
final bool? isOnboardingShowing,
|
|
||||||
final color_utils.CorePalette? corePalette,
|
|
||||||
}) =>
|
|
||||||
AppSettingsState(
|
|
||||||
isDarkModeOn: isDarkModeOn ?? this.isDarkModeOn,
|
|
||||||
isAutoDarkModeOn: isAutoDarkModeOn ?? this.isAutoDarkModeOn,
|
|
||||||
isOnboardingShowing: isOnboardingShowing ?? this.isOnboardingShowing,
|
|
||||||
corePalette: corePalette ?? this.corePalette,
|
|
||||||
);
|
|
||||||
|
|
||||||
color_utils.CorePalette get corePaletteOrDefault =>
|
|
||||||
corePalette ?? color_utils.CorePalette.of(BrandColors.primary.value);
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<dynamic> get props =>
|
|
||||||
[isDarkModeOn, isAutoDarkModeOn, isOnboardingShowing, corePalette];
|
|
||||||
}
|
|
|
@ -475,7 +475,7 @@ class ServerInstallationRepository {
|
||||||
|
|
||||||
Future<void> deleteServerDetails() async {
|
Future<void> deleteServerDetails() async {
|
||||||
await box.delete(BNames.serverDetails);
|
await box.delete(BNames.serverDetails);
|
||||||
getIt<ApiConfigModel>().init();
|
await getIt<ApiConfigModel>().init();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> saveServerProviderType(final ServerProviderType type) async {
|
Future<void> saveServerProviderType(final ServerProviderType type) async {
|
||||||
|
@ -501,7 +501,7 @@ class ServerInstallationRepository {
|
||||||
|
|
||||||
Future<void> deleteServerProviderKey() async {
|
Future<void> deleteServerProviderKey() async {
|
||||||
await box.delete(BNames.hetznerKey);
|
await box.delete(BNames.hetznerKey);
|
||||||
getIt<ApiConfigModel>().init();
|
await getIt<ApiConfigModel>().init();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> saveBackblazeKey(
|
Future<void> saveBackblazeKey(
|
||||||
|
@ -512,7 +512,7 @@ class ServerInstallationRepository {
|
||||||
|
|
||||||
Future<void> deleteBackblazeKey() async {
|
Future<void> deleteBackblazeKey() async {
|
||||||
await box.delete(BNames.backblazeCredential);
|
await box.delete(BNames.backblazeCredential);
|
||||||
getIt<ApiConfigModel>().init();
|
await getIt<ApiConfigModel>().init();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> setDnsApiToken(final String key) async {
|
Future<void> setDnsApiToken(final String key) async {
|
||||||
|
@ -521,7 +521,7 @@ class ServerInstallationRepository {
|
||||||
|
|
||||||
Future<void> deleteDnsProviderKey() async {
|
Future<void> deleteDnsProviderKey() async {
|
||||||
await box.delete(BNames.cloudFlareKey);
|
await box.delete(BNames.cloudFlareKey);
|
||||||
getIt<ApiConfigModel>().init();
|
await getIt<ApiConfigModel>().init();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> saveDomain(final ServerDomain serverDomain) async {
|
Future<void> saveDomain(final ServerDomain serverDomain) async {
|
||||||
|
@ -530,7 +530,7 @@ class ServerInstallationRepository {
|
||||||
|
|
||||||
Future<void> deleteDomain() async {
|
Future<void> deleteDomain() async {
|
||||||
await box.delete(BNames.serverDomain);
|
await box.delete(BNames.serverDomain);
|
||||||
getIt<ApiConfigModel>().init();
|
await getIt<ApiConfigModel>().init();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> saveIsServerStarted(final bool value) async {
|
Future<void> saveIsServerStarted(final bool value) async {
|
||||||
|
@ -604,6 +604,6 @@ class ServerInstallationRepository {
|
||||||
BNames.hasFinalChecked,
|
BNames.hasFinalChecked,
|
||||||
BNames.isLoading,
|
BNames.isLoading,
|
||||||
]);
|
]);
|
||||||
getIt<ApiConfigModel>().init();
|
await getIt<ApiConfigModel>().init();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,9 +49,10 @@ abstract class ServerInstallationState extends Equatable {
|
||||||
bool get isPrimaryUserFilled => rootUser != null;
|
bool get isPrimaryUserFilled => rootUser != null;
|
||||||
bool get isServerCreated => serverDetails != 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
|
ServerSetupProgress get progress => ServerSetupProgress
|
||||||
.values[_fulfilementList.where((final el) => el!).length];
|
.values[_fulfillmentList.where((final el) => el!).length];
|
||||||
|
|
||||||
int get porgressBar {
|
int get porgressBar {
|
||||||
if (progress.index < 6) {
|
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 = [
|
final List<bool> res = [
|
||||||
isServerProviderApiKeyFilled,
|
isServerProviderApiKeyFilled,
|
||||||
isServerTypeFilled,
|
isServerTypeFilled,
|
||||||
|
|
|
@ -9,7 +9,6 @@ class ApiConfigModel {
|
||||||
final Box _box = Hive.box(BNames.serverInstallationBox);
|
final Box _box = Hive.box(BNames.serverInstallationBox);
|
||||||
|
|
||||||
ServerHostingDetails? get serverDetails => _serverDetails;
|
ServerHostingDetails? get serverDetails => _serverDetails;
|
||||||
String? get localeCode => _localeCode;
|
|
||||||
String? get serverProviderKey => _serverProviderKey;
|
String? get serverProviderKey => _serverProviderKey;
|
||||||
String? get serverLocation => _serverLocation;
|
String? get serverLocation => _serverLocation;
|
||||||
String? get serverType => _serverType;
|
String? get serverType => _serverType;
|
||||||
|
@ -21,7 +20,12 @@ class ApiConfigModel {
|
||||||
ServerDomain? get serverDomain => _serverDomain;
|
ServerDomain? get serverDomain => _serverDomain;
|
||||||
BackblazeBucket? get backblazeBucket => _backblazeBucket;
|
BackblazeBucket? get backblazeBucket => _backblazeBucket;
|
||||||
|
|
||||||
|
static const localeCodeFallback = 'en';
|
||||||
String? _localeCode;
|
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? _serverProviderKey;
|
||||||
String? _serverLocation;
|
String? _serverLocation;
|
||||||
String? _dnsProviderKey;
|
String? _dnsProviderKey;
|
||||||
|
@ -33,10 +37,6 @@ class ApiConfigModel {
|
||||||
ServerDomain? _serverDomain;
|
ServerDomain? _serverDomain;
|
||||||
BackblazeBucket? _backblazeBucket;
|
BackblazeBucket? _backblazeBucket;
|
||||||
|
|
||||||
Future<void> setLocaleCode(final String value) async {
|
|
||||||
_localeCode = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> storeServerProviderType(final ServerProviderType value) async {
|
Future<void> storeServerProviderType(final ServerProviderType value) async {
|
||||||
await _box.put(BNames.serverProvider, value);
|
await _box.put(BNames.serverProvider, value);
|
||||||
_serverProvider = value;
|
_serverProvider = value;
|
||||||
|
@ -88,7 +88,6 @@ class ApiConfigModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
void clear() {
|
void clear() {
|
||||||
_localeCode = null;
|
|
||||||
_serverProviderKey = null;
|
_serverProviderKey = null;
|
||||||
_dnsProvider = null;
|
_dnsProvider = null;
|
||||||
_serverLocation = null;
|
_serverLocation = null;
|
||||||
|
@ -101,8 +100,7 @@ class ApiConfigModel {
|
||||||
_serverProvider = null;
|
_serverProvider = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
void init() {
|
Future<void> init() async {
|
||||||
_localeCode = 'en';
|
|
||||||
_serverProviderKey = _box.get(BNames.hetznerKey);
|
_serverProviderKey = _box.get(BNames.hetznerKey);
|
||||||
_serverLocation = _box.get(BNames.serverLocation);
|
_serverLocation = _box.get(BNames.serverLocation);
|
||||||
_dnsProviderKey = _box.get(BNames.cloudFlareKey);
|
_dnsProviderKey = _box.get(BNames.cloudFlareKey);
|
||||||
|
|
|
@ -1,17 +0,0 @@
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:selfprivacy/logic/models/message.dart';
|
|
||||||
|
|
||||||
class ConsoleModel extends ChangeNotifier {
|
|
||||||
final List<Message> _messages = [];
|
|
||||||
|
|
||||||
List<Message> get messages => _messages;
|
|
||||||
|
|
||||||
void addMessage(final Message message) {
|
|
||||||
messages.add(message);
|
|
||||||
notifyListeners();
|
|
||||||
// Make sure we don't have too many messages
|
|
||||||
if (messages.length > 500) {
|
|
||||||
messages.removeAt(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
51
lib/logic/get_it/console_model.dart
Normal file
51
lib/logic/get_it/console_model.dart
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:selfprivacy/logic/models/console_log.dart';
|
||||||
|
|
||||||
|
class ConsoleModel extends ChangeNotifier {
|
||||||
|
/// limit for history, so logs won't affect memory and overflow
|
||||||
|
static const logBufferLimit = 500;
|
||||||
|
|
||||||
|
/// differs from log buffer limit so as to not rearrange memory each time
|
||||||
|
/// we add incoming log
|
||||||
|
static const incomingBufferBreakpoint = 750;
|
||||||
|
|
||||||
|
final List<ConsoleLog> _logs = [];
|
||||||
|
final List<ConsoleLog> _incomingQueue = [];
|
||||||
|
|
||||||
|
bool _paused = false;
|
||||||
|
bool get paused => _paused;
|
||||||
|
List<ConsoleLog> get logs => _logs;
|
||||||
|
|
||||||
|
void log(final ConsoleLog newLog) {
|
||||||
|
if (paused) {
|
||||||
|
_incomingQueue.add(newLog);
|
||||||
|
if (_incomingQueue.length > incomingBufferBreakpoint) {
|
||||||
|
logs.removeRange(0, _incomingQueue.length - logBufferLimit);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logs.add(newLog);
|
||||||
|
_updateQueue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void play() {
|
||||||
|
_logs.addAll(_incomingQueue);
|
||||||
|
_paused = false;
|
||||||
|
_updateQueue();
|
||||||
|
_incomingQueue.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void pause() {
|
||||||
|
_paused = true;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// drop logs over the limit and
|
||||||
|
void _updateQueue() {
|
||||||
|
// Make sure we don't have too many
|
||||||
|
if (logs.length > logBufferLimit) {
|
||||||
|
logs.removeRange(0, logs.length - logBufferLimit);
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
184
lib/logic/models/console_log.dart
Normal file
184
lib/logic/models/console_log.dart
Normal file
|
@ -0,0 +1,184 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:gql/language.dart' as gql;
|
||||||
|
import 'package:graphql/client.dart' as gql_client;
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
|
enum ConsoleLogSeverity {
|
||||||
|
normal,
|
||||||
|
warning,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Base entity for console logs.
|
||||||
|
sealed class ConsoleLog {
|
||||||
|
ConsoleLog({
|
||||||
|
final String? customTitle,
|
||||||
|
this.severity = ConsoleLogSeverity.normal,
|
||||||
|
}) : title = customTitle ??
|
||||||
|
(severity == ConsoleLogSeverity.warning ? 'Error' : 'Log'),
|
||||||
|
time = DateTime.now();
|
||||||
|
|
||||||
|
final DateTime time;
|
||||||
|
final ConsoleLogSeverity severity;
|
||||||
|
bool get isError => severity == ConsoleLogSeverity.warning;
|
||||||
|
|
||||||
|
/// title for both in listing and in dialog
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
/// formatted data to be shown in listing
|
||||||
|
String get content;
|
||||||
|
|
||||||
|
/// data available for copy in dialog
|
||||||
|
String? get shareableData => '{"title": "$title",\n'
|
||||||
|
'"timestamp": "$fullUTCString",\n'
|
||||||
|
'"data":{\n$content\n}'
|
||||||
|
'\n}';
|
||||||
|
|
||||||
|
static final DateFormat _formatter = DateFormat('hh:mm:ss');
|
||||||
|
String get timeString => _formatter.format(time);
|
||||||
|
|
||||||
|
String get fullUTCString => time.toUtc().toIso8601String();
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class LogWithRawResponse {
|
||||||
|
String get rawResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// entity for manually created logs, as opposed to automated ones coming
|
||||||
|
/// from requests / responses
|
||||||
|
class ManualConsoleLog extends ConsoleLog {
|
||||||
|
ManualConsoleLog({
|
||||||
|
required this.content,
|
||||||
|
super.customTitle,
|
||||||
|
super.severity,
|
||||||
|
});
|
||||||
|
|
||||||
|
ManualConsoleLog.warning({
|
||||||
|
required this.content,
|
||||||
|
super.customTitle,
|
||||||
|
}) : super(severity: ConsoleLogSeverity.warning);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String content;
|
||||||
|
}
|
||||||
|
|
||||||
|
class RestApiRequestConsoleLog extends ConsoleLog {
|
||||||
|
RestApiRequestConsoleLog({
|
||||||
|
this.method,
|
||||||
|
this.uri,
|
||||||
|
this.headers,
|
||||||
|
this.data,
|
||||||
|
super.severity,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// headers thath should not be included into clipboard buffer, as opposed to
|
||||||
|
/// `[[ConsoleLogItemDialog]]` `_KeyValueRow.hideList` which filters values,
|
||||||
|
/// that should be accessible from UI, but hidden in screenshots
|
||||||
|
static const blacklistedHeaders = ['Authorization'];
|
||||||
|
|
||||||
|
final String? method;
|
||||||
|
final Uri? uri;
|
||||||
|
final Map<String, dynamic>? headers;
|
||||||
|
final String? data;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get title => 'Rest API Request';
|
||||||
|
|
||||||
|
Map<String, dynamic> get filteredHeaders => Map.fromEntries(
|
||||||
|
headers?.entries.where(
|
||||||
|
(final entry) => !blacklistedHeaders.contains(entry.key),
|
||||||
|
) ??
|
||||||
|
const [],
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get content => '"method": "$method",\n'
|
||||||
|
'"uri": "$uri",\n'
|
||||||
|
'"headers": ${jsonEncode(filteredHeaders)},\n' // censor header to not expose API keys
|
||||||
|
'"data": $data';
|
||||||
|
}
|
||||||
|
|
||||||
|
class RestApiResponseConsoleLog extends ConsoleLog {
|
||||||
|
RestApiResponseConsoleLog({
|
||||||
|
this.method,
|
||||||
|
this.uri,
|
||||||
|
this.statusCode,
|
||||||
|
this.data,
|
||||||
|
super.severity,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String? method;
|
||||||
|
final Uri? uri;
|
||||||
|
final int? statusCode;
|
||||||
|
final String? data;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get title => 'Rest API Response';
|
||||||
|
@override
|
||||||
|
String get content => '"method": "$method",\n'
|
||||||
|
'"status_code": $statusCode,\n'
|
||||||
|
'"uri": "$uri",\n'
|
||||||
|
'"data": $data';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// there is no actual getter for context fields outside of its class
|
||||||
|
/// one can extract unique entries by their type, which implements
|
||||||
|
/// `ContextEntry` class, I'll leave the code here if in the future
|
||||||
|
/// some entries will actually be needed.
|
||||||
|
// extension ContextEncoder on gql_client.Context {
|
||||||
|
// String get encode {
|
||||||
|
// return '""';
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
class GraphQlRequestConsoleLog extends ConsoleLog {
|
||||||
|
GraphQlRequestConsoleLog({
|
||||||
|
required this.operationType,
|
||||||
|
required this.operation,
|
||||||
|
required this.variables,
|
||||||
|
// this.context,
|
||||||
|
super.severity,
|
||||||
|
});
|
||||||
|
|
||||||
|
// final gql_client.Context? context;
|
||||||
|
final String operationType;
|
||||||
|
final gql_client.Operation? operation;
|
||||||
|
String get operationDocument =>
|
||||||
|
operation != null ? gql.printNode(operation!.document) : 'null';
|
||||||
|
final Map<String, dynamic>? variables;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get title => 'GraphQL Request';
|
||||||
|
@override
|
||||||
|
String get content =>
|
||||||
|
// '"context": ${context?.encode},\n'
|
||||||
|
'"variables": ${jsonEncode(variables)},\n'
|
||||||
|
'"type": "$operationType",\n'
|
||||||
|
'"name": "${operation?.operationName}",\n'
|
||||||
|
'"document": ${jsonEncode(operationDocument)}';
|
||||||
|
}
|
||||||
|
|
||||||
|
class GraphQlResponseConsoleLog extends ConsoleLog
|
||||||
|
implements LogWithRawResponse {
|
||||||
|
GraphQlResponseConsoleLog({
|
||||||
|
required this.rawResponse,
|
||||||
|
// this.context,
|
||||||
|
this.data,
|
||||||
|
this.errors,
|
||||||
|
super.severity,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
final String rawResponse;
|
||||||
|
// final gql_client.Context? context;
|
||||||
|
final Map<String, dynamic>? data;
|
||||||
|
final List<gql_client.GraphQLError>? errors;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get title => 'GraphQL Response';
|
||||||
|
@override
|
||||||
|
String get content =>
|
||||||
|
// '"context": ${context?.encode},\n'
|
||||||
|
'"data": ${jsonEncode(data)},\n'
|
||||||
|
'"errors": $errors';
|
||||||
|
}
|
|
@ -77,46 +77,21 @@ class DigitalOceanLocation {
|
||||||
return emoji;
|
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 get countryDisplayKey {
|
||||||
String displayKey = 'countries.';
|
final countryName = _townPrefixToCountryMap[slug.substring(0, 3)] ?? slug;
|
||||||
switch (slug.substring(0, 3)) {
|
return 'countries.$countryName';
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,8 +10,10 @@ DigitalOceanVolume _$DigitalOceanVolumeFromJson(Map<String, dynamic> json) =>
|
||||||
DigitalOceanVolume(
|
DigitalOceanVolume(
|
||||||
json['id'] as String,
|
json['id'] as String,
|
||||||
json['name'] as String,
|
json['name'] as String,
|
||||||
json['size_gigabytes'] as int,
|
(json['size_gigabytes'] as num).toInt(),
|
||||||
(json['droplet_ids'] as List<dynamic>?)?.map((e) => e as int).toList(),
|
(json['droplet_ids'] as List<dynamic>?)
|
||||||
|
?.map((e) => (e as num).toInt())
|
||||||
|
.toList(),
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$DigitalOceanVolumeToJson(DigitalOceanVolume instance) =>
|
Map<String, dynamic> _$DigitalOceanVolumeToJson(DigitalOceanVolume instance) =>
|
||||||
|
@ -42,10 +44,10 @@ DigitalOceanServerType _$DigitalOceanServerTypeFromJson(
|
||||||
(json['regions'] as List<dynamic>).map((e) => e as String).toList(),
|
(json['regions'] as List<dynamic>).map((e) => e as String).toList(),
|
||||||
(json['memory'] as num).toDouble(),
|
(json['memory'] as num).toDouble(),
|
||||||
json['description'] as String,
|
json['description'] as String,
|
||||||
json['disk'] as int,
|
(json['disk'] as num).toInt(),
|
||||||
(json['price_monthly'] as num).toDouble(),
|
(json['price_monthly'] as num).toDouble(),
|
||||||
json['slug'] as String,
|
json['slug'] as String,
|
||||||
json['vcpus'] as int,
|
(json['vcpus'] as num).toInt(),
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$DigitalOceanServerTypeToJson(
|
Map<String, dynamic> _$DigitalOceanServerTypeToJson(
|
||||||
|
|
|
@ -24,8 +24,8 @@ CloudflareDnsRecord _$CloudflareDnsRecordFromJson(Map<String, dynamic> json) =>
|
||||||
name: json['name'] as String?,
|
name: json['name'] as String?,
|
||||||
content: json['content'] as String?,
|
content: json['content'] as String?,
|
||||||
zoneName: json['zone_name'] as String,
|
zoneName: json['zone_name'] as String,
|
||||||
ttl: json['ttl'] as int? ?? 3600,
|
ttl: (json['ttl'] as num?)?.toInt() ?? 3600,
|
||||||
priority: json['priority'] as int? ?? 10,
|
priority: (json['priority'] as num?)?.toInt() ?? 10,
|
||||||
id: json['id'] as String?,
|
id: json['id'] as String?,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ part of 'desec_dns_info.dart';
|
||||||
|
|
||||||
DesecDomain _$DesecDomainFromJson(Map<String, dynamic> json) => DesecDomain(
|
DesecDomain _$DesecDomainFromJson(Map<String, dynamic> json) => DesecDomain(
|
||||||
name: json['name'] as String,
|
name: json['name'] as String,
|
||||||
minimumTtl: json['minimum_ttl'] as int?,
|
minimumTtl: (json['minimum_ttl'] as num?)?.toInt(),
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$DesecDomainToJson(DesecDomain instance) =>
|
Map<String, dynamic> _$DesecDomainToJson(DesecDomain instance) =>
|
||||||
|
@ -21,7 +21,7 @@ DesecDnsRecord _$DesecDnsRecordFromJson(Map<String, dynamic> json) =>
|
||||||
DesecDnsRecord(
|
DesecDnsRecord(
|
||||||
subname: json['subname'] as String,
|
subname: json['subname'] as String,
|
||||||
type: json['type'] as String,
|
type: json['type'] as String,
|
||||||
ttl: json['ttl'] as int,
|
ttl: (json['ttl'] as num).toInt(),
|
||||||
records:
|
records:
|
||||||
(json['records'] as List<dynamic>).map((e) => e as String).toList(),
|
(json['records'] as List<dynamic>).map((e) => e as String).toList(),
|
||||||
);
|
);
|
||||||
|
|
|
@ -9,7 +9,7 @@ part of 'digital_ocean_dns_info.dart';
|
||||||
DigitalOceanDomain _$DigitalOceanDomainFromJson(Map<String, dynamic> json) =>
|
DigitalOceanDomain _$DigitalOceanDomainFromJson(Map<String, dynamic> json) =>
|
||||||
DigitalOceanDomain(
|
DigitalOceanDomain(
|
||||||
name: json['name'] as String,
|
name: json['name'] as String,
|
||||||
ttl: json['ttl'] as int?,
|
ttl: (json['ttl'] as num?)?.toInt(),
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$DigitalOceanDomainToJson(DigitalOceanDomain instance) =>
|
Map<String, dynamic> _$DigitalOceanDomainToJson(DigitalOceanDomain instance) =>
|
||||||
|
@ -21,12 +21,12 @@ Map<String, dynamic> _$DigitalOceanDomainToJson(DigitalOceanDomain instance) =>
|
||||||
DigitalOceanDnsRecord _$DigitalOceanDnsRecordFromJson(
|
DigitalOceanDnsRecord _$DigitalOceanDnsRecordFromJson(
|
||||||
Map<String, dynamic> json) =>
|
Map<String, dynamic> json) =>
|
||||||
DigitalOceanDnsRecord(
|
DigitalOceanDnsRecord(
|
||||||
id: json['id'] as int?,
|
id: (json['id'] as num?)?.toInt(),
|
||||||
name: json['name'] as String,
|
name: json['name'] as String,
|
||||||
type: json['type'] as String,
|
type: json['type'] as String,
|
||||||
ttl: json['ttl'] as int,
|
ttl: (json['ttl'] as num).toInt(),
|
||||||
data: json['data'] as String,
|
data: json['data'] as String,
|
||||||
priority: json['priority'] as int?,
|
priority: (json['priority'] as num?)?.toInt(),
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$DigitalOceanDnsRecordToJson(
|
Map<String, dynamic> _$DigitalOceanDnsRecordToJson(
|
||||||
|
|
|
@ -8,7 +8,7 @@ part of 'hetzner_server_info.dart';
|
||||||
|
|
||||||
HetznerServerInfo _$HetznerServerInfoFromJson(Map<String, dynamic> json) =>
|
HetznerServerInfo _$HetznerServerInfoFromJson(Map<String, dynamic> json) =>
|
||||||
HetznerServerInfo(
|
HetznerServerInfo(
|
||||||
json['id'] as int,
|
(json['id'] as num).toInt(),
|
||||||
json['name'] as String,
|
json['name'] as String,
|
||||||
$enumDecode(_$ServerStatusEnumMap, json['status']),
|
$enumDecode(_$ServerStatusEnumMap, json['status']),
|
||||||
DateTime.parse(json['created'] as String),
|
DateTime.parse(json['created'] as String),
|
||||||
|
@ -16,7 +16,9 @@ HetznerServerInfo _$HetznerServerInfoFromJson(Map<String, dynamic> json) =>
|
||||||
json['server_type'] as Map<String, dynamic>),
|
json['server_type'] as Map<String, dynamic>),
|
||||||
HetznerServerInfo.locationFromJson(json['datacenter'] as Map),
|
HetznerServerInfo.locationFromJson(json['datacenter'] as Map),
|
||||||
HetznerPublicNetInfo.fromJson(json['public_net'] as Map<String, dynamic>),
|
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) =>
|
Map<String, dynamic> _$HetznerServerInfoToJson(HetznerServerInfo instance) =>
|
||||||
|
@ -58,7 +60,7 @@ Map<String, dynamic> _$HetznerPublicNetInfoToJson(
|
||||||
};
|
};
|
||||||
|
|
||||||
HetznerIp4 _$HetznerIp4FromJson(Map<String, dynamic> json) => HetznerIp4(
|
HetznerIp4 _$HetznerIp4FromJson(Map<String, dynamic> json) => HetznerIp4(
|
||||||
json['id'] as int,
|
(json['id'] as num).toInt(),
|
||||||
json['ip'] as String,
|
json['ip'] as String,
|
||||||
json['blocked'] as bool,
|
json['blocked'] as bool,
|
||||||
json['dns_ptr'] as String,
|
json['dns_ptr'] as String,
|
||||||
|
@ -75,9 +77,9 @@ Map<String, dynamic> _$HetznerIp4ToJson(HetznerIp4 instance) =>
|
||||||
HetznerServerTypeInfo _$HetznerServerTypeInfoFromJson(
|
HetznerServerTypeInfo _$HetznerServerTypeInfoFromJson(
|
||||||
Map<String, dynamic> json) =>
|
Map<String, dynamic> json) =>
|
||||||
HetznerServerTypeInfo(
|
HetznerServerTypeInfo(
|
||||||
json['cores'] as int,
|
(json['cores'] as num).toInt(),
|
||||||
json['memory'] as num,
|
json['memory'] as num,
|
||||||
json['disk'] as int,
|
(json['disk'] as num).toInt(),
|
||||||
(json['prices'] as List<dynamic>)
|
(json['prices'] as List<dynamic>)
|
||||||
.map((e) => HetznerPriceInfo.fromJson(e as Map<String, dynamic>))
|
.map((e) => HetznerPriceInfo.fromJson(e as Map<String, dynamic>))
|
||||||
.toList(),
|
.toList(),
|
||||||
|
@ -132,9 +134,9 @@ Map<String, dynamic> _$HetznerLocationToJson(HetznerLocation instance) =>
|
||||||
|
|
||||||
HetznerVolume _$HetznerVolumeFromJson(Map<String, dynamic> json) =>
|
HetznerVolume _$HetznerVolumeFromJson(Map<String, dynamic> json) =>
|
||||||
HetznerVolume(
|
HetznerVolume(
|
||||||
json['id'] as int,
|
(json['id'] as num).toInt(),
|
||||||
json['size'] as int,
|
(json['size'] as num).toInt(),
|
||||||
json['serverId'] as int?,
|
(json['serverId'] as num?)?.toInt(),
|
||||||
json['name'] as String,
|
json['name'] as String,
|
||||||
json['linux_device'] as String?,
|
json['linux_device'] as String?,
|
||||||
);
|
);
|
||||||
|
|
|
@ -15,7 +15,7 @@ RecoveryKeyStatus _$RecoveryKeyStatusFromJson(Map<String, dynamic> json) =>
|
||||||
expiration: json['expiration'] == null
|
expiration: json['expiration'] == null
|
||||||
? null
|
? null
|
||||||
: DateTime.parse(json['expiration'] as String),
|
: 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) =>
|
Map<String, dynamic> _$RecoveryKeyStatusToJson(RecoveryKeyStatus instance) =>
|
||||||
|
|
|
@ -15,7 +15,7 @@ ServerJob _$ServerJobFromJson(Map<String, dynamic> json) => ServerJob(
|
||||||
updatedAt: DateTime.parse(json['updatedAt'] as String),
|
updatedAt: DateTime.parse(json['updatedAt'] as String),
|
||||||
createdAt: DateTime.parse(json['createdAt'] as String),
|
createdAt: DateTime.parse(json['createdAt'] as String),
|
||||||
error: json['error'] as String?,
|
error: json['error'] as String?,
|
||||||
progress: json['progress'] as int?,
|
progress: (json['progress'] as num?)?.toInt(),
|
||||||
result: json['result'] as String?,
|
result: json['result'] as String?,
|
||||||
statusText: json['statusText'] as String?,
|
statusText: json['statusText'] as String?,
|
||||||
finishedAt: json['finishedAt'] == null
|
finishedAt: json['finishedAt'] == null
|
||||||
|
|
|
@ -1,75 +0,0 @@
|
||||||
import 'package:graphql/client.dart';
|
|
||||||
import 'package:intl/intl.dart';
|
|
||||||
|
|
||||||
/// TODO(misterfourtytwo): add equality override
|
|
||||||
class Message {
|
|
||||||
Message({this.text, this.severity = MessageSeverity.normal})
|
|
||||||
: time = DateTime.now();
|
|
||||||
Message.warn({this.text})
|
|
||||||
: severity = MessageSeverity.warning,
|
|
||||||
time = DateTime.now();
|
|
||||||
|
|
||||||
final String? text;
|
|
||||||
final DateTime time;
|
|
||||||
final MessageSeverity severity;
|
|
||||||
|
|
||||||
static final DateFormat _formatter = DateFormat('hh:mm');
|
|
||||||
String get timeString => _formatter.format(time);
|
|
||||||
}
|
|
||||||
|
|
||||||
enum MessageSeverity {
|
|
||||||
normal,
|
|
||||||
warning,
|
|
||||||
}
|
|
||||||
|
|
||||||
class RestApiRequestMessage extends Message {
|
|
||||||
RestApiRequestMessage({
|
|
||||||
this.method,
|
|
||||||
this.uri,
|
|
||||||
this.data,
|
|
||||||
this.headers,
|
|
||||||
}) : super(text: 'request-uri: $uri\nheaders: $headers\ndata: $data');
|
|
||||||
|
|
||||||
final String? method;
|
|
||||||
final Uri? uri;
|
|
||||||
final String? data;
|
|
||||||
final Map<String, dynamic>? headers;
|
|
||||||
}
|
|
||||||
|
|
||||||
class RestApiResponseMessage extends Message {
|
|
||||||
RestApiResponseMessage({
|
|
||||||
this.method,
|
|
||||||
this.uri,
|
|
||||||
this.statusCode,
|
|
||||||
this.data,
|
|
||||||
}) : super(text: 'response-uri: $uri\ncode: $statusCode\ndata: $data');
|
|
||||||
|
|
||||||
final String? method;
|
|
||||||
final Uri? uri;
|
|
||||||
final int? statusCode;
|
|
||||||
final String? data;
|
|
||||||
}
|
|
||||||
|
|
||||||
class GraphQlResponseMessage extends Message {
|
|
||||||
GraphQlResponseMessage({
|
|
||||||
this.data,
|
|
||||||
this.errors,
|
|
||||||
this.context,
|
|
||||||
}) : super(text: 'GraphQL Response\ndata: $data');
|
|
||||||
|
|
||||||
final Map<String, dynamic>? data;
|
|
||||||
final List<GraphQLError>? errors;
|
|
||||||
final Context? context;
|
|
||||||
}
|
|
||||||
|
|
||||||
class GraphQlRequestMessage extends Message {
|
|
||||||
GraphQlRequestMessage({
|
|
||||||
this.operation,
|
|
||||||
this.variables,
|
|
||||||
this.context,
|
|
||||||
}) : super(text: 'GraphQL Request\noperation: $operation');
|
|
||||||
|
|
||||||
final Operation? operation;
|
|
||||||
final Map<String, dynamic>? variables;
|
|
||||||
final Context? context;
|
|
||||||
}
|
|
|
@ -27,9 +27,9 @@ class ApiAdapter {
|
||||||
class CloudflareDnsProvider extends DnsProvider {
|
class CloudflareDnsProvider extends DnsProvider {
|
||||||
CloudflareDnsProvider() : _adapter = ApiAdapter();
|
CloudflareDnsProvider() : _adapter = ApiAdapter();
|
||||||
CloudflareDnsProvider.load(
|
CloudflareDnsProvider.load(
|
||||||
final bool isAuthotized,
|
final bool isAuthorized,
|
||||||
) : _adapter = ApiAdapter(
|
) : _adapter = ApiAdapter(
|
||||||
isWithToken: isAuthotized,
|
isWithToken: isAuthorized,
|
||||||
);
|
);
|
||||||
|
|
||||||
ApiAdapter _adapter;
|
ApiAdapter _adapter;
|
||||||
|
|
|
@ -22,9 +22,9 @@ class ApiAdapter {
|
||||||
class DesecDnsProvider extends DnsProvider {
|
class DesecDnsProvider extends DnsProvider {
|
||||||
DesecDnsProvider() : _adapter = ApiAdapter();
|
DesecDnsProvider() : _adapter = ApiAdapter();
|
||||||
DesecDnsProvider.load(
|
DesecDnsProvider.load(
|
||||||
final bool isAuthotized,
|
final bool isAuthorized,
|
||||||
) : _adapter = ApiAdapter(
|
) : _adapter = ApiAdapter(
|
||||||
isWithToken: isAuthotized,
|
isWithToken: isAuthorized,
|
||||||
);
|
);
|
||||||
|
|
||||||
ApiAdapter _adapter;
|
ApiAdapter _adapter;
|
||||||
|
|
|
@ -22,9 +22,9 @@ class ApiAdapter {
|
||||||
class DigitalOceanDnsProvider extends DnsProvider {
|
class DigitalOceanDnsProvider extends DnsProvider {
|
||||||
DigitalOceanDnsProvider() : _adapter = ApiAdapter();
|
DigitalOceanDnsProvider() : _adapter = ApiAdapter();
|
||||||
DigitalOceanDnsProvider.load(
|
DigitalOceanDnsProvider.load(
|
||||||
final bool isAuthotized,
|
final bool isAuthorized,
|
||||||
) : _adapter = ApiAdapter(
|
) : _adapter = ApiAdapter(
|
||||||
isWithToken: isAuthotized,
|
isWithToken: isAuthorized,
|
||||||
);
|
);
|
||||||
|
|
||||||
ApiAdapter _adapter;
|
ApiAdapter _adapter;
|
||||||
|
|
|
@ -38,9 +38,9 @@ class DigitalOceanServerProvider extends ServerProvider {
|
||||||
DigitalOceanServerProvider() : _adapter = ApiAdapter();
|
DigitalOceanServerProvider() : _adapter = ApiAdapter();
|
||||||
DigitalOceanServerProvider.load(
|
DigitalOceanServerProvider.load(
|
||||||
final String? location,
|
final String? location,
|
||||||
final bool isAuthotized,
|
final bool isAuthorized,
|
||||||
) : _adapter = ApiAdapter(
|
) : _adapter = ApiAdapter(
|
||||||
isWithToken: isAuthotized,
|
isWithToken: isAuthorized,
|
||||||
region: location,
|
region: location,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -38,9 +38,9 @@ class HetznerServerProvider extends ServerProvider {
|
||||||
HetznerServerProvider() : _adapter = ApiAdapter();
|
HetznerServerProvider() : _adapter = ApiAdapter();
|
||||||
HetznerServerProvider.load(
|
HetznerServerProvider.load(
|
||||||
final String? location,
|
final String? location,
|
||||||
final bool isAuthotized,
|
final bool isAuthorized,
|
||||||
) : _adapter = ApiAdapter(
|
) : _adapter = ApiAdapter(
|
||||||
isWithToken: isAuthotized,
|
isWithToken: isAuthorized,
|
||||||
region: location,
|
region: location,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
174
lib/main.dart
174
lib/main.dart
|
@ -2,28 +2,20 @@ import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.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_config.dart';
|
||||||
import 'package:selfprivacy/config/bloc_observer.dart';
|
import 'package:selfprivacy/config/bloc_observer.dart';
|
||||||
import 'package:selfprivacy/config/brand_colors.dart';
|
import 'package:selfprivacy/config/brand_colors.dart';
|
||||||
import 'package:selfprivacy/config/get_it_config.dart';
|
import 'package:selfprivacy/config/get_it_config.dart';
|
||||||
import 'package:selfprivacy/config/hive_config.dart';
|
import 'package:selfprivacy/config/hive_config.dart';
|
||||||
import 'package:selfprivacy/config/localization.dart';
|
import 'package:selfprivacy/config/localization.dart';
|
||||||
import 'package:selfprivacy/logic/cubit/app_settings/app_settings_cubit.dart';
|
import 'package:selfprivacy/config/preferences_repository/datasources/preferences_hive_datasource.dart';
|
||||||
import 'package:selfprivacy/theming/factory/app_theme_factory.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/pages/errors/failed_to_init_secure_storage.dart';
|
||||||
import 'package:selfprivacy/ui/router/router.dart';
|
import 'package:selfprivacy/ui/router/router.dart';
|
||||||
// import 'package:wakelock/wakelock.dart';
|
|
||||||
import 'package:timezone/data/latest.dart' as tz;
|
import 'package:timezone/data/latest.dart' as tz;
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
|
||||||
try {
|
|
||||||
await HiveConfig.init();
|
|
||||||
} on PlatformException catch (e) {
|
|
||||||
runApp(
|
|
||||||
FailedToInitSecureStorageScreen(e: e),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
|
// await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
|
||||||
|
|
||||||
// try {
|
// try {
|
||||||
|
@ -34,85 +26,117 @@ void main() async {
|
||||||
// print(e);
|
// print(e);
|
||||||
// }
|
// }
|
||||||
|
|
||||||
await getItSetup();
|
try {
|
||||||
await EasyLocalization.ensureInitialized();
|
await Future.wait(
|
||||||
tz.initializeTimeZones();
|
<Future<void>>[
|
||||||
|
HiveConfig.init(),
|
||||||
|
EasyLocalization.ensureInitialized(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
await getItSetup();
|
||||||
|
} on PlatformException catch (e) {
|
||||||
|
runApp(
|
||||||
|
FailedToInitSecureStorageScreen(e: e),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
final ThemeData lightThemeData = await AppThemeFactory.create(
|
tz.initializeTimeZones();
|
||||||
isDark: false,
|
|
||||||
fallbackColor: BrandColors.primary,
|
|
||||||
);
|
|
||||||
final ThemeData darkThemeData = await AppThemeFactory.create(
|
|
||||||
isDark: true,
|
|
||||||
fallbackColor: BrandColors.primary,
|
|
||||||
);
|
|
||||||
|
|
||||||
Bloc.observer = SimpleBlocObserver();
|
Bloc.observer = SimpleBlocObserver();
|
||||||
|
|
||||||
runApp(
|
runApp(
|
||||||
Localization(
|
Localization(
|
||||||
child: SelfprivacyApp(
|
child: InheritedPreferencesRepository(
|
||||||
lightThemeData: lightThemeData,
|
dataSource: PreferencesHiveDataSource(),
|
||||||
darkThemeData: darkThemeData,
|
child: const InheritedAppController(
|
||||||
|
child: AppBuilder(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class SelfprivacyApp extends StatelessWidget {
|
class AppBuilder extends StatelessWidget {
|
||||||
SelfprivacyApp({
|
const AppBuilder({super.key});
|
||||||
required this.lightThemeData,
|
|
||||||
required this.darkThemeData,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
final ThemeData lightThemeData;
|
|
||||||
final ThemeData darkThemeData;
|
|
||||||
|
|
||||||
final _appRouter = RootRouter(getIt.get<NavigationService>().navigatorKey);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(final BuildContext context) => Localization(
|
Widget build(final BuildContext context) {
|
||||||
child: BlocAndProviderConfig(
|
final appController = InheritedAppController.of(context);
|
||||||
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;
|
|
||||||
|
|
||||||
return widget ?? error;
|
if (appController.loaded) {
|
||||||
},
|
return const SelfprivacyApp();
|
||||||
);
|
}
|
||||||
},
|
|
||||||
|
return const SplashScreen();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Widget to be shown
|
||||||
|
/// until essential app initialization is completed
|
||||||
|
class SplashScreen extends StatelessWidget {
|
||||||
|
const SplashScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(final BuildContext context) => const ColoredBox(
|
||||||
|
color: Colors.white,
|
||||||
|
child: Center(
|
||||||
|
child: CircularProgressIndicator.adaptive(
|
||||||
|
valueColor: AlwaysStoppedAnimation(BrandColors.primary),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class SelfprivacyApp extends StatefulWidget {
|
||||||
|
const SelfprivacyApp({
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SelfprivacyApp> createState() => _SelfprivacyAppState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SelfprivacyAppState extends State<SelfprivacyApp> {
|
||||||
|
final appKey = UniqueKey();
|
||||||
|
final _appRouter = RootRouter(getIt.get<NavigationService>().navigatorKey);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(final BuildContext context) {
|
||||||
|
final appController = InheritedAppController.of(context);
|
||||||
|
|
||||||
|
return BlocAndProviderConfig(
|
||||||
|
child: MaterialApp.router(
|
||||||
|
key: appKey,
|
||||||
|
title: 'SelfPrivacy',
|
||||||
|
// routing
|
||||||
|
routeInformationParser: _appRouter.defaultRouteParser(),
|
||||||
|
routerDelegate: _appRouter.delegate(),
|
||||||
|
scaffoldMessengerKey:
|
||||||
|
getIt.get<NavigationService>().scaffoldMessengerKey,
|
||||||
|
// localization settings
|
||||||
|
locale: context.locale,
|
||||||
|
supportedLocales: context.supportedLocales,
|
||||||
|
localizationsDelegates: context.localizationDelegates,
|
||||||
|
// theme settings
|
||||||
|
themeMode: appController.themeMode,
|
||||||
|
theme: appController.lightTheme,
|
||||||
|
darkTheme: appController.darkTheme,
|
||||||
|
// other preferences
|
||||||
|
debugShowCheckedModeBanner: false,
|
||||||
|
scrollBehavior:
|
||||||
|
const MaterialScrollBehavior().copyWith(scrollbars: false),
|
||||||
|
builder: _builder,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _builder(final BuildContext context, final Widget? widget) {
|
||||||
|
Widget error = const Center(child: Text('...rendering error...'));
|
||||||
|
if (widget is Scaffold || widget is Navigator) {
|
||||||
|
error = Scaffold(body: error);
|
||||||
|
}
|
||||||
|
ErrorWidget.builder = (final FlutterErrorDetails errorDetails) => error;
|
||||||
|
|
||||||
|
return widget ?? error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -42,6 +42,11 @@ abstract class AppThemeFactory {
|
||||||
typography: appTypography,
|
typography: appTypography,
|
||||||
useMaterial3: true,
|
useMaterial3: true,
|
||||||
scaffoldBackgroundColor: colorScheme.background,
|
scaffoldBackgroundColor: colorScheme.background,
|
||||||
|
listTileTheme: ListTileThemeData(
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
return materialThemeData;
|
return materialThemeData;
|
||||||
|
@ -50,7 +55,8 @@ abstract class AppThemeFactory {
|
||||||
static Future<ColorScheme?> _getDynamicColors(final Brightness brightness) {
|
static Future<ColorScheme?> _getDynamicColors(final Brightness brightness) {
|
||||||
try {
|
try {
|
||||||
return DynamicColorPlugin.getCorePalette().then(
|
return DynamicColorPlugin.getCorePalette().then(
|
||||||
(final corePallet) => corePallet?.toColorScheme(brightness: brightness),
|
(final corePallete) =>
|
||||||
|
corePallete?.toColorScheme(brightness: brightness),
|
||||||
);
|
);
|
||||||
} on PlatformException {
|
} on PlatformException {
|
||||||
return Future.value(null);
|
return Future.value(null);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class BrandHeader extends StatelessWidget {
|
class BrandHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||||
const BrandHeader({
|
const BrandHeader({
|
||||||
super.key,
|
super.key,
|
||||||
this.title = '',
|
this.title = '',
|
||||||
|
@ -8,6 +8,9 @@ class BrandHeader extends StatelessWidget {
|
||||||
this.onBackButtonPressed,
|
this.onBackButtonPressed,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Size get preferredSize => const Size.fromHeight(52.0);
|
||||||
|
|
||||||
final String title;
|
final String title;
|
||||||
final bool hasBackButton;
|
final bool hasBackButton;
|
||||||
final VoidCallback? onBackButtonPressed;
|
final VoidCallback? onBackButtonPressed;
|
||||||
|
|
|
@ -39,6 +39,7 @@ class _BrandMarkdownState extends State<BrandMarkdown> {
|
||||||
return MarkdownBody(
|
return MarkdownBody(
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
styleSheet: markdown,
|
styleSheet: markdown,
|
||||||
|
selectable: true,
|
||||||
onTapLink: (final String text, final String? href, final String title) {
|
onTapLink: (final String text, final String? href, final String title) {
|
||||||
if (href != null) {
|
if (href != null) {
|
||||||
canLaunchUrlString(href).then((final bool canLaunchURL) {
|
canLaunchUrlString(href).then((final bool canLaunchURL) {
|
||||||
|
|
|
@ -11,15 +11,16 @@ class InfoBox extends StatelessWidget {
|
||||||
final bool isWarning;
|
final bool isWarning;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(final BuildContext context) => Column(
|
Widget build(final BuildContext context) => Wrap(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
spacing: 8.0,
|
||||||
|
runSpacing: 16.0,
|
||||||
|
crossAxisAlignment: WrapCrossAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
Icon(
|
||||||
isWarning ? Icons.warning_amber_outlined : Icons.info_outline,
|
isWarning ? Icons.warning_amber_outlined : Icons.info_outline,
|
||||||
size: 24,
|
size: 24,
|
||||||
color: Theme.of(context).colorScheme.onBackground,
|
color: Theme.of(context).colorScheme.onBackground,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
|
||||||
Text(
|
Text(
|
||||||
text,
|
text,
|
||||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||||
|
|
|
@ -283,7 +283,7 @@ class JobsContent extends StatelessWidget {
|
||||||
(final job) => job.uid == state.rebuildJobUid,
|
(final job) => job.uid == state.rebuildJobUid,
|
||||||
);
|
);
|
||||||
if (rebuildJob == null) {
|
if (rebuildJob == null) {
|
||||||
return const Gap(0);
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
|
|
|
@ -1,304 +0,0 @@
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:selfprivacy/logic/models/message.dart';
|
|
||||||
import 'package:selfprivacy/utils/platform_adapter.dart';
|
|
||||||
|
|
||||||
class LogListItem extends StatelessWidget {
|
|
||||||
const LogListItem({
|
|
||||||
required this.message,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
final Message message;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(final BuildContext context) {
|
|
||||||
final messageItem = message;
|
|
||||||
if (messageItem is RestApiRequestMessage) {
|
|
||||||
return _RestApiRequestMessageItem(message: messageItem);
|
|
||||||
} else if (messageItem is RestApiResponseMessage) {
|
|
||||||
return _RestApiResponseMessageItem(message: messageItem);
|
|
||||||
} else if (messageItem is GraphQlResponseMessage) {
|
|
||||||
return _GraphQlResponseMessageItem(message: messageItem);
|
|
||||||
} else if (messageItem is GraphQlRequestMessage) {
|
|
||||||
return _GraphQlRequestMessageItem(message: messageItem);
|
|
||||||
} else {
|
|
||||||
return _DefaultMessageItem(message: messageItem);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _RestApiRequestMessageItem extends StatelessWidget {
|
|
||||||
const _RestApiRequestMessageItem({required this.message});
|
|
||||||
|
|
||||||
final RestApiRequestMessage message;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(final BuildContext context) => ListTile(
|
|
||||||
title: Text(
|
|
||||||
'${message.method}\n${message.uri}',
|
|
||||||
),
|
|
||||||
subtitle: Text(message.timeString),
|
|
||||||
leading: const Icon(Icons.upload_outlined),
|
|
||||||
iconColor: Theme.of(context).colorScheme.secondary,
|
|
||||||
onTap: () => showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (final BuildContext context) => AlertDialog(
|
|
||||||
scrollable: true,
|
|
||||||
title: Text(
|
|
||||||
'${message.method}\n${message.uri}',
|
|
||||||
),
|
|
||||||
content: Column(
|
|
||||||
children: [
|
|
||||||
Text(message.timeString),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
// Headers is a map of key-value pairs
|
|
||||||
if (message.headers != null) const Text('Headers'),
|
|
||||||
if (message.headers != null)
|
|
||||||
Text(
|
|
||||||
message.headers!.entries
|
|
||||||
.map((final entry) => '${entry.key}: ${entry.value}')
|
|
||||||
.join('\n'),
|
|
||||||
),
|
|
||||||
if (message.data != null && message.data != 'null')
|
|
||||||
const Text('Data'),
|
|
||||||
if (message.data != null && message.data != 'null')
|
|
||||||
Text(message.data!),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
// A button to copy the request to the clipboard
|
|
||||||
if (message.text != null)
|
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
PlatformAdapter.setClipboard(message.text ?? '');
|
|
||||||
},
|
|
||||||
child: Text('console_page.copy'.tr()),
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
|
||||||
child: Text('basis.close'.tr()),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
class _RestApiResponseMessageItem extends StatelessWidget {
|
|
||||||
const _RestApiResponseMessageItem({required this.message});
|
|
||||||
|
|
||||||
final RestApiResponseMessage message;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(final BuildContext context) => ListTile(
|
|
||||||
title: Text(
|
|
||||||
'${message.statusCode} ${message.method}\n${message.uri}',
|
|
||||||
),
|
|
||||||
subtitle: Text(message.timeString),
|
|
||||||
leading: const Icon(Icons.download_outlined),
|
|
||||||
iconColor: Theme.of(context).colorScheme.primary,
|
|
||||||
onTap: () => showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (final BuildContext context) => AlertDialog(
|
|
||||||
scrollable: true,
|
|
||||||
title: Text(
|
|
||||||
'${message.statusCode} ${message.method}\n${message.uri}',
|
|
||||||
),
|
|
||||||
content: Column(
|
|
||||||
children: [
|
|
||||||
Text(message.timeString),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
// Headers is a map of key-value pairs
|
|
||||||
if (message.data != null && message.data != 'null')
|
|
||||||
const Text('Data'),
|
|
||||||
if (message.data != null && message.data != 'null')
|
|
||||||
Text(message.data!),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
// A button to copy the request to the clipboard
|
|
||||||
if (message.text != null)
|
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
PlatformAdapter.setClipboard(message.text ?? '');
|
|
||||||
},
|
|
||||||
child: Text('console_page.copy'.tr()),
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
|
||||||
child: Text('basis.close'.tr()),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
class _GraphQlResponseMessageItem extends StatelessWidget {
|
|
||||||
const _GraphQlResponseMessageItem({required this.message});
|
|
||||||
|
|
||||||
final GraphQlResponseMessage message;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(final BuildContext context) => ListTile(
|
|
||||||
title: Text(
|
|
||||||
'GraphQL Response at ${message.timeString}',
|
|
||||||
),
|
|
||||||
subtitle: Text(
|
|
||||||
message.data.toString(),
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
maxLines: 1,
|
|
||||||
),
|
|
||||||
leading: const Icon(Icons.arrow_circle_down_outlined),
|
|
||||||
iconColor: Theme.of(context).colorScheme.tertiary,
|
|
||||||
onTap: () => showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (final BuildContext context) => AlertDialog(
|
|
||||||
scrollable: true,
|
|
||||||
title: Text(
|
|
||||||
'GraphQL Response at ${message.timeString}',
|
|
||||||
),
|
|
||||||
content: Column(
|
|
||||||
children: [
|
|
||||||
Text(message.timeString),
|
|
||||||
const Divider(),
|
|
||||||
if (message.data != null) const Text('Data'),
|
|
||||||
// Data is a map of key-value pairs
|
|
||||||
if (message.data != null)
|
|
||||||
Text(
|
|
||||||
message.data!.entries
|
|
||||||
.map((final entry) => '${entry.key}: ${entry.value}')
|
|
||||||
.join('\n'),
|
|
||||||
),
|
|
||||||
const Divider(),
|
|
||||||
if (message.errors != null) const Text('Errors'),
|
|
||||||
if (message.errors != null)
|
|
||||||
Text(
|
|
||||||
message.errors!
|
|
||||||
.map(
|
|
||||||
(final entry) =>
|
|
||||||
'${entry.message} at ${entry.locations}',
|
|
||||||
)
|
|
||||||
.join('\n'),
|
|
||||||
),
|
|
||||||
const Divider(),
|
|
||||||
if (message.context != null) const Text('Context'),
|
|
||||||
if (message.context != null)
|
|
||||||
Text(
|
|
||||||
message.context!.toString(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
// A button to copy the request to the clipboard
|
|
||||||
if (message.text != null)
|
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
PlatformAdapter.setClipboard(message.text ?? '');
|
|
||||||
},
|
|
||||||
child: Text('console_page.copy'.tr()),
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
|
||||||
child: Text('basis.close'.tr()),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
class _GraphQlRequestMessageItem extends StatelessWidget {
|
|
||||||
const _GraphQlRequestMessageItem({required this.message});
|
|
||||||
|
|
||||||
final GraphQlRequestMessage message;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(final BuildContext context) => ListTile(
|
|
||||||
title: Text(
|
|
||||||
'GraphQL Request at ${message.timeString}',
|
|
||||||
),
|
|
||||||
subtitle: Text(
|
|
||||||
message.operation.toString(),
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
maxLines: 1,
|
|
||||||
),
|
|
||||||
leading: const Icon(Icons.arrow_circle_up_outlined),
|
|
||||||
iconColor: Theme.of(context).colorScheme.secondary,
|
|
||||||
onTap: () => showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (final BuildContext context) => AlertDialog(
|
|
||||||
scrollable: true,
|
|
||||||
title: Text(
|
|
||||||
'GraphQL Response at ${message.timeString}',
|
|
||||||
),
|
|
||||||
content: Column(
|
|
||||||
children: [
|
|
||||||
Text(message.timeString),
|
|
||||||
const Divider(),
|
|
||||||
if (message.operation != null) const Text('Operation'),
|
|
||||||
// Data is a map of key-value pairs
|
|
||||||
if (message.operation != null)
|
|
||||||
Text(
|
|
||||||
message.operation!.toString(),
|
|
||||||
),
|
|
||||||
const Divider(),
|
|
||||||
if (message.variables != null) const Text('Variables'),
|
|
||||||
if (message.variables != null)
|
|
||||||
Text(
|
|
||||||
message.variables!.entries
|
|
||||||
.map((final entry) => '${entry.key}: ${entry.value}')
|
|
||||||
.join('\n'),
|
|
||||||
),
|
|
||||||
const Divider(),
|
|
||||||
if (message.context != null) const Text('Context'),
|
|
||||||
if (message.context != null)
|
|
||||||
Text(
|
|
||||||
message.context!.toString(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
// A button to copy the request to the clipboard
|
|
||||||
if (message.text != null)
|
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
PlatformAdapter.setClipboard(message.text ?? '');
|
|
||||||
},
|
|
||||||
child: Text('console_page.copy'.tr()),
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
|
||||||
child: Text('basis.close'.tr()),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
class _DefaultMessageItem extends StatelessWidget {
|
|
||||||
const _DefaultMessageItem({required this.message});
|
|
||||||
|
|
||||||
final Message message;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(final BuildContext context) => Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
|
||||||
child: RichText(
|
|
||||||
text: TextSpan(
|
|
||||||
style: DefaultTextStyle.of(context).style,
|
|
||||||
children: <TextSpan>[
|
|
||||||
TextSpan(
|
|
||||||
text: '${message.timeString}: \n',
|
|
||||||
style: const TextStyle(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
TextSpan(text: message.text),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
import 'package:selfprivacy/ui/components/not_ready_card/not_ready_card.dart';
|
import 'package:selfprivacy/ui/components/not_ready_card/not_ready_card.dart';
|
||||||
|
|
||||||
class EmptyPagePlaceholder extends StatelessWidget {
|
class EmptyPagePlaceholder extends StatelessWidget {
|
||||||
|
@ -10,50 +11,72 @@ class EmptyPagePlaceholder extends StatelessWidget {
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
final bool showReadyCard;
|
||||||
|
final IconData iconData;
|
||||||
final String title;
|
final String title;
|
||||||
final String description;
|
final String description;
|
||||||
final IconData iconData;
|
|
||||||
final bool showReadyCard;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(final BuildContext context) => !showReadyCard
|
Widget build(final BuildContext context) => showReadyCard
|
||||||
? _expandedContent(context)
|
? Column(
|
||||||
: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
const Padding(
|
if (showReadyCard)
|
||||||
padding: EdgeInsets.symmetric(horizontal: 15),
|
const Padding(
|
||||||
child: NotReadyCard(),
|
padding: EdgeInsets.symmetric(
|
||||||
),
|
vertical: 15,
|
||||||
Expanded(
|
horizontal: 15,
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 15),
|
|
||||||
child: Center(
|
|
||||||
child: _expandedContent(context),
|
|
||||||
),
|
),
|
||||||
|
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),
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
Icon(
|
||||||
iconData,
|
iconData,
|
||||||
size: 50,
|
size: 50,
|
||||||
color: Theme.of(context).colorScheme.onBackground,
|
color: Theme.of(context).colorScheme.onBackground,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const Gap(16),
|
||||||
Text(
|
Text(
|
||||||
title,
|
title,
|
||||||
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
||||||
color: Theme.of(context).colorScheme.onBackground,
|
color: Theme.of(context).colorScheme.onBackground,
|
||||||
),
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const Gap(8),
|
||||||
Text(
|
Text(
|
||||||
description,
|
description,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
|
|
|
@ -1,295 +0,0 @@
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:auto_route/auto_route.dart';
|
|
||||||
import 'package:flutter/cupertino.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:selfprivacy/ui/components/drawers/support_drawer.dart';
|
|
||||||
import 'package:selfprivacy/ui/components/pre_styled_buttons/flash_fab.dart';
|
|
||||||
import 'package:selfprivacy/ui/router/root_destinations.dart';
|
|
||||||
import 'package:selfprivacy/utils/breakpoints.dart';
|
|
||||||
|
|
||||||
class RootScaffoldWithNavigation extends StatelessWidget {
|
|
||||||
const RootScaffoldWithNavigation({
|
|
||||||
required this.child,
|
|
||||||
required this.title,
|
|
||||||
required this.destinations,
|
|
||||||
this.showBottomBar = true,
|
|
||||||
this.showFab = true,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
final Widget child;
|
|
||||||
final String title;
|
|
||||||
final bool showBottomBar;
|
|
||||||
final List<RouteDestination> destinations;
|
|
||||||
final bool showFab;
|
|
||||||
|
|
||||||
@override
|
|
||||||
// ignore: prefer_expression_function_bodies
|
|
||||||
Widget build(final BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
appBar: Breakpoints.mediumAndUp.isActive(context)
|
|
||||||
? PreferredSize(
|
|
||||||
preferredSize: const Size.fromHeight(52),
|
|
||||||
child: _RootAppBar(title: title),
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
endDrawer: const SupportDrawer(),
|
|
||||||
endDrawerEnableOpenDragGesture: false,
|
|
||||||
body: Row(
|
|
||||||
children: [
|
|
||||||
if (Breakpoints.medium.isActive(context))
|
|
||||||
_MainScreenNavigationRail(
|
|
||||||
destinations: destinations,
|
|
||||||
showFab: showFab,
|
|
||||||
),
|
|
||||||
if (Breakpoints.large.isActive(context))
|
|
||||||
_MainScreenNavigationDrawer(
|
|
||||||
destinations: destinations,
|
|
||||||
showFab: showFab,
|
|
||||||
),
|
|
||||||
Expanded(child: child),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
bottomNavigationBar: _BottomBar(
|
|
||||||
destinations: destinations,
|
|
||||||
hidden: !(Breakpoints.small.isActive(context) && showBottomBar),
|
|
||||||
key: const Key('bottomBar'),
|
|
||||||
),
|
|
||||||
floatingActionButton:
|
|
||||||
showFab && Breakpoints.small.isActive(context) && showBottomBar
|
|
||||||
? const BrandFab()
|
|
||||||
: null,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _RootAppBar extends StatelessWidget {
|
|
||||||
const _RootAppBar({
|
|
||||||
required this.title,
|
|
||||||
});
|
|
||||||
|
|
||||||
final String title;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(final BuildContext context) => AppBar(
|
|
||||||
title: AnimatedSwitcher(
|
|
||||||
duration: const Duration(milliseconds: 200),
|
|
||||||
transitionBuilder:
|
|
||||||
(final Widget child, final Animation<double> animation) =>
|
|
||||||
SlideTransition(
|
|
||||||
position: animation.drive(
|
|
||||||
Tween<Offset>(
|
|
||||||
begin: const Offset(0.0, 0.2),
|
|
||||||
end: Offset.zero,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: FadeTransition(
|
|
||||||
opacity: animation,
|
|
||||||
child: child,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: SizedBox(
|
|
||||||
key: ValueKey<String>(title),
|
|
||||||
width: double.infinity,
|
|
||||||
child: Text(
|
|
||||||
title,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
leading: context.router.pageCount > 1
|
|
||||||
? IconButton(
|
|
||||||
icon: const Icon(Icons.arrow_back),
|
|
||||||
onPressed: () => context.router.maybePop(),
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
actions: const [
|
|
||||||
SizedBox.shrink(),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
class _MainScreenNavigationRail extends StatelessWidget {
|
|
||||||
const _MainScreenNavigationRail({
|
|
||||||
required this.destinations,
|
|
||||||
this.showFab = true,
|
|
||||||
});
|
|
||||||
|
|
||||||
final List<RouteDestination> destinations;
|
|
||||||
final bool showFab;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(final BuildContext context) {
|
|
||||||
int? activeIndex = destinations.indexWhere(
|
|
||||||
(final destination) =>
|
|
||||||
context.router.isRouteActive(destination.route.routeName),
|
|
||||||
);
|
|
||||||
|
|
||||||
final prevActiveIndex = destinations.indexWhere(
|
|
||||||
(final destination) => context.router.stack
|
|
||||||
.any((final route) => route.name == destination.route.routeName),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (activeIndex == -1) {
|
|
||||||
if (prevActiveIndex != -1) {
|
|
||||||
activeIndex = prevActiveIndex;
|
|
||||||
} else {
|
|
||||||
activeIndex = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final isExtended = Breakpoints.large.isActive(context);
|
|
||||||
|
|
||||||
return LayoutBuilder(
|
|
||||||
builder: (final context, final constraints) => SingleChildScrollView(
|
|
||||||
child: ConstrainedBox(
|
|
||||||
constraints: BoxConstraints(minHeight: constraints.maxHeight),
|
|
||||||
child: IntrinsicHeight(
|
|
||||||
child: NavigationRail(
|
|
||||||
backgroundColor: Colors.transparent,
|
|
||||||
labelType: isExtended
|
|
||||||
? NavigationRailLabelType.none
|
|
||||||
: NavigationRailLabelType.all,
|
|
||||||
extended: isExtended,
|
|
||||||
leading: showFab
|
|
||||||
? const BrandFab(
|
|
||||||
extended: false,
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
groupAlignment: 0.0,
|
|
||||||
destinations: destinations
|
|
||||||
.map(
|
|
||||||
(final destination) => NavigationRailDestination(
|
|
||||||
icon: Icon(destination.icon),
|
|
||||||
label: Text(destination.label),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
selectedIndex: activeIndex,
|
|
||||||
onDestinationSelected: (final index) {
|
|
||||||
context.router.replaceAll([destinations[index].route]);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _BottomBar extends StatelessWidget {
|
|
||||||
const _BottomBar({
|
|
||||||
required this.destinations,
|
|
||||||
required this.hidden,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
final List<RouteDestination> destinations;
|
|
||||||
final bool hidden;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(final BuildContext context) {
|
|
||||||
final prevActiveIndex = destinations.indexWhere(
|
|
||||||
(final destination) => context.router.stack
|
|
||||||
.any((final route) => route.name == destination.route.routeName),
|
|
||||||
);
|
|
||||||
|
|
||||||
return AnimatedContainer(
|
|
||||||
duration: const Duration(milliseconds: 500),
|
|
||||||
height: hidden ? 0 : 80,
|
|
||||||
curve: Curves.easeInOutCubicEmphasized,
|
|
||||||
clipBehavior: Clip.antiAlias,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Theme.of(context).scaffoldBackgroundColor,
|
|
||||||
),
|
|
||||||
child: Platform.isIOS
|
|
||||||
? CupertinoTabBar(
|
|
||||||
currentIndex: prevActiveIndex == -1 ? 0 : prevActiveIndex,
|
|
||||||
onTap: (final index) {
|
|
||||||
context.router.replaceAll([destinations[index].route]);
|
|
||||||
},
|
|
||||||
items: destinations
|
|
||||||
.map(
|
|
||||||
(final destination) => BottomNavigationBarItem(
|
|
||||||
icon: Icon(destination.icon),
|
|
||||||
label: destination.label,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
)
|
|
||||||
: NavigationBar(
|
|
||||||
selectedIndex: prevActiveIndex == -1 ? 0 : prevActiveIndex,
|
|
||||||
labelBehavior: NavigationDestinationLabelBehavior.alwaysShow,
|
|
||||||
onDestinationSelected: (final index) {
|
|
||||||
context.router.replaceAll([destinations[index].route]);
|
|
||||||
},
|
|
||||||
destinations: destinations
|
|
||||||
.map(
|
|
||||||
(final destination) => NavigationDestination(
|
|
||||||
icon: Icon(destination.icon),
|
|
||||||
label: destination.label,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _MainScreenNavigationDrawer extends StatelessWidget {
|
|
||||||
const _MainScreenNavigationDrawer({
|
|
||||||
required this.destinations,
|
|
||||||
this.showFab = true,
|
|
||||||
});
|
|
||||||
|
|
||||||
final List<RouteDestination> destinations;
|
|
||||||
final bool showFab;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(final BuildContext context) {
|
|
||||||
int? activeIndex = destinations.indexWhere(
|
|
||||||
(final destination) =>
|
|
||||||
context.router.isRouteActive(destination.route.routeName),
|
|
||||||
);
|
|
||||||
|
|
||||||
final prevActiveIndex = destinations.indexWhere(
|
|
||||||
(final destination) => context.router.stack
|
|
||||||
.any((final route) => route.name == destination.route.routeName),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (activeIndex == -1) {
|
|
||||||
if (prevActiveIndex != -1) {
|
|
||||||
activeIndex = prevActiveIndex;
|
|
||||||
} else {
|
|
||||||
activeIndex = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return SizedBox(
|
|
||||||
height: MediaQuery.of(context).size.height,
|
|
||||||
width: 296,
|
|
||||||
child: NavigationDrawer(
|
|
||||||
key: const Key('PrimaryNavigationDrawer'),
|
|
||||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
|
||||||
surfaceTintColor: Colors.transparent,
|
|
||||||
selectedIndex: activeIndex,
|
|
||||||
onDestinationSelected: (final index) {
|
|
||||||
context.router.replaceAll([destinations[index].route]);
|
|
||||||
},
|
|
||||||
children: [
|
|
||||||
const Padding(
|
|
||||||
padding: EdgeInsets.all(16.0),
|
|
||||||
child: BrandFab(extended: true),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
...destinations.map(
|
|
||||||
(final destination) => NavigationDrawerDestination(
|
|
||||||
icon: Icon(destination.icon),
|
|
||||||
label: Text(destination.label),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
part of 'root_scaffold_with_subroute_selector.dart';
|
||||||
|
|
||||||
|
class _BottomTabBar extends SubrouteSelector {
|
||||||
|
const _BottomTabBar({
|
||||||
|
required super.subroutes,
|
||||||
|
required this.hidden,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final bool hidden;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(final BuildContext context) {
|
||||||
|
final int activeIndex = getActiveIndex(context);
|
||||||
|
|
||||||
|
return AnimatedContainer(
|
||||||
|
duration: const Duration(milliseconds: 500),
|
||||||
|
height: hidden ? 0 : 80,
|
||||||
|
curve: Curves.easeInOutCubicEmphasized,
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).scaffoldBackgroundColor,
|
||||||
|
),
|
||||||
|
child: Platform.isIOS
|
||||||
|
? CupertinoTabBar(
|
||||||
|
currentIndex: activeIndex,
|
||||||
|
onTap: openSubpage(context),
|
||||||
|
items: [
|
||||||
|
for (final destination in subroutes)
|
||||||
|
BottomNavigationBarItem(
|
||||||
|
icon: Icon(destination.icon),
|
||||||
|
label: destination.label.tr(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: NavigationBar(
|
||||||
|
selectedIndex: activeIndex,
|
||||||
|
labelBehavior: NavigationDestinationLabelBehavior.alwaysShow,
|
||||||
|
onDestinationSelected: openSubpage(context),
|
||||||
|
destinations: [
|
||||||
|
for (final destination in subroutes)
|
||||||
|
NavigationDestination(
|
||||||
|
icon: Icon(destination.icon),
|
||||||
|
label: destination.label.tr(),
|
||||||
|
),
|
||||||
|
].toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
part of 'root_scaffold_with_subroute_selector.dart';
|
||||||
|
|
||||||
|
class _NavigationDrawer extends SubrouteSelector {
|
||||||
|
const _NavigationDrawer({
|
||||||
|
required super.subroutes,
|
||||||
|
this.showFab = true,
|
||||||
|
});
|
||||||
|
|
||||||
|
final bool showFab;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(final BuildContext context) => SizedBox(
|
||||||
|
height: MediaQuery.of(context).size.height,
|
||||||
|
width: 296,
|
||||||
|
child: NavigationDrawer(
|
||||||
|
key: const Key('PrimaryNavigationDrawer'),
|
||||||
|
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||||
|
surfaceTintColor: Colors.transparent,
|
||||||
|
selectedIndex: getActiveIndex(context),
|
||||||
|
onDestinationSelected: openSubpage(context),
|
||||||
|
children: [
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.all(16.0),
|
||||||
|
child: BrandFab(extended: true),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
for (final destination in subroutes)
|
||||||
|
NavigationDrawerDestination(
|
||||||
|
icon: Icon(destination.icon),
|
||||||
|
label: Text(destination.label.tr()),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
part of 'root_scaffold_with_subroute_selector.dart';
|
||||||
|
|
||||||
|
class _NavigationRail extends SubrouteSelector {
|
||||||
|
const _NavigationRail({
|
||||||
|
required super.subroutes,
|
||||||
|
this.showFab = true,
|
||||||
|
});
|
||||||
|
|
||||||
|
final bool showFab;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(final BuildContext context) {
|
||||||
|
final isExtended = Breakpoints.large.isActive(context);
|
||||||
|
|
||||||
|
return LayoutBuilder(
|
||||||
|
builder: (final context, final constraints) => SingleChildScrollView(
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(minHeight: constraints.maxHeight),
|
||||||
|
child: IntrinsicHeight(
|
||||||
|
child: NavigationRail(
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
labelType: isExtended
|
||||||
|
? NavigationRailLabelType.none
|
||||||
|
: NavigationRailLabelType.all,
|
||||||
|
extended: isExtended,
|
||||||
|
leading: showFab
|
||||||
|
? const BrandFab(
|
||||||
|
extended: false,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
groupAlignment: 0.0,
|
||||||
|
destinations: [
|
||||||
|
for (final destination in subroutes)
|
||||||
|
NavigationRailDestination(
|
||||||
|
icon: Icon(destination.icon),
|
||||||
|
label: Text(destination.label.tr()),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
selectedIndex: getActiveIndex(context),
|
||||||
|
onDestinationSelected: openSubpage(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
part of 'root_scaffold_with_subroute_selector.dart';
|
||||||
|
|
||||||
|
class _RootAppBar extends StatelessWidget implements PreferredSizeWidget {
|
||||||
|
const _RootAppBar({
|
||||||
|
required this.title,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Size get preferredSize => const Size.fromHeight(52);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(final BuildContext context) => AppBar(
|
||||||
|
title: AnimatedSwitcher(
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
transitionBuilder:
|
||||||
|
(final Widget child, final Animation<double> animation) =>
|
||||||
|
SlideTransition(
|
||||||
|
position: animation.drive(
|
||||||
|
Tween<Offset>(
|
||||||
|
begin: const Offset(0.0, 0.2),
|
||||||
|
end: Offset.zero,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: FadeTransition(
|
||||||
|
opacity: animation,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: SizedBox(
|
||||||
|
key: ValueKey<String>(title),
|
||||||
|
width: double.infinity,
|
||||||
|
child: Text(
|
||||||
|
title,
|
||||||
|
maxLines: 1,
|
||||||
|
textAlign: TextAlign.start,
|
||||||
|
overflow: TextOverflow.fade,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
leading: context.router.pageCount > 1
|
||||||
|
? IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_back),
|
||||||
|
onPressed: () => context.router.maybePop(),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
actions: const [SizedBox.shrink()],
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,67 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:selfprivacy/ui/components/drawers/support_drawer.dart';
|
||||||
|
import 'package:selfprivacy/ui/components/pre_styled_buttons/flash_fab.dart';
|
||||||
|
import 'package:selfprivacy/ui/router/root_destinations.dart';
|
||||||
|
import 'package:selfprivacy/ui/router/router.dart';
|
||||||
|
import 'package:selfprivacy/utils/breakpoints.dart';
|
||||||
|
|
||||||
|
part 'bottom_tab_bar.dart';
|
||||||
|
part 'navigation_drawer.dart';
|
||||||
|
part 'navigation_rail.dart';
|
||||||
|
part 'root_app_bar.dart';
|
||||||
|
part 'subroute_selector.dart';
|
||||||
|
|
||||||
|
class RootScaffoldWithSubrouteSelector extends StatelessWidget {
|
||||||
|
const RootScaffoldWithSubrouteSelector({
|
||||||
|
required this.child,
|
||||||
|
required this.destinations,
|
||||||
|
this.showBottomBar = true,
|
||||||
|
this.showFab = true,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Widget child;
|
||||||
|
final bool showBottomBar;
|
||||||
|
final List<RouteDestination> destinations;
|
||||||
|
final bool showFab;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(final BuildContext context) => Scaffold(
|
||||||
|
appBar: Breakpoints.mediumAndUp.isActive(context)
|
||||||
|
? _RootAppBar(
|
||||||
|
title: getRouteTitle(context.router.current.name).tr(),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
endDrawer: const SupportDrawer(),
|
||||||
|
endDrawerEnableOpenDragGesture: false,
|
||||||
|
body: Row(
|
||||||
|
children: [
|
||||||
|
if (Breakpoints.medium.isActive(context))
|
||||||
|
_NavigationRail(
|
||||||
|
subroutes: destinations,
|
||||||
|
showFab: showFab,
|
||||||
|
)
|
||||||
|
else if (Breakpoints.large.isActive(context))
|
||||||
|
_NavigationDrawer(
|
||||||
|
subroutes: destinations,
|
||||||
|
showFab: showFab,
|
||||||
|
),
|
||||||
|
Expanded(child: child),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
bottomNavigationBar: _BottomTabBar(
|
||||||
|
key: const ValueKey('bottomBar'),
|
||||||
|
subroutes: destinations,
|
||||||
|
hidden: !(Breakpoints.small.isActive(context) && showBottomBar),
|
||||||
|
),
|
||||||
|
floatingActionButton:
|
||||||
|
showFab && Breakpoints.small.isActive(context) && showBottomBar
|
||||||
|
? const BrandFab()
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
part of 'root_scaffold_with_subroute_selector.dart';
|
||||||
|
|
||||||
|
abstract class SubrouteSelector extends StatelessWidget {
|
||||||
|
const SubrouteSelector({
|
||||||
|
required this.subroutes,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final List<RouteDestination> subroutes;
|
||||||
|
|
||||||
|
int getActiveIndex(final BuildContext context) {
|
||||||
|
int activeIndex = subroutes.indexWhere(
|
||||||
|
(final destination) =>
|
||||||
|
context.router.isRouteActive(destination.route.routeName),
|
||||||
|
);
|
||||||
|
|
||||||
|
final prevActiveIndex = subroutes.indexWhere(
|
||||||
|
(final destination) => context.router.stack.any(
|
||||||
|
(final route) => route.name == destination.route.routeName,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (activeIndex == -1) {
|
||||||
|
activeIndex = prevActiveIndex != -1 ? prevActiveIndex : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return activeIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
ValueSetter<int> openSubpage(final BuildContext context) => (final index) {
|
||||||
|
context.router.replaceAll([subroutes[index].route]);
|
||||||
|
};
|
||||||
|
}
|
|
@ -39,6 +39,7 @@ class NewDeviceScreen extends StatelessWidget {
|
||||||
|
|
||||||
class _KeyDisplay extends StatelessWidget {
|
class _KeyDisplay extends StatelessWidget {
|
||||||
const _KeyDisplay({required this.newDeviceKey});
|
const _KeyDisplay({required this.newDeviceKey});
|
||||||
|
|
||||||
final String newDeviceKey;
|
final String newDeviceKey;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -47,7 +48,7 @@ class _KeyDisplay extends StatelessWidget {
|
||||||
children: [
|
children: [
|
||||||
const Divider(),
|
const Divider(),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text(
|
SelectableText(
|
||||||
newDeviceKey,
|
newDeviceKey,
|
||||||
style: Theme.of(context).textTheme.bodyLarge!.copyWith(
|
style: Theme.of(context).textTheme.bodyLarge!.copyWith(
|
||||||
fontSize: 24,
|
fontSize: 24,
|
||||||
|
|
|
@ -3,7 +3,7 @@ import 'dart:io';
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.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/config/get_it_config.dart';
|
||||||
import 'package:selfprivacy/ui/components/list_tiles/section_title.dart';
|
import 'package:selfprivacy/ui/components/list_tiles/section_title.dart';
|
||||||
import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart';
|
import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart';
|
||||||
|
|
|
@ -1,10 +1,17 @@
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.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/logic/cubit/server_installation/server_installation_cubit.dart';
|
||||||
import 'package:selfprivacy/ui/components/buttons/dialog_action_button.dart';
|
import 'package:selfprivacy/ui/components/buttons/dialog_action_button.dart';
|
||||||
import 'package:selfprivacy/ui/layouts/brand_hero_screen.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()
|
@RoutePage()
|
||||||
class AppSettingsPage extends StatefulWidget {
|
class AppSettingsPage extends StatefulWidget {
|
||||||
|
@ -16,82 +23,35 @@ class AppSettingsPage extends StatefulWidget {
|
||||||
|
|
||||||
class _AppSettingsPageState extends State<AppSettingsPage> {
|
class _AppSettingsPageState extends State<AppSettingsPage> {
|
||||||
@override
|
@override
|
||||||
Widget build(final BuildContext context) {
|
Widget build(final BuildContext context) => BrandHeroScreen(
|
||||||
final bool isDarkModeOn =
|
hasBackButton: true,
|
||||||
context.watch<AppSettingsCubit>().state.isDarkModeOn;
|
hasFlashButton: false,
|
||||||
|
bodyPadding: const EdgeInsets.symmetric(
|
||||||
final bool isSystemDarkModeOn =
|
horizontal: 12,
|
||||||
context.watch<AppSettingsCubit>().state.isAutoDarkModeOn;
|
vertical: 16,
|
||||||
|
|
||||||
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),
|
|
||||||
),
|
),
|
||||||
SwitchListTile.adaptive(
|
heroTitle: 'application_settings.title'.tr(),
|
||||||
title: Text('application_settings.dark_theme_title'.tr()),
|
children: [
|
||||||
subtitle: Text('application_settings.dark_theme_description'.tr()),
|
_ThemePicker(
|
||||||
value: Theme.of(context).brightness == Brightness.dark,
|
key: ValueKey('theme_picker'.tr()),
|
||||||
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,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
_LanguagePicker(
|
||||||
const _ResetAppTile(),
|
key: ValueKey('language_picker'.tr()),
|
||||||
],
|
),
|
||||||
);
|
const Gap(8),
|
||||||
}
|
Padding(
|
||||||
}
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
child: Text(
|
||||||
class _ResetAppTile extends StatelessWidget {
|
'application_settings.dangerous_settings'.tr(),
|
||||||
const _ResetAppTile();
|
style: Theme.of(context).textTheme.labelLarge!.copyWith(
|
||||||
|
color: Theme.of(context).colorScheme.error,
|
||||||
@override
|
),
|
||||||
Widget build(final BuildContext context) => ListTile(
|
|
||||||
title: Text('application_settings.reset_config_title'.tr()),
|
|
||||||
subtitle: Text('application_settings.reset_config_description'.tr()),
|
|
||||||
onTap: () {
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (final _) => AlertDialog(
|
|
||||||
title: Text('modals.are_you_sure'.tr()),
|
|
||||||
content: Text('modals.purge_all_keys'.tr()),
|
|
||||||
actions: [
|
|
||||||
DialogActionButton(
|
|
||||||
text: 'modals.purge_all_keys_confirm'.tr(),
|
|
||||||
isRed: true,
|
|
||||||
onPressed: () {
|
|
||||||
context.read<ServerInstallationCubit>().clearAppConfig();
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
DialogActionButton(
|
|
||||||
text: 'basis.cancel'.tr(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
},
|
const Gap(4),
|
||||||
|
_ResetAppTile(
|
||||||
|
key: ValueKey('reset_app'.tr()),
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.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/config/get_it_config.dart';
|
||||||
import 'package:selfprivacy/logic/api_maps/tls_options.dart';
|
import 'package:selfprivacy/logic/api_maps/tls_options.dart';
|
||||||
import 'package:selfprivacy/logic/bloc/services/services_bloc.dart';
|
import 'package:selfprivacy/logic/bloc/services/services_bloc.dart';
|
||||||
import 'package:selfprivacy/logic/bloc/volumes/volumes_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/components/list_tiles/section_title.dart';
|
||||||
import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart';
|
import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart';
|
||||||
import 'package:selfprivacy/ui/router/router.dart';
|
import 'package:selfprivacy/ui/router/router.dart';
|
||||||
|
@ -60,17 +61,14 @@ class _DeveloperSettingsPageState extends State<DeveloperSettingsPage> {
|
||||||
title: Text('developer_settings.reset_onboarding'.tr()),
|
title: Text('developer_settings.reset_onboarding'.tr()),
|
||||||
subtitle:
|
subtitle:
|
||||||
Text('developer_settings.reset_onboarding_description'.tr()),
|
Text('developer_settings.reset_onboarding_description'.tr()),
|
||||||
enabled:
|
enabled: !InheritedAppController.of(context).shouldShowOnboarding,
|
||||||
!context.watch<AppSettingsCubit>().state.isOnboardingShowing,
|
onTap: () => InheritedAppController.of(context)
|
||||||
onTap: () => context
|
.setShouldShowOnboarding(true),
|
||||||
.read<AppSettingsCubit>()
|
|
||||||
.turnOffOnboarding(isOnboardingShowing: true),
|
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text('storage.start_migration_button'.tr()),
|
title: Text('storage.start_migration_button'.tr()),
|
||||||
subtitle: Text('storage.data_migration_notice'.tr()),
|
subtitle: Text('storage.data_migration_notice'.tr()),
|
||||||
enabled:
|
enabled: InheritedAppController.of(context).shouldShowOnboarding,
|
||||||
!context.watch<AppSettingsCubit>().state.isOnboardingShowing,
|
|
||||||
onTap: () => context.pushRoute(
|
onTap: () => context.pushRoute(
|
||||||
ServicesMigrationRoute(
|
ServicesMigrationRoute(
|
||||||
diskStatus: context.read<VolumesBloc>().state.diskStatus,
|
diskStatus: context.read<VolumesBloc>().state.diskStatus,
|
||||||
|
|
62
lib/ui/pages/more/app_settings/language_picker.dart
Normal file
62
lib/ui/pages/more/app_settings/language_picker.dart
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
part of 'app_settings.dart';
|
||||||
|
|
||||||
|
class _LanguagePicker extends StatelessWidget {
|
||||||
|
const _LanguagePicker({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(final BuildContext context) => ListTile(
|
||||||
|
title: Text(
|
||||||
|
'application_settings.language'.tr(),
|
||||||
|
),
|
||||||
|
subtitle: Text('application_settings.click_to_change_locale'.tr()),
|
||||||
|
trailing: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
|
child: Text(
|
||||||
|
Localization.getLanguageName(context.locale),
|
||||||
|
style: Theme.of(context).textTheme.labelLarge,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onTap: () async {
|
||||||
|
final appController = InheritedAppController.of(context);
|
||||||
|
final Locale? newLocale = await showDialog<Locale?>(
|
||||||
|
context: context,
|
||||||
|
builder: (final context) => const _LanguagePickerDialog(),
|
||||||
|
routeSettings: _LanguagePickerDialog.routeSettings,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (newLocale != null) {
|
||||||
|
await appController.setLocale(newLocale);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LanguagePickerDialog extends StatelessWidget {
|
||||||
|
const _LanguagePickerDialog();
|
||||||
|
static const routeSettings = RouteSettings(name: 'LanguagePickerDialog');
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(final BuildContext context) {
|
||||||
|
final appController = InheritedAppController.of(context);
|
||||||
|
|
||||||
|
return SimpleDialog(
|
||||||
|
title: Text('application_settings.language'.tr()),
|
||||||
|
children: [
|
||||||
|
for (final locale in appController.supportedLocales)
|
||||||
|
RadioMenuButton(
|
||||||
|
groupValue: appController.locale,
|
||||||
|
value: locale,
|
||||||
|
child: Text(
|
||||||
|
Localization.getLanguageName(locale),
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: locale == appController.locale
|
||||||
|
? FontWeight.w800
|
||||||
|
: FontWeight.w400,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onChanged: (final newValue) => Navigator.of(context).pop(newValue),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
42
lib/ui/pages/more/app_settings/reset_app_button.dart
Normal file
42
lib/ui/pages/more/app_settings/reset_app_button.dart
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
part of 'app_settings.dart';
|
||||||
|
|
||||||
|
class _ResetAppTile extends StatelessWidget {
|
||||||
|
const _ResetAppTile({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(final BuildContext context) => ListTile(
|
||||||
|
title: Text('application_settings.reset_config_title'.tr()),
|
||||||
|
subtitle: Text('application_settings.reset_config_description'.tr()),
|
||||||
|
onTap: () => showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (final context) => const _ResetAppDialog(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ResetAppDialog extends StatelessWidget {
|
||||||
|
const _ResetAppDialog();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(final BuildContext context) => AlertDialog(
|
||||||
|
title: Text('modals.are_you_sure'.tr()),
|
||||||
|
content: Text('modals.purge_all_keys'.tr()),
|
||||||
|
actions: [
|
||||||
|
DialogActionButton(
|
||||||
|
text: 'modals.purge_all_keys_confirm'.tr(),
|
||||||
|
isRed: true,
|
||||||
|
onPressed: () {
|
||||||
|
context.read<ServerInstallationCubit>().clearAppConfig();
|
||||||
|
|
||||||
|
context.router.maybePop([
|
||||||
|
const RootRoute(),
|
||||||
|
]);
|
||||||
|
context.resetLocale();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
DialogActionButton(
|
||||||
|
text: 'basis.cancel'.tr(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
31
lib/ui/pages/more/app_settings/theme_picker.dart
Normal file
31
lib/ui/pages/more/app_settings/theme_picker.dart
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
part of 'app_settings.dart';
|
||||||
|
|
||||||
|
class _ThemePicker extends StatelessWidget {
|
||||||
|
const _ThemePicker({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(final BuildContext context) {
|
||||||
|
final appController = InheritedAppController.of(context);
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
SwitchListTile.adaptive(
|
||||||
|
title: Text('application_settings.system_theme_mode_title'.tr()),
|
||||||
|
subtitle:
|
||||||
|
Text('application_settings.system_theme_mode_description'.tr()),
|
||||||
|
value: appController.systemThemeModeActive,
|
||||||
|
onChanged: appController.setSystemThemeModeFlag,
|
||||||
|
),
|
||||||
|
SwitchListTile.adaptive(
|
||||||
|
title: Text('application_settings.dark_theme_title'.tr()),
|
||||||
|
subtitle: Text('application_settings.change_application_theme'.tr()),
|
||||||
|
value: appController.darkThemeModeActive,
|
||||||
|
onChanged: appController.systemThemeModeActive
|
||||||
|
? null
|
||||||
|
: appController.setDarkThemeModeFlag,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,107 +0,0 @@
|
||||||
import 'package:auto_route/auto_route.dart';
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:gap/gap.dart';
|
|
||||||
import 'package:selfprivacy/config/get_it_config.dart';
|
|
||||||
import 'package:selfprivacy/logic/models/message.dart';
|
|
||||||
import 'package:selfprivacy/ui/components/list_tiles/log_list_tile.dart';
|
|
||||||
|
|
||||||
@RoutePage()
|
|
||||||
class ConsolePage extends StatefulWidget {
|
|
||||||
const ConsolePage({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<ConsolePage> createState() => _ConsolePageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ConsolePageState extends State<ConsolePage> {
|
|
||||||
bool paused = false;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
|
|
||||||
getIt<ConsoleModel>().addListener(update);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
getIt<ConsoleModel>().removeListener(update);
|
|
||||||
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
void update() {
|
|
||||||
/// listener update could come at any time, like when widget is already
|
|
||||||
/// unmounted or during frame build, adding as postframe callback ensures
|
|
||||||
/// that element is marked for rebuild
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((final _) {
|
|
||||||
if (!paused && mounted) {
|
|
||||||
setState(() => {});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void togglePause() {
|
|
||||||
paused ^= true;
|
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(final BuildContext context) => SafeArea(
|
|
||||||
child: Scaffold(
|
|
||||||
appBar: AppBar(
|
|
||||||
title: Text('console_page.title'.tr()),
|
|
||||||
leading: IconButton(
|
|
||||||
icon: const Icon(Icons.arrow_back),
|
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
IconButton(
|
|
||||||
icon: Icon(
|
|
||||||
paused ? Icons.play_arrow_outlined : Icons.pause_outlined,
|
|
||||||
),
|
|
||||||
onPressed: togglePause,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
body: FutureBuilder(
|
|
||||||
future: getIt.allReady(),
|
|
||||||
builder: (
|
|
||||||
final BuildContext context,
|
|
||||||
final AsyncSnapshot<void> snapshot,
|
|
||||||
) {
|
|
||||||
if (snapshot.hasData) {
|
|
||||||
final List<Message> messages =
|
|
||||||
getIt.get<ConsoleModel>().messages;
|
|
||||||
|
|
||||||
return ListView(
|
|
||||||
reverse: true,
|
|
||||||
shrinkWrap: true,
|
|
||||||
children: [
|
|
||||||
const Gap(20),
|
|
||||||
...messages.reversed.map(
|
|
||||||
(final message) => LogListItem(
|
|
||||||
key: ValueKey(message),
|
|
||||||
message: message,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: [
|
|
||||||
Text('console_page.waiting'.tr()),
|
|
||||||
const Gap(16),
|
|
||||||
const CircularProgressIndicator.adaptive(),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
304
lib/ui/pages/more/console/console_log_item_dialog.dart
Normal file
304
lib/ui/pages/more/console/console_log_item_dialog.dart
Normal file
|
@ -0,0 +1,304 @@
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:selfprivacy/logic/models/console_log.dart';
|
||||||
|
import 'package:selfprivacy/utils/platform_adapter.dart';
|
||||||
|
|
||||||
|
extension on ConsoleLog {
|
||||||
|
List<Widget> unwrapContent(final BuildContext context) => switch (this) {
|
||||||
|
(final RestApiRequestConsoleLog log) => [
|
||||||
|
if (log.method != null) _KeyValueRow('method', log.method),
|
||||||
|
if (log.uri != null) _KeyValueRow('uri', '${log.uri}'),
|
||||||
|
|
||||||
|
// headers bloc
|
||||||
|
if (log.headers?.isNotEmpty ?? false) ...[
|
||||||
|
const _SectionRow('console_page.headers'),
|
||||||
|
for (final entry in log.headers!.entries)
|
||||||
|
_KeyValueRow(entry.key, '${entry.value}'),
|
||||||
|
],
|
||||||
|
|
||||||
|
// data
|
||||||
|
const _SectionRow('console_page.data'),
|
||||||
|
_DataRow('${log.data}'),
|
||||||
|
],
|
||||||
|
(final RestApiResponseConsoleLog log) => [
|
||||||
|
if (log.method != null) _KeyValueRow('method', '${log.method}'),
|
||||||
|
if (log.uri != null) _KeyValueRow('uri', '${log.uri}'),
|
||||||
|
if (log.statusCode != null)
|
||||||
|
_KeyValueRow('statusCode', '${log.statusCode}'),
|
||||||
|
|
||||||
|
// data
|
||||||
|
const _SectionRow('console_page.response_data'),
|
||||||
|
_DataRow('${log.data}'),
|
||||||
|
],
|
||||||
|
(final GraphQlRequestConsoleLog log) => [
|
||||||
|
// // context
|
||||||
|
// if (log.context != null) ...[
|
||||||
|
// const _SectionRow('console_page.context'),
|
||||||
|
// _DataRow('${log.context}'),
|
||||||
|
// ],
|
||||||
|
|
||||||
|
const _SectionRow('console_page.operation'),
|
||||||
|
if (log.operation != null) ...[
|
||||||
|
_KeyValueRow(
|
||||||
|
'console_page.operation_type'.tr(),
|
||||||
|
log.operationType,
|
||||||
|
),
|
||||||
|
_KeyValueRow(
|
||||||
|
'console_page.operation_name'.tr(),
|
||||||
|
log.operation?.operationName,
|
||||||
|
),
|
||||||
|
const Divider(),
|
||||||
|
// data
|
||||||
|
_DataRow(log.operationDocument),
|
||||||
|
],
|
||||||
|
// preset variables
|
||||||
|
if (log.variables?.isNotEmpty ?? false) ...[
|
||||||
|
const _SectionRow('console_page.variables'),
|
||||||
|
for (final entry in log.variables!.entries)
|
||||||
|
_KeyValueRow(entry.key, '${entry.value}'),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
(final GraphQlResponseConsoleLog log) => [
|
||||||
|
// // context
|
||||||
|
// const _SectionRow('console_page.context'),
|
||||||
|
// _DataRow('${log.context}'),
|
||||||
|
// data
|
||||||
|
if (log.data != null) ...[
|
||||||
|
const _SectionRow('console_page.data'),
|
||||||
|
for (final entry in log.data!.entries)
|
||||||
|
_KeyValueRow(entry.key, '${entry.value}'),
|
||||||
|
],
|
||||||
|
// errors
|
||||||
|
if (log.errors?.isNotEmpty ?? false) ...[
|
||||||
|
const _SectionRow('console_page.errors'),
|
||||||
|
for (final entry in log.errors!) ...[
|
||||||
|
_KeyValueRow(
|
||||||
|
'${'console_page.error_message'.tr()}: ',
|
||||||
|
entry.message,
|
||||||
|
),
|
||||||
|
_KeyValueRow(
|
||||||
|
'${'console_page.error_path'.tr()}: ',
|
||||||
|
'${entry.path}',
|
||||||
|
),
|
||||||
|
if (entry.locations?.isNotEmpty ?? false)
|
||||||
|
_KeyValueRow(
|
||||||
|
'${'console_page.error_locations'.tr()}: ',
|
||||||
|
'${entry.locations}',
|
||||||
|
),
|
||||||
|
if (entry.extensions?.isNotEmpty ?? false)
|
||||||
|
_KeyValueRow(
|
||||||
|
'${'console_page.error_extensions'.tr()}: ',
|
||||||
|
'${entry.extensions}',
|
||||||
|
),
|
||||||
|
const Divider(),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
(final ManualConsoleLog log) => [
|
||||||
|
_DataRow(log.content),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// dialog with detailed log content
|
||||||
|
class ConsoleItemDialog extends StatelessWidget {
|
||||||
|
const ConsoleItemDialog({
|
||||||
|
required this.log,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final ConsoleLog log;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(final BuildContext context) => AlertDialog(
|
||||||
|
scrollable: true,
|
||||||
|
title: Text(log.title),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
vertical: 16,
|
||||||
|
horizontal: 12,
|
||||||
|
),
|
||||||
|
content: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
const Divider(),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
|
child: SelectableText.rich(
|
||||||
|
TextSpan(
|
||||||
|
style: DefaultTextStyle.of(context).style,
|
||||||
|
children: <TextSpan>[
|
||||||
|
TextSpan(
|
||||||
|
text: '${'console_page.logged_at'.tr()}: ',
|
||||||
|
style: const TextStyle(),
|
||||||
|
),
|
||||||
|
TextSpan(
|
||||||
|
text: '${log.timeString} (${log.fullUTCString})',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
fontFeatures: [FontFeature.tabularFigures()],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(),
|
||||||
|
...log.unwrapContent(context),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
if (log is LogWithRawResponse)
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => PlatformAdapter.setClipboard(
|
||||||
|
(log as LogWithRawResponse).rawResponse,
|
||||||
|
),
|
||||||
|
child: Text('console_page.copy_raw'.tr()),
|
||||||
|
),
|
||||||
|
// A button to copy the request to the clipboard
|
||||||
|
if (log.shareableData?.isNotEmpty ?? false)
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => PlatformAdapter.setClipboard(log.shareableData!),
|
||||||
|
child: Text('console_page.copy'.tr()),
|
||||||
|
),
|
||||||
|
// close dialog
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
child: Text('basis.close'.tr()),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// different sections delimiter with `title`
|
||||||
|
class _SectionRow extends StatelessWidget {
|
||||||
|
const _SectionRow(this.title);
|
||||||
|
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(final BuildContext context) => Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
|
child: DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border(
|
||||||
|
bottom: BorderSide(
|
||||||
|
color: Theme.of(context).colorScheme.outlineVariant,
|
||||||
|
width: 2.4,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: SelectableText(
|
||||||
|
title.tr(),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
fontSize: 20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// data row with a {key: value} pair
|
||||||
|
class _KeyValueRow extends StatelessWidget {
|
||||||
|
const _KeyValueRow(this.title, this.value);
|
||||||
|
|
||||||
|
/// headers thath should be hidden in screenshots, but still accessible for
|
||||||
|
/// user, as opposed to `[[ConsoleLog]]`
|
||||||
|
/// `RestApiRequestConsoleLog.blacklistedHeaders` which need to be filtered
|
||||||
|
/// out from clipboard content
|
||||||
|
static const List<String> hideList = ['Authorization'];
|
||||||
|
|
||||||
|
final String title;
|
||||||
|
final String? value;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(final BuildContext context) => hideList.contains(title)
|
||||||
|
? _ObscuredKeyValueRow(title, value)
|
||||||
|
: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
|
child: SelectableText.rich(
|
||||||
|
TextSpan(
|
||||||
|
style: DefaultTextStyle.of(context).style,
|
||||||
|
children: <TextSpan>[
|
||||||
|
TextSpan(
|
||||||
|
text: '$title: ',
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
TextSpan(text: value ?? ''),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ObscuredKeyValueRow extends StatefulWidget {
|
||||||
|
const _ObscuredKeyValueRow(this.title, this.value);
|
||||||
|
|
||||||
|
final String title;
|
||||||
|
final String? value;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_ObscuredKeyValueRow> createState() => _ObscuredKeyValueRowState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ObscuredKeyValueRowState extends State<_ObscuredKeyValueRow> {
|
||||||
|
static const obscuringCharacter = '•';
|
||||||
|
bool _obscureValue = true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(final BuildContext context) => Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: SelectableText.rich(
|
||||||
|
TextSpan(
|
||||||
|
style: DefaultTextStyle.of(context).style,
|
||||||
|
children: <TextSpan>[
|
||||||
|
TextSpan(
|
||||||
|
text: '${widget.title}: ',
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
TextSpan(
|
||||||
|
text: _obscureValue
|
||||||
|
? obscuringCharacter * (widget.value?.length ?? 4)
|
||||||
|
: widget.value ?? '',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontFeatures: [FontFeature.tabularFigures()],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
_obscureValue
|
||||||
|
? Icons.visibility_outlined
|
||||||
|
: Icons.visibility_off_outlined,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
_obscureValue ^= true; // toggle value
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// data row with only text
|
||||||
|
class _DataRow extends StatelessWidget {
|
||||||
|
const _DataRow(this.data);
|
||||||
|
|
||||||
|
final String? data;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(final BuildContext context) => Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
|
child: SelectableText(
|
||||||
|
data ?? 'null',
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.w400),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
75
lib/ui/pages/more/console/console_log_item_widget.dart
Normal file
75
lib/ui/pages/more/console/console_log_item_widget.dart
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:selfprivacy/logic/models/console_log.dart';
|
||||||
|
import 'package:selfprivacy/ui/pages/more/console/console_log_item_dialog.dart';
|
||||||
|
|
||||||
|
extension on ConsoleLog {
|
||||||
|
Color resolveColor(final BuildContext context) => isError
|
||||||
|
? Theme.of(context).colorScheme.error
|
||||||
|
: switch (this) {
|
||||||
|
(final RestApiRequestConsoleLog _) =>
|
||||||
|
Theme.of(context).colorScheme.secondary,
|
||||||
|
(final RestApiResponseConsoleLog _) =>
|
||||||
|
Theme.of(context).colorScheme.primary,
|
||||||
|
(final GraphQlRequestConsoleLog _) =>
|
||||||
|
Theme.of(context).colorScheme.secondary,
|
||||||
|
(final GraphQlResponseConsoleLog _) =>
|
||||||
|
Theme.of(context).colorScheme.tertiary,
|
||||||
|
(final ManualConsoleLog _) => Theme.of(context).colorScheme.tertiary,
|
||||||
|
};
|
||||||
|
|
||||||
|
IconData resolveIcon() => switch (this) {
|
||||||
|
(final RestApiRequestConsoleLog _) => Icons.upload_outlined,
|
||||||
|
(final RestApiResponseConsoleLog _) => Icons.download_outlined,
|
||||||
|
(final GraphQlRequestConsoleLog _) => Icons.arrow_circle_up_outlined,
|
||||||
|
(final GraphQlResponseConsoleLog _) => Icons.arrow_circle_down_outlined,
|
||||||
|
(final ManualConsoleLog _) => Icons.read_more_outlined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class ConsoleLogItemWidget extends StatelessWidget {
|
||||||
|
const ConsoleLogItemWidget({
|
||||||
|
required this.log,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final ConsoleLog log;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(final BuildContext context) => Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
|
child: ListTile(
|
||||||
|
dense: true,
|
||||||
|
title: Text.rich(
|
||||||
|
TextSpan(
|
||||||
|
style: DefaultTextStyle.of(context).style,
|
||||||
|
children: <TextSpan>[
|
||||||
|
TextSpan(
|
||||||
|
text: '${log.timeString}: ',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontFeatures: [FontFeature.tabularFigures()],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextSpan(
|
||||||
|
text: log.title,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
log.content,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
maxLines: 3,
|
||||||
|
),
|
||||||
|
leading: Icon(log.resolveIcon()),
|
||||||
|
iconColor: log.resolveColor(context),
|
||||||
|
onTap: () => showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (final BuildContext context) =>
|
||||||
|
ConsoleItemDialog(log: log),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
143
lib/ui/pages/more/console/console_page.dart
Normal file
143
lib/ui/pages/more/console/console_page.dart
Normal file
|
@ -0,0 +1,143 @@
|
||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:selfprivacy/config/get_it_config.dart';
|
||||||
|
import 'package:selfprivacy/logic/models/console_log.dart';
|
||||||
|
import 'package:selfprivacy/ui/pages/more/console/console_log_item_widget.dart';
|
||||||
|
|
||||||
|
/// listing with 500 latest app operations.
|
||||||
|
@RoutePage()
|
||||||
|
class ConsolePage extends StatefulWidget {
|
||||||
|
const ConsolePage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ConsolePage> createState() => _ConsolePageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ConsolePageState extends State<ConsolePage> {
|
||||||
|
ConsoleModel get console => getIt<ConsoleModel>();
|
||||||
|
|
||||||
|
/// should freeze logs state to properly read logs
|
||||||
|
late final Future<void> future;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
future = getIt.allReady();
|
||||||
|
console.addListener(update);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
console.removeListener(update);
|
||||||
|
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void update() {
|
||||||
|
/// listener update could come at any time, like when widget is already
|
||||||
|
/// unmounted or during frame build, adding as postframe callback ensures
|
||||||
|
/// that element is marked for rebuild
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((final _) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() => {});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(final BuildContext context) => SafeArea(
|
||||||
|
child: Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text('console_page.title'.tr()),
|
||||||
|
leading: IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_back),
|
||||||
|
onPressed: () => Navigator.of(context).maybePop(),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
console.paused
|
||||||
|
? Icons.play_arrow_outlined
|
||||||
|
: Icons.pause_outlined,
|
||||||
|
),
|
||||||
|
onPressed: console.paused ? console.play : console.pause,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: Scrollbar(
|
||||||
|
child: FutureBuilder(
|
||||||
|
future: future,
|
||||||
|
builder: (
|
||||||
|
final BuildContext context,
|
||||||
|
final AsyncSnapshot<void> snapshot,
|
||||||
|
) {
|
||||||
|
if (snapshot.hasData) {
|
||||||
|
final List<ConsoleLog> logs = console.logs;
|
||||||
|
|
||||||
|
return logs.isEmpty
|
||||||
|
? const _ConsoleViewEmpty()
|
||||||
|
: _ConsoleViewLoaded(logs: logs);
|
||||||
|
}
|
||||||
|
|
||||||
|
return const _ConsoleViewLoading();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ConsoleViewLoading extends StatelessWidget {
|
||||||
|
const _ConsoleViewLoading();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(final BuildContext context) => Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
Text('console_page.waiting'.tr()),
|
||||||
|
const Gap(16),
|
||||||
|
const Expanded(
|
||||||
|
child: Center(
|
||||||
|
child: CircularProgressIndicator.adaptive(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ConsoleViewEmpty extends StatelessWidget {
|
||||||
|
const _ConsoleViewEmpty();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(final BuildContext context) => Align(
|
||||||
|
alignment: Alignment.topCenter,
|
||||||
|
child: Text('console_page.history_empty'.tr()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ConsoleViewLoaded extends StatelessWidget {
|
||||||
|
const _ConsoleViewLoaded({required this.logs});
|
||||||
|
|
||||||
|
final List<ConsoleLog> logs;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(final BuildContext context) => ListView.separated(
|
||||||
|
primary: true,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
|
itemCount: logs.length,
|
||||||
|
itemBuilder: (final BuildContext context, final int index) {
|
||||||
|
final log = logs[logs.length - 1 - index];
|
||||||
|
|
||||||
|
return ConsoleLogItemWidget(
|
||||||
|
key: ValueKey(log),
|
||||||
|
log: log,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
separatorBuilder: (final context, final _) => const Divider(),
|
||||||
|
);
|
||||||
|
}
|
|
@ -21,11 +21,8 @@ class MorePage extends StatelessWidget {
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: Breakpoints.small.isActive(context)
|
appBar: Breakpoints.small.isActive(context)
|
||||||
? PreferredSize(
|
? BrandHeader(
|
||||||
preferredSize: const Size.fromHeight(52),
|
title: 'basis.more'.tr(),
|
||||||
child: BrandHeader(
|
|
||||||
title: 'basis.more'.tr(),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
body: ListView(
|
body: ListView(
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:flutter/material.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/pages/onboarding/views/views.dart';
|
||||||
import 'package:selfprivacy/ui/router/router.dart';
|
import 'package:selfprivacy/ui/router/router.dart';
|
||||||
|
|
||||||
|
@ -37,7 +37,8 @@ class _OnboardingPageState extends State<OnboardingPage> {
|
||||||
),
|
),
|
||||||
OnboardingSecondView(
|
OnboardingSecondView(
|
||||||
onProceed: () {
|
onProceed: () {
|
||||||
context.read<AppSettingsCubit>().turnOffOnboarding();
|
InheritedAppController.of(context)
|
||||||
|
.setShouldShowOnboarding(false);
|
||||||
context.router.replaceAll([
|
context.router.replaceAll([
|
||||||
const RootRoute(),
|
const RootRoute(),
|
||||||
const InitializingRoute(),
|
const InitializingRoute(),
|
||||||
|
|
|
@ -65,11 +65,8 @@ class _ProvidersPageState extends State<ProvidersPage> {
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: Breakpoints.small.isActive(context)
|
appBar: Breakpoints.small.isActive(context)
|
||||||
? PreferredSize(
|
? BrandHeader(
|
||||||
preferredSize: const Size.fromHeight(52),
|
title: 'basis.providers_title'.tr(),
|
||||||
child: BrandHeader(
|
|
||||||
title: 'basis.providers_title'.tr(),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
body: ListView(
|
body: ListView(
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
import 'package:flutter/material.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/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/root_destinations.dart';
|
||||||
import 'package:selfprivacy/ui/router/router.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 {
|
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
|
@override
|
||||||
Widget build(final BuildContext context) {
|
Widget build(final BuildContext context) {
|
||||||
final bool isReady = context.watch<ServerInstallationCubit>().state
|
final bool isReady = context.watch<ServerInstallationCubit>().state
|
||||||
is ServerInstallationFinished;
|
is ServerInstallationFinished;
|
||||||
|
|
||||||
if (context.read<AppSettingsCubit>().state.isOnboardingShowing) {
|
|
||||||
context.router.replace(const OnboardingRoute());
|
|
||||||
}
|
|
||||||
|
|
||||||
return AutoRouter(
|
return AutoRouter(
|
||||||
builder: (final context, final child) {
|
builder: (final context, final child) {
|
||||||
final currentDestinationIndex = destinations.indexWhere(
|
final currentDestinationIndex = rootDestinations.indexWhere(
|
||||||
(final destination) =>
|
(final destination) =>
|
||||||
context.router.isRouteActive(destination.route.routeName),
|
context.router.isRouteActive(destination.route.routeName),
|
||||||
);
|
);
|
||||||
final isOtherRouterActive =
|
final isOtherRouterActive =
|
||||||
context.router.root.current.name != RootRoute.name;
|
context.router.root.current.name != RootRoute.name;
|
||||||
final routeName = getRouteTitle(context.router.current.name).tr();
|
|
||||||
return RootScaffoldWithNavigation(
|
return RootScaffoldWithSubrouteSelector(
|
||||||
title: routeName,
|
destinations: rootDestinations,
|
||||||
destinations: destinations,
|
|
||||||
showBottomBar:
|
showBottomBar:
|
||||||
!(currentDestinationIndex == -1 && !isOtherRouterActive),
|
!(currentDestinationIndex == -1 && !isOtherRouterActive),
|
||||||
showFab: isReady,
|
showFab: isReady,
|
||||||
|
@ -53,99 +52,3 @@ class _RootPageState extends State<RootPage> with TickerProviderStateMixin {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class MainScreenNavigationRail extends StatelessWidget {
|
|
||||||
const MainScreenNavigationRail({
|
|
||||||
required this.destinations,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
final List<RouteDestination> destinations;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(final BuildContext context) {
|
|
||||||
int? activeIndex = destinations.indexWhere(
|
|
||||||
(final destination) =>
|
|
||||||
context.router.isRouteActive(destination.route.routeName),
|
|
||||||
);
|
|
||||||
if (activeIndex == -1) {
|
|
||||||
activeIndex = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.all(8.0),
|
|
||||||
child: SizedBox(
|
|
||||||
height: MediaQuery.of(context).size.height,
|
|
||||||
width: 72,
|
|
||||||
child: LayoutBuilder(
|
|
||||||
builder: (final context, final constraints) => SingleChildScrollView(
|
|
||||||
child: ConstrainedBox(
|
|
||||||
constraints: BoxConstraints(minHeight: constraints.maxHeight),
|
|
||||||
child: IntrinsicHeight(
|
|
||||||
child: NavigationRail(
|
|
||||||
backgroundColor: Colors.transparent,
|
|
||||||
labelType: NavigationRailLabelType.all,
|
|
||||||
destinations: destinations
|
|
||||||
.map(
|
|
||||||
(final destination) => NavigationRailDestination(
|
|
||||||
icon: Icon(destination.icon),
|
|
||||||
label: Text(destination.label),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
selectedIndex: activeIndex,
|
|
||||||
onDestinationSelected: (final index) {
|
|
||||||
context.router.replaceAll([destinations[index].route]);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class MainScreenNavigationDrawer extends StatelessWidget {
|
|
||||||
const MainScreenNavigationDrawer({
|
|
||||||
required this.destinations,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
final List<RouteDestination> destinations;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(final BuildContext context) {
|
|
||||||
int? activeIndex = destinations.indexWhere(
|
|
||||||
(final destination) =>
|
|
||||||
context.router.isRouteActive(destination.route.routeName),
|
|
||||||
);
|
|
||||||
if (activeIndex == -1) {
|
|
||||||
activeIndex = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return SizedBox(
|
|
||||||
height: MediaQuery.of(context).size.height,
|
|
||||||
width: 296,
|
|
||||||
child: LayoutBuilder(
|
|
||||||
builder: (final context, final constraints) => NavigationDrawer(
|
|
||||||
key: const Key('PrimaryNavigationDrawer'),
|
|
||||||
selectedIndex: activeIndex,
|
|
||||||
onDestinationSelected: (final index) {
|
|
||||||
context.router.replaceAll([destinations[index].route]);
|
|
||||||
},
|
|
||||||
children: [
|
|
||||||
const SizedBox(height: 18),
|
|
||||||
...destinations.map(
|
|
||||||
(final destination) => NavigationDrawerDestination(
|
|
||||||
icon: Icon(destination.icon),
|
|
||||||
label: Text(destination.label),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue