Merge branch 'master' into inex/april-refactor

# Conflicts:
#	lib/config/get_it_config.dart
#	lib/config/hive_config.dart
#	lib/logic/api_maps/graphql_maps/graphql_api_map.dart
#	lib/logic/cubit/server_installation/server_installation_repository.dart
#	lib/logic/cubit/server_installation/server_installation_state.dart
#	lib/logic/get_it/api_config.dart
This commit is contained in:
Inex Code 2024-06-25 18:02:51 +03:00
commit 4ce7b0bcdb
126 changed files with 3687 additions and 2042 deletions

6
.gitignore vendored
View file

@ -40,3 +40,9 @@ app.*.symbols
# Obfuscation related
app.*.map.json
# Flatpak
.flatpak-builder/
flatpak-build/
flatpak-repo/
*.flatpak

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

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

View file

@ -1,3 +1,10 @@
plugins {
id "com.android.application"
id "kotlin-android"
id "dev.flutter.flutter-gradle-plugin"
}
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
@ -6,10 +13,6 @@ if (localPropertiesFile.exists()) {
}
}
def flutterRoot = localProperties.getProperty('flutter.sdk')
if (flutterRoot == null) {
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
}
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
@ -21,14 +24,9 @@ if (flutterVersionName == null) {
flutterVersionName = '1.0'
}
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android {
namespace 'org.selfprivacy.app'
compileSdkVersion flutter.compileSdkVersion
namespace 'org.selfprivacy.app'
compileSdkVersion flutter.compileSdkVersion
ndkVersion flutter.ndkVersion
sourceSets {
@ -43,13 +41,16 @@ android {
kotlinOptions {
jvmTarget = '1.8'
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
lintOptions {
disable 'InvalidPackage'
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "org.selfprivacy.app"
minSdkVersion 21
targetSdkVersion 34
@ -57,31 +58,33 @@ android {
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
buildTypes {
debug {
flavorDimensions "default"
productFlavors {
fdroid {
applicationId "pro.kherel.selfprivacy"
}
production {
applicationIdSuffix ""
profile {
}
nightly {
applicationIdSuffix ".nightly"
versionCode project.getVersionCode()
versionName "nightly-" + project.getVersionCode()
release {
}
}
buildFeatures {
flavorDimensions = ["default"]
}
flavorDimensions "default"
productFlavors {
fdroid {
dimension 'default'
applicationId "pro.kherel.selfprivacy"
}
production {
applicationIdSuffix ""
dimension 'default'
}
nightly {
dimension 'default'
applicationIdSuffix ".nightly"
versionCode project.getVersionCode()
versionName "nightly-" + project.getVersionCode()
@ -93,6 +96,5 @@ flutter {
source '../..'
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
}
dependencies {}

View file

@ -1,6 +1,5 @@
buildscript {
ext.kotlin_version = '1.9.21'
ext.getVersionCode = { ->
ext.getVersionCode = { ->
try {
def stdout = new ByteArrayOutputStream()
exec {
@ -13,15 +12,6 @@ buildscript {
return -1
}
}
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.1.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
allprojects {

View file

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

View file

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

View file

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

View file

@ -1,19 +1,7 @@
Login as root user to your server and look at the contents of the file `/etc/nixos/userdata/tokens.json`
[Login as root user to your server](https://selfprivacy.org/docs/how-to-guides/root_ssh/) and enter this command:
```sh
cat /etc/nixos/userdata/tokens.json
sp-print-api-token
```
This file will have a similar construction:
```json
{
"tokens": [
{
"token": "token_to_copy",
"name": "device_name",
"date": "date"
}
```
Copy the token from the file and paste it in the next window.
Copy the token from the terminal and paste it in the next window.

View file

@ -1,19 +1,7 @@
Login as root user to your server and look at the contents of the file `/etc/nixos/userdata/tokens.json`
[Login as root user to your server](https://selfprivacy.org/docs/how-to-guides/root_ssh/) and enter this command:
```sh
cat /etc/nixos/userdata/tokens.json
sp-print-api-token
```
This file will have a similar construction:
```json
{
"tokens": [
{
"token": "token_to_copy",
"name": "device_name",
"date": "date"
}
```
Copy the token from the file and paste it in the next window.
Copy the token from the terminal and paste it in the next window.

View file

@ -1,19 +1,7 @@
Login as root user to your server and look at the contents of the file `/etc/nixos/userdata/tokens.json`
[Login as root user to your server](https://selfprivacy.org/docs/how-to-guides/root_ssh/) and enter this command:
```sh
cat /etc/nixos/userdata/tokens.json
sp-print-api-token
```
This file will have a similar construction:
```json
{
"tokens": [
{
"token": "token_to_copy",
"name": "device_name",
"date": "date"
}
```
Copy the token from the file and paste it in the next window.
Copy the token from the terminal and paste it in the next window.

View file

@ -1,19 +1,7 @@
Login as root user to your server and look at the contents of the file `/etc/nixos/userdata/tokens.json`
[Login as root user to your server](https://selfprivacy.org/docs/how-to-guides/root_ssh/) and enter this command:
```sh
cat /etc/nixos/userdata/tokens.json
sp-print-api-token
```
This file will have a similar construction:
```json
{
"tokens": [
{
"token": "token_to_copy",
"name": "device_name",
"date": "date"
}
```
Copy the token from the file and paste it in the next window.
Copy the token from the terminal and paste it in the next window.

View file

@ -1,19 +1,7 @@
Login as root user to your server and look at the contents of the file `/etc/nixos/userdata/tokens.json`
[Login as root user to your server](https://selfprivacy.org/docs/how-to-guides/root_ssh/) and enter this command:
```sh
cat /etc/nixos/userdata/tokens.json
sp-print-api-token
```
This file will have a similar construction:
```json
{
"tokens": [
{
"token": "token_to_copy",
"name": "device_name",
"date": "date"
}
```
Copy the token from the file and paste it in the next window.
Copy the token from the terminal and paste it in the next window.

View file

@ -1,19 +1,7 @@
Login as root user to your server and look at the contents of the file `/etc/nixos/userdata/tokens.json`
[Login as root user to your server](https://selfprivacy.org/docs/how-to-guides/root_ssh/) and enter this command:
```sh
cat /etc/nixos/userdata/tokens.json
sp-print-api-token
```
This file will have a similar construction:
```json
{
"tokens": [
{
"token": "token_to_copy",
"name": "device_name",
"date": "date"
}
```
Copy the token from the file and paste it in the next window.
Copy the token from the terminal and paste it in the next window.

View file

@ -1,19 +1,7 @@
Login as root user to your server and look at the contents of the file `/etc/nixos/userdata/tokens.json`
[Login as root user to your server](https://selfprivacy.org/docs/how-to-guides/root_ssh/) and enter this command:
```sh
cat /etc/nixos/userdata/tokens.json
sp-print-api-token
```
This file will have a similar construction:
```json
{
"tokens": [
{
"token": "token_to_copy",
"name": "device_name",
"date": "date"
}
```
Copy the token from the file and paste it in the next window.
Copy the token from the terminal and paste it in the next window.

View file

@ -1,19 +1,7 @@
Login as root user to your server and look at the contents of the file `/etc/nixos/userdata/tokens.json`
[Login as root user to your server](https://selfprivacy.org/docs/how-to-guides/root_ssh/) and enter this command:
```sh
cat /etc/nixos/userdata/tokens.json
sp-print-api-token
```
This file will have a similar construction:
```json
{
"tokens": [
{
"token": "token_to_copy",
"name": "device_name",
"date": "date"
}
```
Copy the token from the file and paste it in the next window.
Copy the token from the terminal and paste it in the next window.

View file

@ -333,14 +333,12 @@
},
"application_settings": {
"title": "إعدادات التطبيق",
"system_dark_theme_title": "الوضع الافتراضي للنظام",
"system_dark_theme_description": "قم بتطبيق الوضع الفاتح أو الداكن حسب إعدادات النظام",
"system_theme_mode_title": "الوضع الافتراضي للنظام",
"system_theme_mode_description": "قم بتطبيق الوضع الفاتح أو الداكن حسب إعدادات النظام",
"dark_theme_title": "الوضع الداكن",
"change_application_theme": "قم بتبديل وضع التطبيق",
"dangerous_settings": "إعدادات خطرة",
"reset_config_title": "قم بإعادة ضبط إعدادات التطبيق",
"delete_server_title": "قم بحذف الخادم",
"delete_server_description": "سيزيل هذا الخادم الخاص بك، حيث أنه لن تتمكن من الوصول إليه بعد ذلك.",
"dark_theme_description": "قم بتبديل وضع التطبيق",
"reset_config_description": "قم بإعادة ضبط مفاتيح API والمستخدم المميز."
},
"ssh": {

View file

@ -54,15 +54,15 @@
},
"application_settings": {
"title": "Tətbiq parametrləri",
"system_theme_mode_title": "Defolt sistem mövzusu",
"system_theme_mode_description": "Sistem parametrlərindən asılı olaraq açıq və ya qaranlıq mövzudan istifadə edin",
"dark_theme_title": "Qaranlıq mövzu",
"change_application_theme": "Rəng mövzusunu dəyişdirin",
"dangerous_settings": "Təhlükəli Parametrlər",
"reset_config_title": "Tətbiq Sıfırlayın",
"reset_config_description": "API və Super İstifadəçi Açarlarını sıfırlayın.",
"delete_server_title": "Serveri silin",
"dark_theme_description": "Rəng mövzusunu dəyişdirin",
"delete_server_description": "Əməliyyat serveri siləcək. Bundan sonra o, əlçatmaz olacaq.",
"system_dark_theme_title": "Defolt sistem mövzusu",
"system_dark_theme_description": "Sistem parametrlərindən asılı olaraq açıq və ya qaranlıq mövzudan istifadə edin",
"dangerous_settings": "Təhlükəli Parametrlər"
"reset_config_description": "API və Super İstifadəçi Açarlarını sıfırlayın."
},
"ssh": {
"title": "SSH açarları",

View file

@ -51,7 +51,7 @@
"connect_to_server_provider": "Аўтарызавацца ў ",
"connect_to_server_provider_text": "З дапамогай API токена праграма SelfPrivacy зможа ад вашага імя замовіць і наладзіць сервер",
"steps": {
"nixos_installation": "Ўстаноўка NixOS",
"nixos_installation": "Ўсталёўка NixOS",
"hosting": "Хостынг",
"server_type": "Тып сервера",
"dns_provider": "DNS правайдэр",
@ -59,7 +59,7 @@
"domain": "Дамен",
"master_account": "Майстар акаўнт",
"server": "Сервер",
"dns_setup": "Устаноўка DNS",
"dns_setup": "Усталёўка DNS",
"server_reboot": "Перазагрузка сервера",
"final_checks": "Фінальныя праверкі"
},
@ -100,7 +100,7 @@
"modal_confirmation_dns_invalid": "Зваротны DNS паказвае на іншы дамен",
"modal_confirmation_ip_invalid": "IP не супадае з паказаным у DNS запісу",
"fallback_select_provider_console": "Доступ да кансолі хостынгу.",
"provider_connected_description": "Сувязь устаноўлена. Увядзіце свой токен з доступам да {}:",
"provider_connected_description": "Сувязь наладжана. Увядзіце свой токен з доступам да {}:",
"choose_server": "Выберыце сервер",
"no_servers": "На вашым акаўнце няма даступных сэрвэраў.",
"modal_confirmation_description": "Падлучэнне да няправільнага сервера можа прывесці да дэструктыўных наступстваў.",
@ -114,7 +114,7 @@
"authorize_new_device": "Аўтарызаваць новую прыладу",
"access_granted_on": "Доступ выдадзены {}",
"tip": "Націсніце на прыладу, каб адклікаць доступ.",
"description": "Гэтыя прылады маюць поўны доступ да кіравання серверам праз прыкладанне SelfPrivacy."
"description": "Гэтыя прылады маюць поўны доступ да кіравання серверам праз прыладу SelfPrivacy."
},
"add_new_device_screen": {
"description": "Увядзіце гэты ключ на новай прыладзе:",
@ -127,7 +127,7 @@
"revoke_device_alert": {
"header": "Адклікаць доступ?",
"yes": "Адклікаць",
"no": "Адменіць",
"no": "Адхіліць",
"description": "Прылада {} больш не зможа кіраваць серверам."
}
},
@ -143,7 +143,7 @@
"later": "Прапусціць і наладзіць потым",
"no_data": "Няма дадзеных",
"services": "Сэрвісы",
"users": "Ужыткоўнікі",
"users": "Карыстальнікі",
"more": "Дадаткова",
"got_it": "Зразумеў",
"settings": "Налады",
@ -234,7 +234,7 @@
},
"more_page": {
"configuration_wizard": "Майстар наладкі",
"onboarding": "Прівітанне",
"onboarding": "Прывітанне",
"create_ssh_key": "SSH ключы адміністратара"
},
"about_application_page": {
@ -244,16 +244,16 @@
"privacy_policy": "Палітыка прыватнасці"
},
"application_settings": {
"reset_config_description": "Скінуць API ключы i суперкарыстальніка.",
"delete_server_description": "Дзеянне прывядзе да выдалення сервера. Пасля гэтага ён будзе недаступны.",
"title": "Налады праграмы",
"system_theme_mode_title": "Сістэмная тэма па-змаўчанні",
"system_theme_mode_description": "Выкарыстоўвайце светлую ці цёмную тэмы ў залежнасці ад сістэмных налад",
"dark_theme_title": "Цёмная тэма",
"dark_theme_description": "Змяніць каляровую тэму",
"change_application_theme": "Змяніць каляровую тэму",
"language": "Мова",
"click_to_change_locale": "Націсніце, каб адчыніць меню выбару мовы",
"dangerous_settings": "Небяспечныя налады",
"reset_config_title": "Скід налад",
"delete_server_title": "Выдаліць сервер",
"system_dark_theme_title": "Сістэмная тэма па-змаўчанні",
"system_dark_theme_description": "Выкарыстоўвайце светлую ці цёмную тэмы ў залежнасці ад сістэмных налад",
"dangerous_settings": "Небяспечныя наладкі"
"reset_config_description": "Скінуць API ключы i суперкарыстальніка."
},
"ssh": {
"root_subtitle": "Уладальнікі паказаных тут ключоў атрымліваюць поўны доступ да дадзеных і налад сервера. Дадавайце выключна свае ключы.",

View file

@ -54,15 +54,13 @@
},
"application_settings": {
"title": "Nastavení aplikace",
"system_theme_mode_title": "Výchozí téma systému",
"system_theme_mode_description": "Použití světlého nebo tmavého motivu v závislosti na nastavení systému",
"dark_theme_title": "Tmavé téma",
"change_application_theme": "Přepnutí tématu aplikace",
"dangerous_settings": "Nebezpečná nastavení",
"reset_config_title": "Obnovení konfigurace aplikace",
"reset_config_description": "Obnovení klíčů API a uživatele root.",
"delete_server_title": "Odstranit server",
"dark_theme_description": "Přepnutí tématu aplikace",
"delete_server_description": "Tím odstraníte svůj server. Nebude již přístupný.",
"system_dark_theme_title": "Výchozí téma systému",
"system_dark_theme_description": "Použití světlého nebo tmavého motivu v závislosti na nastavení systému",
"dangerous_settings": "Nebezpečná nastavení"
"reset_config_description": "Obnovení klíčů API a uživatele root."
},
"ssh": {
"title": "Klíče SSH",

View file

@ -57,15 +57,13 @@
},
"application_settings": {
"title": "Anwendungseinstellungen",
"system_theme_mode_title": "Standard-Systemthema",
"system_theme_mode_description": "Verwenden Sie je nach Systemeinstellungen ein helles oder dunkles Thema",
"dark_theme_title": "Dunkles Thema",
"dark_theme_description": "Ihr Anwendungsdesign wechseln",
"change_application_theme": "Ihr Anwendungsdesign wechseln",
"dangerous_settings": "Gefährliche Einstellungen",
"reset_config_title": "Anwendungseinstellungen zurücksetzen",
"reset_config_description": "API Sclüssel und root Benutzer zurücksetzen.",
"delete_server_title": "Server löschen",
"delete_server_description": "Das wird Ihren Server löschen. Es wird nicht mehr zugänglich sein.",
"system_dark_theme_title": "Standard-Systemthema",
"system_dark_theme_description": "Verwenden Sie je nach Systemeinstellungen ein helles oder dunkles Thema",
"dangerous_settings": "Gefährliche Einstellungen"
"reset_config_description": "API Sclüssel und root Benutzer zurücksetzen."
},
"ssh": {
"title": "SSH Schlüssel",

View file

@ -47,7 +47,29 @@
"console_page": {
"title": "Console",
"waiting": "Waiting for initialization…",
"copy": "Copy"
"copy": "Copy",
"copy_raw": "Raw response",
"history_empty": "No data yet",
"error": "Error",
"log": "Log",
"rest_api_request": "Rest API Request",
"rest_api_response": "Rest API Response",
"graphql_request": "GraphQL Request",
"graphql_response": "GraphQL Response",
"logged_at": "Logged at",
"data": "Data",
"errors": "Errors",
"error_path": "Path",
"error_locations": "Locations",
"error_extensions": "Extensions",
"request_data": "Request data",
"headers": "Headers",
"response_data": "Response data",
"context": "Context",
"operation": "Operation",
"operation_type": "Operation type",
"operation_name": "Operation name",
"variables": "Variables"
},
"about_application_page": {
"title": "About & support",
@ -75,10 +97,12 @@
},
"application_settings": {
"title": "Application settings",
"system_dark_theme_title": "System default theme",
"system_dark_theme_description": "Use light or dark theme depending on system settings",
"system_theme_mode_title": "System default theme",
"system_theme_mode_description": "Use light or dark theme depending on system settings",
"dark_theme_title": "Dark theme",
"dark_theme_description": "Switch your application theme",
"change_application_theme": "Switch your application theme",
"language": "Language",
"click_to_change_locale": "Click to open language list",
"dangerous_settings": "Dangerous settings",
"reset_config_title": "Reset application config",
"reset_config_description": "Resets API keys and root user."
@ -565,6 +589,8 @@
"upgrade_success": "Server upgrade started",
"upgrade_failed": "Failed to upgrade server",
"upgrade_server": "Upgrade server",
"collect_nix_garbage": "Collect system garbage",
"collect_nix_garbage_failed": "Failed to collect system garbage",
"reboot_server": "Reboot server",
"create_ssh_key": "Create SSH key for {}",
"delete_ssh_key": "Delete SSH key for {}",
@ -606,5 +632,16 @@
"reset_onboarding": "Reset onboarding switch",
"reset_onboarding_description": "Reset onboarding switch to show onboarding screen again",
"cubit_statuses": "Cubit loading statuses"
},
"countries": {
"germany": "Germany",
"netherlands": "Netherlands",
"singapore": "Singapore",
"united_kingdom": "United Kingdom",
"canada": "Canada",
"india": "India",
"australia": "Australia",
"united_states": "United States",
"finland": "Finland"
}
}
}

View file

@ -39,16 +39,14 @@
"test": "es-test",
"locale": "es",
"application_settings": {
"reset_config_title": "Restablecer la configuración de la aplicación",
"dark_theme_description": "Cambia el tema de tu aplicación",
"reset_config_description": "Restablecer claves API y usuario root.",
"delete_server_title": "Eliminar servidor",
"delete_server_description": "Esto elimina su servidor. Ya no será accesible.",
"title": "Ajustes de la aplicación",
"system_theme_mode_title": "Tema del sistema",
"system_theme_mode_description": "Utiliza un tema claro u oscuro de la configuración del sistema",
"dark_theme_title": "Tema oscuro",
"system_dark_theme_title": "Tema del sistema",
"system_dark_theme_description": "Utiliza un tema claro u oscuro de la configuración del sistema",
"dangerous_settings": "Configuraciones peligrosas"
"change_application_theme": "Cambia el tema de tu aplicación",
"dangerous_settings": "Configuraciones peligrosas",
"reset_config_title": "Restablecer la configuración de la aplicación",
"reset_config_description": "Restablecer claves API y usuario root."
},
"ssh": {
"delete_confirm_question": "¿Está seguro de que desea eliminar la clave SSH?",

View file

@ -1,15 +1,13 @@
{
"application_settings": {
"system_dark_theme_description": "Kasutage valgus- või tumeteemat sõltuvalt süsteemi seadetest",
"delete_server_description": "See eemaldab teie serveri. Seda ei saa enam juurde pääseda.",
"title": "Rakenduse seaded",
"system_dark_theme_title": "Süsteemi vaiketeema",
"system_theme_mode_title": "Süsteemi vaiketeema",
"system_theme_mode_description": "Kasutage valgus- või tumeteemat sõltuvalt süsteemi seadetest",
"dark_theme_title": "Tume teema",
"dark_theme_description": "Vaheta oma rakenduse teemat",
"change_application_theme": "Vaheta oma rakenduse teemat",
"dangerous_settings": "Ohtlikud seaded",
"reset_config_title": "Lähtesta rakenduse konfiguratsioon",
"reset_config_description": "Lähtestab API võtmed ja juurkasutaja.",
"delete_server_title": "Kustuta server"
"reset_config_description": "Lähtestab API võtmed ja juurkasutaja."
},
"server": {
"reboot_after_upgrade": "Taaskäivita pärast värskendust",

View file

@ -56,15 +56,13 @@
},
"application_settings": {
"title": "Paramètres de l'application",
"dark_theme_description": "Changer le thème de l'application",
"reset_config_title": "Réinitialiser la configuration de l'application",
"delete_server_title": "Supprimer le serveur",
"delete_server_description": "Cela va supprimer votre serveur. Celui-ci ne sera plus accessible.",
"system_theme_mode_title": "Thème par défaut du système",
"system_theme_mode_description": "Affichage de jour ou de nuit en fonction du paramétrage système",
"dark_theme_title": "Thème sombre",
"reset_config_description": "Réinitialiser les clés API et l'utilisateur root.",
"system_dark_theme_title": "Thème par défaut du système",
"system_dark_theme_description": "Affichage de jour ou de nuit en fonction du paramétrage système",
"dangerous_settings": "Paramètres dangereux"
"change_application_theme": "Changer le thème de l'application",
"dangerous_settings": "Paramètres dangereux",
"reset_config_title": "Réinitialiser la configuration de l'application",
"reset_config_description": "Réinitialiser les clés API et l'utilisateur root."
},
"ssh": {
"title": "Clés SSH",

View file

@ -81,15 +81,13 @@
},
"application_settings": {
"title": "הגדרות יישום",
"system_dark_theme_title": "ערכת העיצוב כברירת המחדל של המערכת",
"system_dark_theme_description": "להשתמש בערכות עיצוב בהירה או כהה בהתאם להגדרות המערכת שלך",
"system_theme_mode_title": "ערכת העיצוב כברירת המחדל של המערכת",
"system_theme_mode_description": "להשתמש בערכות עיצוב בהירה או כהה בהתאם להגדרות המערכת שלך",
"dark_theme_title": "ערכת עיצוב כהה",
"dark_theme_description": "החלפת ערכת העיצוב של המערכת שלך",
"change_application_theme": "החלפת ערכת העיצוב של המערכת שלך",
"dangerous_settings": "הגדרות מסוכנות",
"reset_config_title": "איפוס הגדרות היישומון",
"reset_config_description": "איפוס מפתחות ה־API ומשתמש העל.",
"delete_server_title": "מחיקת שרת",
"delete_server_description": "מסיר את השרת שלך. הוא לא יהיה זמין עוד."
"reset_config_description": "איפוס מפתחות ה־API ומשתמש העל."
},
"backup": {
"create_new_select_heading": "לבחור מה לגבות",

View file

@ -91,16 +91,14 @@
"bug_report_subtitle": "Спамға байланысты есептік жазбаны қолмен растау қажет. Тіркелгіні белсендіру үшін Қолдау чатында бізге хабарласыңыз."
},
"application_settings": {
"title": "Қосымша параметрлері",
"system_theme_mode_title": "Системалық қараңғы тақырып",
"system_theme_mode_description": "Системалық қараңғы тақырып сипаттамасы",
"dark_theme_title": "Қараңғы тақырып",
"change_application_theme": "Қараңғы тақырып сипаттамасы",
"dangerous_settings": "Қауіпті параметрлер",
"reset_config_title": "Конфигурацияны қалпына келтіру",
"title": "Қосымша параметрлері",
"system_dark_theme_title": "Системалық қараңғы тақырып",
"system_dark_theme_description": "Системалық қараңғы тақырып сипаттамасы",
"dark_theme_title": "Қараңғы тақырып",
"dark_theme_description": "Қараңғы тақырып сипаттамасы",
"delete_server_title": "Серверді жою",
"reset_config_description": "Конфигурацияны қалпына келтіру сипаттамасы.",
"delete_server_description": "Серверді жою сипаттамасы."
"reset_config_description": "Конфигурацияны қалпына келтіру сипаттамасы."
},
"resource_chart": {
"month": "Ай",

View file

@ -52,16 +52,14 @@
"privacy_policy": "Privātuma politika"
},
"application_settings": {
"system_dark_theme_title": "Sistēmas noklusējuma dizains",
"dark_theme_title": "Tumšs dizains",
"title": "Aplikācijas iestatījumi",
"system_dark_theme_description": "Izmantojiet gaišu vai tumšu dizainu atkarībā no sistēmas iestatījumiem",
"dark_theme_description": "Lietojumprogrammas dizaina pārslēgšana",
"system_theme_mode_title": "Sistēmas noklusējuma dizains",
"system_theme_mode_description": "Izmantojiet gaišu vai tumšu dizainu atkarībā no sistēmas iestatījumiem",
"dark_theme_title": "Tumšs dizains",
"change_application_theme": "Lietojumprogrammas dizaina pārslēgšana",
"dangerous_settings": "Bīstamie iestatījumi",
"reset_config_title": "Atiestatīt lietojumprogrammas konfigurāciju",
"reset_config_description": "Atiestatīt API atslēgas un saknes lietotāju.",
"delete_server_title": "Izdzēst serveri",
"delete_server_description": "Šis izdzēš jūsu serveri. Tas vairs nebūs pieejams."
"reset_config_description": "Atiestatīt API atslēgas un saknes lietotāju."
},
"locale": "lv",
"ssh": {

View file

@ -56,15 +56,13 @@
},
"application_settings": {
"title": "Ustawienia aplikacji",
"system_theme_mode_description": "Użyj jasnego lub ciemnego motywu w zależności od ustawień systemu",
"system_theme_mode_title": "Domyślny motyw systemowy",
"dark_theme_title": "Ciemny motyw aplikacji",
"dark_theme_description": "Zmień kolor motywu aplikacji",
"change_application_theme": "Zmień kolor motywu aplikacji",
"dangerous_settings": "Niebezpieczne ustawienia",
"reset_config_title": "Resetowanie",
"reset_config_description": "Zresetuj klucze API i użytkownika root.",
"delete_server_title": "Usuń serwer",
"delete_server_description": "Ta czynność usunie serwer. Po tym będzie niedostępny.",
"system_dark_theme_description": "Użyj jasnego lub ciemnego motywu w zależności od ustawień systemu",
"system_dark_theme_title": "Domyślny motyw systemowy",
"dangerous_settings": "Niebezpieczne ustawienia"
"reset_config_description": "Zresetuj klucze API i użytkownika root."
},
"ssh": {
"title": "klucze SSH",

View file

@ -75,15 +75,15 @@
},
"application_settings": {
"title": "Настройки приложения",
"system_theme_mode_title": "Системная тема",
"system_theme_mode_description": "Будет использована светлая или тёмная тема в зависимости от системных настроек",
"dark_theme_title": "Тёмная тема",
"dark_theme_description": "Сменить цветовую тему",
"change_application_theme": "Сменить цветовую тему",
"language": "Язык",
"click_to_change_locale": "Нажмите, чтобы открыть список языков",
"dangerous_settings": "Опасные настройки",
"reset_config_title": "Сброс настроек",
"reset_config_description": "Сбросить API ключи и root пользователя.",
"delete_server_title": "Удалить сервер",
"delete_server_description": "Действие приведёт к удалению сервера. После этого он будет недоступен.",
"system_dark_theme_title": "Системная тема",
"system_dark_theme_description": "Будет использована светлая или тёмная тема в зависимости от системных настроек",
"dangerous_settings": "Опасные настройки"
"reset_config_description": "Сбросить API ключи и root пользователя."
},
"ssh": {
"title": "SSH ключи",

View file

@ -103,15 +103,13 @@
},
"application_settings": {
"title": "Nastavenia aplikácie",
"system_theme_mode_description": "Použitie svetlej alebo tmavej témy v závislosti od nastavení systému",
"system_theme_mode_title": "Systémová predvolená téma",
"dark_theme_title": "Temná téma",
"dark_theme_description": "Zmeniť tému aplikácie",
"change_application_theme": "Zmeniť tému aplikácie",
"dangerous_settings": "Nebezpečné nastavenia",
"reset_config_title": "Resetovať nastavenia aplikácie",
"reset_config_description": "Resetovať kľúče API a užívateľa root.",
"delete_server_title": "Zmazať server",
"delete_server_description": "Tým sa odstráni váš server. Už nebude prístupným.",
"system_dark_theme_description": "Použitie svetlej alebo tmavej témy v závislosti od nastavení systému",
"system_dark_theme_title": "Systémová predvolená téma",
"dangerous_settings": "Nebezpečné nastavenia"
"reset_config_description": "Resetovať kľúče API a užívateľa root."
},
"ssh": {
"title": "Kľúče SSH",

View file

@ -53,15 +53,13 @@
"application_version_text": "Različica aplikacije"
},
"application_settings": {
"dark_theme_title": "Temna tema",
"title": "Nastavitve aplikacije",
"system_dark_theme_title": "Privzeta tema sistema",
"system_dark_theme_description": "Uporaba svetle ali temne teme glede na sistemske nastavitve",
"dark_theme_description": "Spreminjanje barvne teme",
"system_theme_mode_title": "Privzeta tema sistema",
"system_theme_mode_description": "Uporaba svetle ali temne teme glede na sistemske nastavitve",
"dark_theme_title": "Temna tema",
"change_application_theme": "Spreminjanje barvne teme",
"dangerous_settings": "Nevarne nastavitve",
"reset_config_title": "Ponastavitev konfiguracije aplikacije",
"delete_server_title": "Brisanje strežnika",
"delete_server_description": "To dejanje povzroči izbris strežnika. Nato bo nedosegljiv."
"reset_config_title": "Ponastavitev konfiguracije aplikacije"
},
"onboarding": {
"page1_title": "Digitalna neodvisnost je na voljo vsem",

View file

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

View file

@ -41,15 +41,14 @@
"locale": "ua",
"application_settings": {
"title": "Налаштування додатка",
"reset_config_title": "Скинути налаштування",
"system_theme_mode_title": "Системна тема за замовчуванням",
"system_theme_mode_description": "Використовуйте світлу або темну теми залежно від системних налаштувань",
"dark_theme_title": "Темна тема",
"dark_theme_description": "Змінити тему додатка",
"reset_config_description": "Скинути API ключі та root користувача.",
"delete_server_title": "Видалити сервер",
"delete_server_description": "Це видалить ваш сервер. Він більше не буде доступний.",
"system_dark_theme_title": "Системна тема за замовчуванням",
"system_dark_theme_description": "Використовуйте світлу або темну теми залежно від системних налаштувань",
"dangerous_settings": "Небезпечні налаштування"
"change_application_theme": "Змінити тему додатка",
"language": "Мова",
"dangerous_settings": "Небезпечні налаштування",
"reset_config_title": "Скинути налаштування",
"reset_config_description": "Скинути API ключі та root користувача."
},
"ssh": {
"delete_confirm_question": "Ви впевнені, що хочете видалити SSH-ключ?",

View file

@ -476,15 +476,13 @@
},
"application_settings": {
"title": "应用设置",
"system_dark_theme_title": "系统默认主题",
"system_theme_mode_title": "系统默认主题",
"system_theme_mode_description": "根据系统设置自动使用明亮或暗色主题",
"dark_theme_title": "暗色主题",
"system_dark_theme_description": "根据系统设置自动使用明亮或暗色主题",
"dark_theme_description": "切换应用主题",
"change_application_theme": "切换应用主题",
"dangerous_settings": "危险设置",
"reset_config_title": "重置应用配置",
"delete_server_title": "删除服务器",
"delete_server_description": "这将移除您的服务器。它将不再可以访问。",
"reset_config_description": "重置API密钥和root用户。"
"delete_server_title": "删除服务器"
},
"ssh": {
"title": "SSH密钥",

View file

@ -1,6 +1,6 @@
app-id: org.selfprivacy.app
runtime: org.freedesktop.Platform
runtime-version: '22.08'
runtime-version: '23.08'
sdk: org.freedesktop.Sdk
command: selfprivacy
finish-args:
@ -11,6 +11,7 @@ finish-args:
- "--share=network"
- "--own-name=org.selfprivacy.app"
- "--device=dri"
- "--talk-name=org.freedesktop.secrets"
modules:
- name: selfprivacy
buildsystem: simple
@ -35,7 +36,7 @@ modules:
sources:
- type: git
url: https://gitlab.gnome.org/GNOME/libsecret.git
tag: 0.20.5
tag: 0.21.4
- name: libjsoncpp
buildsystem: meson
config-opts:

View file

@ -0,0 +1,175 @@
import 'package:flutter/material.dart';
import 'package:material_color_utilities/material_color_utilities.dart'
as color_utils;
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/config/localization.dart';
import 'package:selfprivacy/config/preferences_repository/preferences_repository.dart';
/// A class that many Widgets can interact with to read current app
/// configuration, update it, or listen to its changes.
///
/// AppController uses repo to change persistent data.
class AppController with ChangeNotifier {
AppController(this._repo);
/// repo encapsulates retrieval and storage of preferences
final PreferencesRepository _repo;
/// TODO: to be removed or changed
late final ApiConfigModel _apiConfigModel = getIt.get<ApiConfigModel>();
bool _loaded = false;
bool get loaded => _loaded;
// localization
late Locale _locale;
Locale get locale => _locale;
late List<Locale> _supportedLocales;
List<Locale> get supportedLocales => _supportedLocales;
// theme
late ThemeData _lightTheme;
ThemeData get lightTheme => _lightTheme;
late ThemeData _darkTheme;
ThemeData get darkTheme => _darkTheme;
late color_utils.CorePalette _corePalette;
color_utils.CorePalette get corePalette => _corePalette;
late bool _systemThemeModeActive;
bool get systemThemeModeActive => _systemThemeModeActive;
late bool _darkThemeModeActive;
bool get darkThemeModeActive => _darkThemeModeActive;
ThemeMode get themeMode => systemThemeModeActive
? ThemeMode.system
: darkThemeModeActive
? ThemeMode.dark
: ThemeMode.light;
late bool _shouldShowOnboarding;
bool get shouldShowOnboarding => _shouldShowOnboarding;
Future<void> init({
// required final AppPreferencesRepository repo,
required final ThemeData lightThemeData,
required final ThemeData darkThemeData,
required final color_utils.CorePalette colorPalette,
}) async {
// _repo = repo;
await Future.wait(<Future>[
// load locale
() async {
_supportedLocales = [
Localization.systemLocale,
...await _repo.getSupportedLocales(),
];
_locale = await _repo.getActiveLocale();
if (_locale != Localization.systemLocale) {
// preset value to other state holders
await _apiConfigModel.setLocaleCode(_locale.languageCode);
await _repo.setDelegateLocale(_locale);
}
}(),
// load theme mode && initialize theme
() async {
_lightTheme = lightThemeData;
_darkTheme = darkThemeData;
_corePalette = colorPalette;
_darkThemeModeActive = await _repo.getDarkThemeModeFlag();
_systemThemeModeActive = await _repo.getSystemThemeModeFlag();
}(),
// load onboarding flag
() async {
_shouldShowOnboarding = await _repo.getShouldShowOnboarding();
}(),
]);
_loaded = true;
// Important! Inform listeners a change has occurred.
notifyListeners();
}
// updateRepoReference
Future<void> setShouldShowOnboarding(final bool newValue) async {
// Do not perform any work if new and old flag values are identical
if (newValue == shouldShowOnboarding) {
return;
}
// Store the flag in memory
_shouldShowOnboarding = newValue;
notifyListeners();
// Persist the change
await _repo.setShouldShowOnboarding(newValue);
}
Future<void> setSystemThemeModeFlag(final bool newValue) async {
// Do not perform any work if new and old ThemeMode are identical
if (systemThemeModeActive == newValue) {
return;
}
// Store the new ThemeMode in memory
_systemThemeModeActive = newValue;
// Inform listeners a change has occurred.
notifyListeners();
// Persist the change
await _repo.setSystemModeFlag(newValue);
}
Future<void> setDarkThemeModeFlag(final bool newValue) async {
// Do not perform any work if new and old ThemeMode are identical
if (darkThemeModeActive == newValue) {
return;
}
// Store the new ThemeMode in memory
_darkThemeModeActive = newValue;
// Inform listeners a change has occurred.
notifyListeners();
// Persist the change
await _repo.setDarkThemeModeFlag(newValue);
}
Future<void> setLocale(final Locale newLocale) async {
// Do not perform any work if new and old Locales are identical
if (newLocale == _locale) {
return;
}
// Store the new Locale in memory
_locale = newLocale;
if (newLocale == Localization.systemLocale) {
return resetLocale();
}
/// update locale delegate, which in turn should update deps
await _repo.setDelegateLocale(newLocale);
// Persist the change
await _repo.setActiveLocale(newLocale);
// Update other locale holders
await _apiConfigModel.setLocaleCode(newLocale.languageCode);
}
Future<void> resetLocale() async {
/// update locale delegate, which in turn should update deps
await _repo.resetDelegateLocale();
// Persist the change
await _repo.resetActiveLocale();
// Update other locale holders
await _apiConfigModel.resetLocaleCode();
}
}

View file

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

View file

@ -1,14 +1,13 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:selfprivacy/logic/bloc/backups/backups_bloc.dart';
import 'package:selfprivacy/logic/bloc/connection_status/connection_status_bloc.dart';
import 'package:selfprivacy/logic/bloc/connection_status_bloc.dart';
import 'package:selfprivacy/logic/bloc/devices/devices_bloc.dart';
import 'package:selfprivacy/logic/bloc/recovery_key/recovery_key_bloc.dart';
import 'package:selfprivacy/logic/bloc/server_jobs/server_jobs_bloc.dart';
import 'package:selfprivacy/logic/bloc/services/services_bloc.dart';
import 'package:selfprivacy/logic/bloc/users/users_bloc.dart';
import 'package:selfprivacy/logic/bloc/volumes/volumes_bloc.dart';
import 'package:selfprivacy/logic/cubit/app_settings/app_settings_cubit.dart';
import 'package:selfprivacy/logic/cubit/client_jobs/client_jobs_cubit.dart';
import 'package:selfprivacy/logic/cubit/dns_records/dns_records_cubit.dart';
import 'package:selfprivacy/logic/cubit/server_detailed_info/server_detailed_info_cubit.dart';
@ -56,58 +55,46 @@ class BlocAndProviderConfigState extends State<BlocAndProviderConfig> {
}
@override
Widget build(final BuildContext context) {
const isDark = false;
const isAutoDark = true;
return MultiProvider(
providers: [
BlocProvider(
create: (final _) => AppSettingsCubit(
isDarkModeOn: isDark,
isAutoDarkModeOn: isAutoDark,
isOnboardingShowing: true,
)..load(),
),
BlocProvider(
create: (final _) => supportSystemCubit,
),
BlocProvider(
create: (final _) => serverInstallationCubit,
lazy: false,
),
BlocProvider(
create: (final _) => usersBloc,
lazy: false,
),
BlocProvider(
create: (final _) => servicesBloc,
),
BlocProvider(
create: (final _) => backupsBloc,
),
BlocProvider(
create: (final _) => dnsRecordsCubit,
),
BlocProvider(
create: (final _) => recoveryKeyBloc,
),
BlocProvider(
create: (final _) => devicesBloc,
),
BlocProvider(
create: (final _) => serverJobsBloc,
),
BlocProvider(create: (final _) => connectionStatusBloc),
BlocProvider(
create: (final _) => serverDetailsCubit,
),
BlocProvider(create: (final _) => volumesBloc),
BlocProvider(
create: (final _) => JobsCubit(),
),
],
child: widget.child,
);
}
Widget build(final BuildContext context) => MultiProvider(
providers: [
BlocProvider(
create: (final _) => supportSystemCubit,
),
BlocProvider(
create: (final _) => serverInstallationCubit,
lazy: false,
),
BlocProvider(
create: (final _) => usersBloc,
lazy: false,
),
BlocProvider(
create: (final _) => servicesBloc,
),
BlocProvider(
create: (final _) => backupsBloc,
),
BlocProvider(
create: (final _) => dnsRecordsCubit,
),
BlocProvider(
create: (final _) => recoveryKeyBloc,
),
BlocProvider(
create: (final _) => devicesBloc,
),
BlocProvider(
create: (final _) => serverJobsBloc,
),
BlocProvider(create: (final _) => connectionStatusBloc),
BlocProvider(
create: (final _) => serverDetailsCubit,
),
BlocProvider(create: (final _) => volumesBloc),
BlocProvider(
create: (final _) => JobsCubit(),
),
],
child: widget.child,
);
}

View file

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

View file

@ -1,6 +1,6 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:flutter/services.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:selfprivacy/logic/models/hive/backblaze_bucket.dart';
@ -36,121 +36,136 @@ class HiveConfig {
await Hive.openBox(BNames.appSettingsBox);
final HiveAesCipher cipher = HiveAesCipher(
await getEncryptedKey(BNames.serverInstallationEncryptionKey),
);
try {
final HiveAesCipher cipher = HiveAesCipher(
await getEncryptedKey(BNames.serverInstallationEncryptionKey),
);
await Hive.openBox(BNames.serverInstallationBox, encryptionCipher: cipher);
await Hive.openBox(BNames.resourcesBox, encryptionCipher: cipher);
await Hive.openBox(BNames.wizardDataBox, encryptionCipher: cipher);
await Hive.openBox(BNames.serverInstallationBox, encryptionCipher: cipher);
await Hive.openBox(BNames.resourcesBox, encryptionCipher: cipher);
await Hive.openBox(BNames.wizardDataBox, encryptionCipher: cipher);
final Box resourcesBox = Hive.box(BNames.resourcesBox);
if (resourcesBox.isEmpty) {
final Box serverInstallationBox = Hive.box(BNames.serverInstallationBox);
final Box resourcesBox = Hive.box(BNames.resourcesBox);
if (resourcesBox.isEmpty) {
final Box serverInstallationBox = Hive.box(BNames.serverInstallationBox);
final String? serverProviderKey =
serverInstallationBox.get(BNames.hetznerKey);
final String? serverLocation =
serverInstallationBox.get(BNames.serverLocation);
final String? dnsProviderKey =
serverInstallationBox.get(BNames.cloudFlareKey);
final BackupsCredential? backblazeCredential =
serverInstallationBox.get(BNames.backblazeCredential);
final ServerDomain? serverDomain =
serverInstallationBox.get(BNames.serverDomain);
final ServerHostingDetails? serverDetails =
serverInstallationBox.get(BNames.serverDetails);
final BackblazeBucket? backblazeBucket =
serverInstallationBox.get(BNames.backblazeBucket);
final String? serverType =
serverInstallationBox.get(BNames.serverTypeIdentifier);
final ServerProviderType? serverProvider =
serverInstallationBox.get(BNames.serverProvider);
final DnsProviderType? dnsProvider =
serverInstallationBox.get(BNames.dnsProvider);
final String? serverProviderKey =
serverInstallationBox.get(BNames.hetznerKey);
final String? serverLocation =
serverInstallationBox.get(BNames.serverLocation);
final String? dnsProviderKey =
serverInstallationBox.get(BNames.cloudFlareKey);
final BackupsCredential? backblazeCredential =
serverInstallationBox.get(BNames.backblazeCredential);
final ServerDomain? serverDomain =
serverInstallationBox.get(BNames.serverDomain);
final ServerHostingDetails? serverDetails =
serverInstallationBox.get(BNames.serverDetails);
final BackblazeBucket? backblazeBucket =
serverInstallationBox.get(BNames.backblazeBucket);
final String? serverType =
serverInstallationBox.get(BNames.serverTypeIdentifier);
final ServerProviderType? serverProvider =
serverInstallationBox.get(BNames.serverProvider);
final DnsProviderType? dnsProvider =
serverInstallationBox.get(BNames.dnsProvider);
if (serverProviderKey != null &&
(serverProvider != null ||
(serverDetails != null &&
serverDetails.provider != ServerProviderType.unknown))) {
final ServerProviderCredential serverProviderCredential =
ServerProviderCredential(
tokenId: null,
token: serverProviderKey,
provider: serverProvider ?? serverDetails!.provider,
associatedServerIds: serverDetails != null ? [serverDetails.id] : [],
);
if (serverProviderKey != null &&
(serverProvider != null ||
(serverDetails != null &&
serverDetails.provider != ServerProviderType.unknown))) {
final ServerProviderCredential serverProviderCredential =
ServerProviderCredential(
tokenId: null,
token: serverProviderKey,
provider: serverProvider ?? serverDetails!.provider,
associatedServerIds: serverDetails != null ? [serverDetails.id] : [],
);
await resourcesBox
.put(BNames.serverProviderTokens, [serverProviderCredential]);
}
await resourcesBox
.put(BNames.serverProviderTokens, [serverProviderCredential]);
}
if (dnsProviderKey != null &&
(dnsProvider != null ||
(serverDomain != null &&
serverDomain.provider != DnsProviderType.unknown))) {
final DnsProviderCredential dnsProviderCredential =
DnsProviderCredential(
tokenId: null,
token: dnsProviderKey,
provider: dnsProvider ?? serverDomain!.provider,
associatedDomainNames:
serverDomain != null ? [serverDomain.domainName] : [],
);
if (dnsProviderKey != null &&
(dnsProvider != null ||
(serverDomain != null &&
serverDomain.provider != DnsProviderType.unknown))) {
final DnsProviderCredential dnsProviderCredential =
DnsProviderCredential(
tokenId: null,
token: dnsProviderKey,
provider: dnsProvider ?? serverDomain!.provider,
associatedDomainNames:
serverDomain != null ? [serverDomain.domainName] : [],
);
await resourcesBox
.put(BNames.dnsProviderTokens, [dnsProviderCredential]);
}
await resourcesBox
.put(BNames.dnsProviderTokens, [dnsProviderCredential]);
}
if (backblazeCredential != null) {
await resourcesBox
.put(BNames.backupsProviderTokens, [backblazeCredential]);
}
if (backblazeCredential != null) {
await resourcesBox
.put(BNames.backupsProviderTokens, [backblazeCredential]);
}
if (backblazeBucket != null) {
await resourcesBox.put(BNames.backblazeBucket, backblazeBucket);
}
if (backblazeBucket != null) {
await resourcesBox.put(BNames.backblazeBucket, backblazeBucket);
}
if (serverDetails != null && serverDomain != null) {
await resourcesBox.put(BNames.servers, [
Server(
domain: serverDomain,
hostingDetails: serverDetails.copyWith(
serverLocation: serverLocation,
serverType: serverType,
if (serverDetails != null && serverDomain != null) {
await resourcesBox.put(BNames.servers, [
Server(
domain: serverDomain,
hostingDetails: serverDetails.copyWith(
serverLocation: serverLocation,
serverType: serverType,
),
),
),
]);
]);
}
}
} on PlatformException catch (e) {
print('HiveConfig: Error while opening boxes: $e');
rethrow;
}
}
static Future<Uint8List> getEncryptedKey(final String encKey) async {
const FlutterSecureStorage secureStorage = FlutterSecureStorage();
final bool hasEncryptionKey = await secureStorage.containsKey(key: encKey);
if (!hasEncryptionKey) {
final List<int> key = Hive.generateSecureKey();
await secureStorage.write(key: encKey, value: base64UrlEncode(key));
}
try {
final bool hasEncryptionKey =
await secureStorage.containsKey(key: encKey);
if (!hasEncryptionKey) {
final List<int> key = Hive.generateSecureKey();
await secureStorage.write(key: encKey, value: base64UrlEncode(key));
}
final String? string = await secureStorage.read(key: encKey);
return base64Url.decode(string!);
final String? string = await secureStorage.read(key: encKey);
return base64Url.decode(string!);
} on PlatformException catch (e) {
print('HiveConfig: Error while getting encryption key: $e');
rethrow;
}
}
}
/// Mappings for the different boxes and their keys
class BNames {
/// App settings box. Contains app settings like [isDarkModeOn], [isOnboardingShowing]
/// App settings box. Contains app settings like [darkThemeModeOn], [shouldShowOnboarding]
static String appSettingsBox = 'appSettings';
/// A boolean field of [appSettingsBox] box.
static String isDarkModeOn = 'isDarkModeOn';
static String darkThemeModeOn = 'isDarkModeOn';
/// A boolean field of [appSettingsBox] box.
static String isAutoDarkModeOn = 'isAutoDarkModeOn';
static String systemThemeModeOn = 'isAutoDarkModeOn';
/// A boolean field of [appSettingsBox] box.
static String isOnboardingShowing = 'isOnboardingShowing';
static String shouldShowOnboarding = 'isOnboardingShowing';
/// A string field
static String appLocale = 'appLocale';
/// Encryption key to decrypt [serverInstallationBox] box.
static String serverInstallationEncryptionKey = 'key';

View file

@ -3,40 +3,76 @@ import 'package:flutter/material.dart';
class Localization extends StatelessWidget {
const Localization({
required this.child,
super.key,
this.child,
});
final Widget? child;
/// value for resetting locale in settings to system default
static const systemLocale = Locale('system');
// when adding new locale, add corresponding native language name to mapper
// below
static const supportedLocales = [
Locale('ar'),
Locale('az'),
Locale('be'),
Locale('cs'),
Locale('de'),
Locale('en'),
Locale('es'),
Locale('et'),
Locale('fr'),
Locale('he'),
Locale('kk'),
Locale('lv'),
Locale('mk'),
Locale('pl'),
Locale('ru'),
Locale('sk'),
Locale('sl'),
Locale('th'),
Locale('uk'),
Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hans'),
];
// https://en.wikipedia.org/wiki/IETF_language_tag#List_of_common_primary_language_subtags
static final _languageNames = {
systemLocale: 'System default',
const Locale('ar'): 'العربية',
const Locale('az'): 'Azərbaycan',
const Locale('be'): 'беларуская',
const Locale('cs'): 'čeština',
const Locale('de'): 'Deutsch',
const Locale('en'): 'English',
const Locale('es'): 'español',
const Locale('et'): 'eesti',
const Locale('fr'): 'français',
const Locale('he'): 'עברית',
const Locale('kk'): 'Қазақша',
const Locale('lv'): 'latviešu',
const Locale('mk'): 'македонски јазик',
const Locale('pl'): 'polski',
const Locale('ru'): 'русский',
const Locale('sk'): 'slovenčina',
const Locale('sl'): 'slovenski',
const Locale('th'): 'ไทย',
const Locale('uk'): 'Українська',
const Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hans'): '中文',
};
static String getLanguageName(final Locale locale) =>
_languageNames[locale] ?? locale.languageCode;
final Widget child;
@override
Widget build(final BuildContext context) => EasyLocalization(
supportedLocales: const [
Locale('ar'),
Locale('az'),
Locale('be'),
Locale('cs'),
Locale('de'),
Locale('en'),
Locale('es'),
Locale('et'),
Locale('fr'),
Locale('he'),
Locale('kk'),
Locale('lv'),
Locale('mk'),
Locale('pl'),
Locale('ru'),
Locale('sk'),
Locale('sl'),
Locale('th'),
Locale('uk'),
Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hans'),
],
supportedLocales: supportedLocales,
path: 'assets/translations',
fallbackLocale: const Locale('en'),
useFallbackTranslations: true,
saveLocale: false,
useOnlyLangCode: false,
child: child!,
child: child,
);
}

View file

@ -0,0 +1,33 @@
/// abstraction for manipulation of stored app preferences
abstract class PreferencesDataSource {
/// should onboarding be shown
Future<bool> getOnboardingFlag();
/// should onboarding be shown
Future<void> setOnboardingFlag(final bool newValue);
// TODO: should probably deprecate the following, instead add the
// getThemeMode and setThemeMode methods, which store one value instead of
// flags.
/// should system theme mode be enabled
Future<bool?> getSystemThemeModeFlag();
/// should system theme mode be enabled
Future<void> setSystemThemeModeFlag(final bool newValue);
/// should dark theme be enabled
Future<bool?> getDarkThemeModeFlag();
/// should dark theme be enabled
Future<void> setDarkThemeModeFlag(final bool newValue);
/// locale, as set by user
///
///
/// when null, app takes system locale
Future<String?> getLocale();
/// locale, as set by user
Future<void> setLocale(final String? newLocale);
}

View file

@ -0,0 +1,40 @@
import 'package:hive/hive.dart';
import 'package:selfprivacy/config/hive_config.dart';
import 'package:selfprivacy/config/preferences_repository/datasources/preferences_datasource.dart';
/// app preferences data source hive implementation
class PreferencesHiveDataSource implements PreferencesDataSource {
final Box _appSettingsBox = Hive.box(BNames.appSettingsBox);
@override
Future<bool> getOnboardingFlag() async =>
_appSettingsBox.get(BNames.shouldShowOnboarding, defaultValue: true);
@override
Future<void> setOnboardingFlag(final bool newValue) async =>
_appSettingsBox.put(BNames.shouldShowOnboarding, newValue);
@override
Future<bool?> getSystemThemeModeFlag() async =>
_appSettingsBox.get(BNames.systemThemeModeOn);
@override
Future<void> setSystemThemeModeFlag(final bool newValue) async =>
_appSettingsBox.put(BNames.systemThemeModeOn, newValue);
@override
Future<bool?> getDarkThemeModeFlag() async =>
_appSettingsBox.get(BNames.darkThemeModeOn);
@override
Future<void> setDarkThemeModeFlag(final bool newValue) async =>
_appSettingsBox.put(BNames.darkThemeModeOn, newValue);
@override
Future<String?> getLocale() async => _appSettingsBox.get(BNames.appLocale);
@override
Future<void> setLocale(final String? newLocale) async => newLocale == null
? _appSettingsBox.delete(BNames.appLocale)
: _appSettingsBox.put(BNames.appLocale, newLocale);
}

View file

@ -0,0 +1,64 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:selfprivacy/config/preferences_repository/datasources/preferences_datasource.dart';
import 'package:selfprivacy/config/preferences_repository/preferences_repository.dart';
class _PreferencesRepositoryInjector extends InheritedWidget {
const _PreferencesRepositoryInjector({
required this.settingsRepository,
required super.child,
});
final PreferencesRepository settingsRepository;
@override
bool updateShouldNotify(
covariant final _PreferencesRepositoryInjector oldWidget,
) =>
oldWidget.settingsRepository != settingsRepository;
}
/// Creates and injects app preferences repository inside widget tree.
class InheritedPreferencesRepository extends StatefulWidget {
const InheritedPreferencesRepository({
required this.child,
required this.dataSource,
super.key,
});
final PreferencesDataSource dataSource;
final Widget child;
@override
State<InheritedPreferencesRepository> createState() =>
_InheritedPreferencesRepositoryState();
static PreferencesRepository? of(final BuildContext context) => context
.dependOnInheritedWidgetOfExactType<_PreferencesRepositoryInjector>()
?.settingsRepository;
}
class _InheritedPreferencesRepositoryState
extends State<InheritedPreferencesRepository> {
late PreferencesRepository repo;
@override
void didChangeDependencies() {
super.didChangeDependencies();
/// recreate repo each time dependencies change
repo = PreferencesRepository(
dataSource: widget.dataSource,
setDelegateLocale: EasyLocalization.of(context)!.setLocale,
resetDelegateLocale: EasyLocalization.of(context)!.resetLocale,
getDelegateLocale: () => EasyLocalization.of(context)!.locale,
getSupportedLocales: () => EasyLocalization.of(context)!.supportedLocales,
);
}
@override
Widget build(final BuildContext context) => _PreferencesRepositoryInjector(
settingsRepository: repo,
child: widget.child,
);
}

View file

@ -0,0 +1,73 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:selfprivacy/config/localization.dart';
import 'package:selfprivacy/config/preferences_repository/datasources/preferences_datasource.dart';
class PreferencesRepository {
const PreferencesRepository({
required this.dataSource,
required this.getSupportedLocales,
required this.getDelegateLocale,
required this.setDelegateLocale,
required this.resetDelegateLocale,
});
final PreferencesDataSource dataSource;
/// easy localizations don't expose type of localization provider,
/// so it needs to be this crutchy (I could've created one more class-wrapper,
/// containing needed functions, but perceive it as boilerplate, because we
/// don't need additional encapsulation level here)
final FutureOr<void> Function(Locale) setDelegateLocale;
final FutureOr<void> Function() resetDelegateLocale;
final FutureOr<List<Locale>> Function() getSupportedLocales;
final FutureOr<Locale> Function() getDelegateLocale;
Future<bool> getSystemThemeModeFlag() async =>
(await dataSource.getSystemThemeModeFlag()) ?? true;
Future<void> setSystemThemeModeFlag(final bool newValue) async =>
dataSource.setSystemThemeModeFlag(newValue);
Future<bool> getDarkThemeModeFlag() async =>
(await dataSource.getDarkThemeModeFlag()) ?? false;
Future<void> setDarkThemeModeFlag(final bool newValue) async =>
dataSource.setDarkThemeModeFlag(newValue);
Future<void> setSystemModeFlag(final bool newValue) async =>
dataSource.setSystemThemeModeFlag(newValue);
Future<List<Locale>> supportedLocales() async => getSupportedLocales();
Future<Locale> getActiveLocale() async {
Locale? chosenLocale;
final String? storedLocaleCode = await dataSource.getLocale();
if (storedLocaleCode != null) {
chosenLocale = Locale(storedLocaleCode);
}
// when it's null fallback on delegate locale
chosenLocale ??= Localization.systemLocale;
return chosenLocale;
}
Future<void> setActiveLocale(final Locale newLocale) async {
await dataSource.setLocale(newLocale.toString());
}
Future<void> resetActiveLocale() async {
await dataSource.setLocale(null);
}
/// true when we need to show onboarding
Future<bool> getShouldShowOnboarding() async =>
dataSource.getOnboardingFlag();
/// true when we need to show onboarding
Future<void> setShouldShowOnboarding(final bool newValue) =>
dataSource.setOnboardingFlag(newValue);
}

View file

@ -1,3 +1,4 @@
import 'dart:convert';
import 'dart:io';
import 'package:graphql_flutter/graphql_flutter.dart';
@ -5,15 +6,10 @@ import 'package:http/io_client.dart';
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/api_maps/tls_options.dart';
import 'package:selfprivacy/logic/get_it/resources_model.dart';
import 'package:selfprivacy/logic/models/message.dart';
import 'package:selfprivacy/logic/models/console_log.dart';
void _logToAppConsole<T>(final T objectToLog) {
getIt.get<ConsoleModel>().addMessage(
Message(
text: objectToLog.toString(),
),
);
}
void _addConsoleLog(final ConsoleLog message) =>
getIt.get<ConsoleModel>().log(message);
class RequestLoggingLink extends Link {
@override
@ -21,13 +17,14 @@ class RequestLoggingLink extends Link {
final Request request, [
final NextLink? forward,
]) async* {
getIt.get<ConsoleModel>().addMessage(
GraphQlRequestMessage(
operation: request.operation,
variables: request.variables,
context: request.context,
),
);
_addConsoleLog(
GraphQlRequestConsoleLog(
// context: request.context,
operationType: request.type.name,
operation: request.operation,
variables: request.variables,
),
);
yield* forward!(request);
}
}
@ -36,20 +33,26 @@ class ResponseLoggingParser extends ResponseParser {
@override
Response parseResponse(final Map<String, dynamic> body) {
final response = super.parseResponse(body);
getIt.get<ConsoleModel>().addMessage(
GraphQlResponseMessage(
data: response.data,
errors: response.errors,
context: response.context,
),
);
_addConsoleLog(
GraphQlResponseConsoleLog(
// context: response.context,
data: response.data,
errors: response.errors,
rawResponse: jsonEncode(response.response),
),
);
return response;
}
@override
GraphQLError parseError(final Map<String, dynamic> error) {
final graphQlError = super.parseError(error);
_logToAppConsole(graphQlError);
_addConsoleLog(
ManualConsoleLog.warning(
customTitle: 'GraphQL Error',
content: graphQlError.toString(),
),
);
return graphQlError;
}
}
@ -114,14 +117,15 @@ abstract class GraphQLApiMap {
);
}
String get _locale => getIt.get<ApiConfigModel>().localeCode ?? 'en';
String get _locale => getIt.get<ApiConfigModel>().localeCode;
String get _token {
String token = '';
final serverDetails = getIt<ResourcesModel>().serverDetails;
if (serverDetails != null) {
token = getIt<ResourcesModel>().serverDetails!.apiToken;
token = serverDetails.apiToken;
}
return token;
}

View file

@ -443,6 +443,7 @@ type SystemMutations {
runSystemUpgrade: GenericJobMutationReturn!
rebootSystem: GenericMutationReturn!
pullRepositoryChanges: GenericMutationReturn!
nixCollectGarbage: GenericJobMutationReturn!
}
type SystemProviderInfo {

View file

@ -79,6 +79,17 @@ mutation RunSystemUpgrade {
}
}
mutation NixCollectGarbage {
system {
nixCollectGarbage {
...basicMutationReturnFields
job {
...basicApiJobsFields
}
}
}
}
mutation RunSystemUpgradeFallback {
system {
runSystemUpgrade {

View file

@ -7043,6 +7043,663 @@ class _CopyWithStubImpl$Mutation$RunSystemUpgrade$system$runSystemUpgrade<TRes>
CopyWith$Fragment$basicApiJobsFields.stub(_res);
}
class Mutation$NixCollectGarbage {
Mutation$NixCollectGarbage({
required this.system,
this.$__typename = 'Mutation',
});
factory Mutation$NixCollectGarbage.fromJson(Map<String, dynamic> json) {
final l$system = json['system'];
final l$$__typename = json['__typename'];
return Mutation$NixCollectGarbage(
system: Mutation$NixCollectGarbage$system.fromJson(
(l$system as Map<String, dynamic>)),
$__typename: (l$$__typename as String),
);
}
final Mutation$NixCollectGarbage$system system;
final String $__typename;
Map<String, dynamic> toJson() {
final _resultData = <String, dynamic>{};
final l$system = system;
_resultData['system'] = l$system.toJson();
final l$$__typename = $__typename;
_resultData['__typename'] = l$$__typename;
return _resultData;
}
@override
int get hashCode {
final l$system = system;
final l$$__typename = $__typename;
return Object.hashAll([
l$system,
l$$__typename,
]);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) {
return true;
}
if (!(other is Mutation$NixCollectGarbage) ||
runtimeType != other.runtimeType) {
return false;
}
final l$system = system;
final lOther$system = other.system;
if (l$system != lOther$system) {
return false;
}
final l$$__typename = $__typename;
final lOther$$__typename = other.$__typename;
if (l$$__typename != lOther$$__typename) {
return false;
}
return true;
}
}
extension UtilityExtension$Mutation$NixCollectGarbage
on Mutation$NixCollectGarbage {
CopyWith$Mutation$NixCollectGarbage<Mutation$NixCollectGarbage>
get copyWith => CopyWith$Mutation$NixCollectGarbage(
this,
(i) => i,
);
}
abstract class CopyWith$Mutation$NixCollectGarbage<TRes> {
factory CopyWith$Mutation$NixCollectGarbage(
Mutation$NixCollectGarbage instance,
TRes Function(Mutation$NixCollectGarbage) then,
) = _CopyWithImpl$Mutation$NixCollectGarbage;
factory CopyWith$Mutation$NixCollectGarbage.stub(TRes res) =
_CopyWithStubImpl$Mutation$NixCollectGarbage;
TRes call({
Mutation$NixCollectGarbage$system? system,
String? $__typename,
});
CopyWith$Mutation$NixCollectGarbage$system<TRes> get system;
}
class _CopyWithImpl$Mutation$NixCollectGarbage<TRes>
implements CopyWith$Mutation$NixCollectGarbage<TRes> {
_CopyWithImpl$Mutation$NixCollectGarbage(
this._instance,
this._then,
);
final Mutation$NixCollectGarbage _instance;
final TRes Function(Mutation$NixCollectGarbage) _then;
static const _undefined = <dynamic, dynamic>{};
TRes call({
Object? system = _undefined,
Object? $__typename = _undefined,
}) =>
_then(Mutation$NixCollectGarbage(
system: system == _undefined || system == null
? _instance.system
: (system as Mutation$NixCollectGarbage$system),
$__typename: $__typename == _undefined || $__typename == null
? _instance.$__typename
: ($__typename as String),
));
CopyWith$Mutation$NixCollectGarbage$system<TRes> get system {
final local$system = _instance.system;
return CopyWith$Mutation$NixCollectGarbage$system(
local$system, (e) => call(system: e));
}
}
class _CopyWithStubImpl$Mutation$NixCollectGarbage<TRes>
implements CopyWith$Mutation$NixCollectGarbage<TRes> {
_CopyWithStubImpl$Mutation$NixCollectGarbage(this._res);
TRes _res;
call({
Mutation$NixCollectGarbage$system? system,
String? $__typename,
}) =>
_res;
CopyWith$Mutation$NixCollectGarbage$system<TRes> get system =>
CopyWith$Mutation$NixCollectGarbage$system.stub(_res);
}
const documentNodeMutationNixCollectGarbage = DocumentNode(definitions: [
OperationDefinitionNode(
type: OperationType.mutation,
name: NameNode(value: 'NixCollectGarbage'),
variableDefinitions: [],
directives: [],
selectionSet: SelectionSetNode(selections: [
FieldNode(
name: NameNode(value: 'system'),
alias: null,
arguments: [],
directives: [],
selectionSet: SelectionSetNode(selections: [
FieldNode(
name: NameNode(value: 'nixCollectGarbage'),
alias: null,
arguments: [],
directives: [],
selectionSet: SelectionSetNode(selections: [
FragmentSpreadNode(
name: NameNode(value: 'basicMutationReturnFields'),
directives: [],
),
FieldNode(
name: NameNode(value: 'job'),
alias: null,
arguments: [],
directives: [],
selectionSet: SelectionSetNode(selections: [
FragmentSpreadNode(
name: NameNode(value: 'basicApiJobsFields'),
directives: [],
),
FieldNode(
name: NameNode(value: '__typename'),
alias: null,
arguments: [],
directives: [],
selectionSet: null,
),
]),
),
FieldNode(
name: NameNode(value: '__typename'),
alias: null,
arguments: [],
directives: [],
selectionSet: null,
),
]),
),
FieldNode(
name: NameNode(value: '__typename'),
alias: null,
arguments: [],
directives: [],
selectionSet: null,
),
]),
),
FieldNode(
name: NameNode(value: '__typename'),
alias: null,
arguments: [],
directives: [],
selectionSet: null,
),
]),
),
fragmentDefinitionbasicMutationReturnFields,
fragmentDefinitionbasicApiJobsFields,
]);
Mutation$NixCollectGarbage _parserFn$Mutation$NixCollectGarbage(
Map<String, dynamic> data) =>
Mutation$NixCollectGarbage.fromJson(data);
typedef OnMutationCompleted$Mutation$NixCollectGarbage = FutureOr<void>
Function(
Map<String, dynamic>?,
Mutation$NixCollectGarbage?,
);
class Options$Mutation$NixCollectGarbage
extends graphql.MutationOptions<Mutation$NixCollectGarbage> {
Options$Mutation$NixCollectGarbage({
String? operationName,
graphql.FetchPolicy? fetchPolicy,
graphql.ErrorPolicy? errorPolicy,
graphql.CacheRereadPolicy? cacheRereadPolicy,
Object? optimisticResult,
Mutation$NixCollectGarbage? typedOptimisticResult,
graphql.Context? context,
OnMutationCompleted$Mutation$NixCollectGarbage? onCompleted,
graphql.OnMutationUpdate<Mutation$NixCollectGarbage>? update,
graphql.OnError? onError,
}) : onCompletedWithParsed = onCompleted,
super(
operationName: operationName,
fetchPolicy: fetchPolicy,
errorPolicy: errorPolicy,
cacheRereadPolicy: cacheRereadPolicy,
optimisticResult: optimisticResult ?? typedOptimisticResult?.toJson(),
context: context,
onCompleted: onCompleted == null
? null
: (data) => onCompleted(
data,
data == null
? null
: _parserFn$Mutation$NixCollectGarbage(data),
),
update: update,
onError: onError,
document: documentNodeMutationNixCollectGarbage,
parserFn: _parserFn$Mutation$NixCollectGarbage,
);
final OnMutationCompleted$Mutation$NixCollectGarbage? onCompletedWithParsed;
@override
List<Object?> get properties => [
...super.onCompleted == null
? super.properties
: super.properties.where((property) => property != onCompleted),
onCompletedWithParsed,
];
}
class WatchOptions$Mutation$NixCollectGarbage
extends graphql.WatchQueryOptions<Mutation$NixCollectGarbage> {
WatchOptions$Mutation$NixCollectGarbage({
String? operationName,
graphql.FetchPolicy? fetchPolicy,
graphql.ErrorPolicy? errorPolicy,
graphql.CacheRereadPolicy? cacheRereadPolicy,
Object? optimisticResult,
Mutation$NixCollectGarbage? typedOptimisticResult,
graphql.Context? context,
Duration? pollInterval,
bool? eagerlyFetchResults,
bool carryForwardDataOnException = true,
bool fetchResults = false,
}) : super(
operationName: operationName,
fetchPolicy: fetchPolicy,
errorPolicy: errorPolicy,
cacheRereadPolicy: cacheRereadPolicy,
optimisticResult: optimisticResult ?? typedOptimisticResult?.toJson(),
context: context,
document: documentNodeMutationNixCollectGarbage,
pollInterval: pollInterval,
eagerlyFetchResults: eagerlyFetchResults,
carryForwardDataOnException: carryForwardDataOnException,
fetchResults: fetchResults,
parserFn: _parserFn$Mutation$NixCollectGarbage,
);
}
extension ClientExtension$Mutation$NixCollectGarbage on graphql.GraphQLClient {
Future<graphql.QueryResult<Mutation$NixCollectGarbage>>
mutate$NixCollectGarbage(
[Options$Mutation$NixCollectGarbage? options]) async =>
await this.mutate(options ?? Options$Mutation$NixCollectGarbage());
graphql.ObservableQuery<
Mutation$NixCollectGarbage> watchMutation$NixCollectGarbage(
[WatchOptions$Mutation$NixCollectGarbage? options]) =>
this.watchMutation(options ?? WatchOptions$Mutation$NixCollectGarbage());
}
class Mutation$NixCollectGarbage$system {
Mutation$NixCollectGarbage$system({
required this.nixCollectGarbage,
this.$__typename = 'SystemMutations',
});
factory Mutation$NixCollectGarbage$system.fromJson(
Map<String, dynamic> json) {
final l$nixCollectGarbage = json['nixCollectGarbage'];
final l$$__typename = json['__typename'];
return Mutation$NixCollectGarbage$system(
nixCollectGarbage:
Mutation$NixCollectGarbage$system$nixCollectGarbage.fromJson(
(l$nixCollectGarbage as Map<String, dynamic>)),
$__typename: (l$$__typename as String),
);
}
final Mutation$NixCollectGarbage$system$nixCollectGarbage nixCollectGarbage;
final String $__typename;
Map<String, dynamic> toJson() {
final _resultData = <String, dynamic>{};
final l$nixCollectGarbage = nixCollectGarbage;
_resultData['nixCollectGarbage'] = l$nixCollectGarbage.toJson();
final l$$__typename = $__typename;
_resultData['__typename'] = l$$__typename;
return _resultData;
}
@override
int get hashCode {
final l$nixCollectGarbage = nixCollectGarbage;
final l$$__typename = $__typename;
return Object.hashAll([
l$nixCollectGarbage,
l$$__typename,
]);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) {
return true;
}
if (!(other is Mutation$NixCollectGarbage$system) ||
runtimeType != other.runtimeType) {
return false;
}
final l$nixCollectGarbage = nixCollectGarbage;
final lOther$nixCollectGarbage = other.nixCollectGarbage;
if (l$nixCollectGarbage != lOther$nixCollectGarbage) {
return false;
}
final l$$__typename = $__typename;
final lOther$$__typename = other.$__typename;
if (l$$__typename != lOther$$__typename) {
return false;
}
return true;
}
}
extension UtilityExtension$Mutation$NixCollectGarbage$system
on Mutation$NixCollectGarbage$system {
CopyWith$Mutation$NixCollectGarbage$system<Mutation$NixCollectGarbage$system>
get copyWith => CopyWith$Mutation$NixCollectGarbage$system(
this,
(i) => i,
);
}
abstract class CopyWith$Mutation$NixCollectGarbage$system<TRes> {
factory CopyWith$Mutation$NixCollectGarbage$system(
Mutation$NixCollectGarbage$system instance,
TRes Function(Mutation$NixCollectGarbage$system) then,
) = _CopyWithImpl$Mutation$NixCollectGarbage$system;
factory CopyWith$Mutation$NixCollectGarbage$system.stub(TRes res) =
_CopyWithStubImpl$Mutation$NixCollectGarbage$system;
TRes call({
Mutation$NixCollectGarbage$system$nixCollectGarbage? nixCollectGarbage,
String? $__typename,
});
CopyWith$Mutation$NixCollectGarbage$system$nixCollectGarbage<TRes>
get nixCollectGarbage;
}
class _CopyWithImpl$Mutation$NixCollectGarbage$system<TRes>
implements CopyWith$Mutation$NixCollectGarbage$system<TRes> {
_CopyWithImpl$Mutation$NixCollectGarbage$system(
this._instance,
this._then,
);
final Mutation$NixCollectGarbage$system _instance;
final TRes Function(Mutation$NixCollectGarbage$system) _then;
static const _undefined = <dynamic, dynamic>{};
TRes call({
Object? nixCollectGarbage = _undefined,
Object? $__typename = _undefined,
}) =>
_then(Mutation$NixCollectGarbage$system(
nixCollectGarbage:
nixCollectGarbage == _undefined || nixCollectGarbage == null
? _instance.nixCollectGarbage
: (nixCollectGarbage
as Mutation$NixCollectGarbage$system$nixCollectGarbage),
$__typename: $__typename == _undefined || $__typename == null
? _instance.$__typename
: ($__typename as String),
));
CopyWith$Mutation$NixCollectGarbage$system$nixCollectGarbage<TRes>
get nixCollectGarbage {
final local$nixCollectGarbage = _instance.nixCollectGarbage;
return CopyWith$Mutation$NixCollectGarbage$system$nixCollectGarbage(
local$nixCollectGarbage, (e) => call(nixCollectGarbage: e));
}
}
class _CopyWithStubImpl$Mutation$NixCollectGarbage$system<TRes>
implements CopyWith$Mutation$NixCollectGarbage$system<TRes> {
_CopyWithStubImpl$Mutation$NixCollectGarbage$system(this._res);
TRes _res;
call({
Mutation$NixCollectGarbage$system$nixCollectGarbage? nixCollectGarbage,
String? $__typename,
}) =>
_res;
CopyWith$Mutation$NixCollectGarbage$system$nixCollectGarbage<TRes>
get nixCollectGarbage =>
CopyWith$Mutation$NixCollectGarbage$system$nixCollectGarbage.stub(
_res);
}
class Mutation$NixCollectGarbage$system$nixCollectGarbage
implements Fragment$basicMutationReturnFields$$GenericJobMutationReturn {
Mutation$NixCollectGarbage$system$nixCollectGarbage({
required this.code,
required this.message,
required this.success,
this.$__typename = 'GenericJobMutationReturn',
this.job,
});
factory Mutation$NixCollectGarbage$system$nixCollectGarbage.fromJson(
Map<String, dynamic> json) {
final l$code = json['code'];
final l$message = json['message'];
final l$success = json['success'];
final l$$__typename = json['__typename'];
final l$job = json['job'];
return Mutation$NixCollectGarbage$system$nixCollectGarbage(
code: (l$code as int),
message: (l$message as String),
success: (l$success as bool),
$__typename: (l$$__typename as String),
job: l$job == null
? null
: Fragment$basicApiJobsFields.fromJson(
(l$job as Map<String, dynamic>)),
);
}
final int code;
final String message;
final bool success;
final String $__typename;
final Fragment$basicApiJobsFields? job;
Map<String, dynamic> toJson() {
final _resultData = <String, dynamic>{};
final l$code = code;
_resultData['code'] = l$code;
final l$message = message;
_resultData['message'] = l$message;
final l$success = success;
_resultData['success'] = l$success;
final l$$__typename = $__typename;
_resultData['__typename'] = l$$__typename;
final l$job = job;
_resultData['job'] = l$job?.toJson();
return _resultData;
}
@override
int get hashCode {
final l$code = code;
final l$message = message;
final l$success = success;
final l$$__typename = $__typename;
final l$job = job;
return Object.hashAll([
l$code,
l$message,
l$success,
l$$__typename,
l$job,
]);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) {
return true;
}
if (!(other is Mutation$NixCollectGarbage$system$nixCollectGarbage) ||
runtimeType != other.runtimeType) {
return false;
}
final l$code = code;
final lOther$code = other.code;
if (l$code != lOther$code) {
return false;
}
final l$message = message;
final lOther$message = other.message;
if (l$message != lOther$message) {
return false;
}
final l$success = success;
final lOther$success = other.success;
if (l$success != lOther$success) {
return false;
}
final l$$__typename = $__typename;
final lOther$$__typename = other.$__typename;
if (l$$__typename != lOther$$__typename) {
return false;
}
final l$job = job;
final lOther$job = other.job;
if (l$job != lOther$job) {
return false;
}
return true;
}
}
extension UtilityExtension$Mutation$NixCollectGarbage$system$nixCollectGarbage
on Mutation$NixCollectGarbage$system$nixCollectGarbage {
CopyWith$Mutation$NixCollectGarbage$system$nixCollectGarbage<
Mutation$NixCollectGarbage$system$nixCollectGarbage>
get copyWith =>
CopyWith$Mutation$NixCollectGarbage$system$nixCollectGarbage(
this,
(i) => i,
);
}
abstract class CopyWith$Mutation$NixCollectGarbage$system$nixCollectGarbage<
TRes> {
factory CopyWith$Mutation$NixCollectGarbage$system$nixCollectGarbage(
Mutation$NixCollectGarbage$system$nixCollectGarbage instance,
TRes Function(Mutation$NixCollectGarbage$system$nixCollectGarbage) then,
) = _CopyWithImpl$Mutation$NixCollectGarbage$system$nixCollectGarbage;
factory CopyWith$Mutation$NixCollectGarbage$system$nixCollectGarbage.stub(
TRes res) =
_CopyWithStubImpl$Mutation$NixCollectGarbage$system$nixCollectGarbage;
TRes call({
int? code,
String? message,
bool? success,
String? $__typename,
Fragment$basicApiJobsFields? job,
});
CopyWith$Fragment$basicApiJobsFields<TRes> get job;
}
class _CopyWithImpl$Mutation$NixCollectGarbage$system$nixCollectGarbage<TRes>
implements
CopyWith$Mutation$NixCollectGarbage$system$nixCollectGarbage<TRes> {
_CopyWithImpl$Mutation$NixCollectGarbage$system$nixCollectGarbage(
this._instance,
this._then,
);
final Mutation$NixCollectGarbage$system$nixCollectGarbage _instance;
final TRes Function(Mutation$NixCollectGarbage$system$nixCollectGarbage)
_then;
static const _undefined = <dynamic, dynamic>{};
TRes call({
Object? code = _undefined,
Object? message = _undefined,
Object? success = _undefined,
Object? $__typename = _undefined,
Object? job = _undefined,
}) =>
_then(Mutation$NixCollectGarbage$system$nixCollectGarbage(
code:
code == _undefined || code == null ? _instance.code : (code as int),
message: message == _undefined || message == null
? _instance.message
: (message as String),
success: success == _undefined || success == null
? _instance.success
: (success as bool),
$__typename: $__typename == _undefined || $__typename == null
? _instance.$__typename
: ($__typename as String),
job: job == _undefined
? _instance.job
: (job as Fragment$basicApiJobsFields?),
));
CopyWith$Fragment$basicApiJobsFields<TRes> get job {
final local$job = _instance.job;
return local$job == null
? CopyWith$Fragment$basicApiJobsFields.stub(_then(_instance))
: CopyWith$Fragment$basicApiJobsFields(local$job, (e) => call(job: e));
}
}
class _CopyWithStubImpl$Mutation$NixCollectGarbage$system$nixCollectGarbage<
TRes>
implements
CopyWith$Mutation$NixCollectGarbage$system$nixCollectGarbage<TRes> {
_CopyWithStubImpl$Mutation$NixCollectGarbage$system$nixCollectGarbage(
this._res);
TRes _res;
call({
int? code,
String? message,
bool? success,
String? $__typename,
Fragment$basicApiJobsFields? job,
}) =>
_res;
CopyWith$Fragment$basicApiJobsFields<TRes> get job =>
CopyWith$Fragment$basicApiJobsFields.stub(_res);
}
class Mutation$RunSystemUpgradeFallback {
Mutation$RunSystemUpgradeFallback({
required this.system,

View file

@ -144,4 +144,38 @@ mixin ServerActionsApi on GraphQLApiMap {
);
}
}
Future<GenericResult<ServerJob?>> collectNixGarbage() async {
try {
final GraphQLClient client = await getClient();
final result = await client.mutate$NixCollectGarbage();
if (result.hasException) {
return GenericResult(
success: false,
data: null,
);
} else if (result.parsedData!.system.nixCollectGarbage.success &&
result.parsedData!.system.nixCollectGarbage.job != null) {
return GenericResult(
success: true,
data: ServerJob.fromGraphQL(
result.parsedData!.system.nixCollectGarbage.job!,
),
message: result.parsedData!.system.nixCollectGarbage.message,
);
} else {
return GenericResult(
success: false,
message: result.parsedData!.system.nixCollectGarbage.message,
data: null,
);
}
} catch (e) {
return GenericResult(
success: false,
message: e.toString(),
data: null,
);
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,27 @@
import 'dart:async';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:selfprivacy/config/get_it_config.dart';
/// basically, a bus for other blocs to listen to server status updates
class ConnectionStatusBloc extends Bloc<ConnectionStatus, ConnectionStatus> {
ConnectionStatusBloc() : super(ConnectionStatus.nonexistent) {
on<ConnectionStatus>(
(final newStatus, final emit) => emit(newStatus),
);
final apiConnectionRepository = getIt<ApiConnectionRepository>();
_apiConnectionStatusSubscription =
apiConnectionRepository.connectionStatusStream.listen(
(final ConnectionStatus newStatus) => add(newStatus),
);
}
StreamSubscription? _apiConnectionStatusSubscription;
@override
Future<void> close() {
_apiConnectionStatusSubscription?.cancel();
return super.close();
}
}

View file

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

View file

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

View file

@ -178,6 +178,45 @@ class JobsCubit extends Cubit<JobsState> {
}
}
Future<void> collectNixGarbage() async {
if (state is JobsStateEmpty) {
emit(
JobsStateLoading(
[CollectNixGarbageJob(status: JobStatusEnum.running)],
null,
const [],
),
);
final result =
await getIt<ApiConnectionRepository>().api.collectNixGarbage();
if (result.success && result.data != null) {
emit(
JobsStateLoading(
[CollectNixGarbageJob(status: JobStatusEnum.finished)],
result.data!.uid,
const [],
),
);
} else if (result.success) {
emit(
JobsStateFinished(
[CollectNixGarbageJob(status: JobStatusEnum.finished)],
null,
const [],
),
);
} else {
emit(
JobsStateFinished(
[CollectNixGarbageJob(status: JobStatusEnum.error)],
null,
const [],
),
);
}
}
}
Future<void> acknowledgeFinished() async {
if (state is! JobsStateFinished) {
return;

View file

@ -49,7 +49,8 @@ abstract class ServerInstallationState extends Equatable {
bool get isPrimaryUserFilled => rootUser != null;
bool get isServerCreated => serverDetails != null;
bool get isFullyInitialized => _fulfillmentList.every((final el) => el!);
bool get isFullyInitialized =>
_fulfillmentList.every((final el) => el ?? false);
ServerSetupProgress get progress => ServerSetupProgress
.values[_fulfillmentList.where((final el) => el!).length];

View file

@ -7,21 +7,22 @@ class ApiConfigModel {
String? get localeCode => _localeCode;
static const localeCodeFallback = 'en';
String? _localeCode;
Future<void> setLocaleCode(final String value) async {
_localeCode = value;
}
String get localeCode => _localeCode ?? localeCodeFallback;
Future<void> setLocaleCode(final String value) async => _localeCode = value;
Future<void> resetLocaleCode() async => _localeCode = null;
Future<void> setBackblazeBucket(final BackblazeBucket value) async {
await _box.put(BNames.backblazeBucket, value);
}
// TODO: Remove it
void clear() {
_localeCode = null;
}
// TODO: Remove it
void init() {
_localeCode = 'en';
}
}

View file

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

View file

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

View file

@ -0,0 +1,184 @@
import 'dart:convert';
import 'package:gql/language.dart' as gql;
import 'package:graphql/client.dart' as gql_client;
import 'package:intl/intl.dart';
enum ConsoleLogSeverity {
normal,
warning,
}
/// Base entity for console logs.
sealed class ConsoleLog {
ConsoleLog({
final String? customTitle,
this.severity = ConsoleLogSeverity.normal,
}) : title = customTitle ??
(severity == ConsoleLogSeverity.warning ? 'Error' : 'Log'),
time = DateTime.now();
final DateTime time;
final ConsoleLogSeverity severity;
bool get isError => severity == ConsoleLogSeverity.warning;
/// title for both in listing and in dialog
final String title;
/// formatted data to be shown in listing
String get content;
/// data available for copy in dialog
String? get shareableData => '{"title": "$title",\n'
'"timestamp": "$fullUTCString",\n'
'"data":{\n$content\n}'
'\n}';
static final DateFormat _formatter = DateFormat('hh:mm:ss');
String get timeString => _formatter.format(time);
String get fullUTCString => time.toUtc().toIso8601String();
}
abstract class LogWithRawResponse {
String get rawResponse;
}
/// entity for manually created logs, as opposed to automated ones coming
/// from requests / responses
class ManualConsoleLog extends ConsoleLog {
ManualConsoleLog({
required this.content,
super.customTitle,
super.severity,
});
ManualConsoleLog.warning({
required this.content,
super.customTitle,
}) : super(severity: ConsoleLogSeverity.warning);
@override
String content;
}
class RestApiRequestConsoleLog extends ConsoleLog {
RestApiRequestConsoleLog({
this.method,
this.uri,
this.headers,
this.data,
super.severity,
});
/// headers thath should not be included into clipboard buffer, as opposed to
/// `[[ConsoleLogItemDialog]]` `_KeyValueRow.hideList` which filters values,
/// that should be accessible from UI, but hidden in screenshots
static const blacklistedHeaders = ['Authorization'];
final String? method;
final Uri? uri;
final Map<String, dynamic>? headers;
final String? data;
@override
String get title => 'Rest API Request';
Map<String, dynamic> get filteredHeaders => Map.fromEntries(
headers?.entries.where(
(final entry) => !blacklistedHeaders.contains(entry.key),
) ??
const [],
);
@override
String get content => '"method": "$method",\n'
'"uri": "$uri",\n'
'"headers": ${jsonEncode(filteredHeaders)},\n' // censor header to not expose API keys
'"data": $data';
}
class RestApiResponseConsoleLog extends ConsoleLog {
RestApiResponseConsoleLog({
this.method,
this.uri,
this.statusCode,
this.data,
super.severity,
});
final String? method;
final Uri? uri;
final int? statusCode;
final String? data;
@override
String get title => 'Rest API Response';
@override
String get content => '"method": "$method",\n'
'"status_code": $statusCode,\n'
'"uri": "$uri",\n'
'"data": $data';
}
/// there is no actual getter for context fields outside of its class
/// one can extract unique entries by their type, which implements
/// `ContextEntry` class, I'll leave the code here if in the future
/// some entries will actually be needed.
// extension ContextEncoder on gql_client.Context {
// String get encode {
// return '""';
// }
// }
class GraphQlRequestConsoleLog extends ConsoleLog {
GraphQlRequestConsoleLog({
required this.operationType,
required this.operation,
required this.variables,
// this.context,
super.severity,
});
// final gql_client.Context? context;
final String operationType;
final gql_client.Operation? operation;
String get operationDocument =>
operation != null ? gql.printNode(operation!.document) : 'null';
final Map<String, dynamic>? variables;
@override
String get title => 'GraphQL Request';
@override
String get content =>
// '"context": ${context?.encode},\n'
'"variables": ${jsonEncode(variables)},\n'
'"type": "$operationType",\n'
'"name": "${operation?.operationName}",\n'
'"document": ${jsonEncode(operationDocument)}';
}
class GraphQlResponseConsoleLog extends ConsoleLog
implements LogWithRawResponse {
GraphQlResponseConsoleLog({
required this.rawResponse,
// this.context,
this.data,
this.errors,
super.severity,
});
@override
final String rawResponse;
// final gql_client.Context? context;
final Map<String, dynamic>? data;
final List<gql_client.GraphQLError>? errors;
@override
String get title => 'GraphQL Response';
@override
String get content =>
// '"context": ${context?.encode},\n'
'"data": ${jsonEncode(data)},\n'
'"errors": $errors';
}

View file

@ -62,6 +62,36 @@ class UpgradeServerJob extends ClientJob {
);
}
class CollectNixGarbageJob extends ClientJob {
CollectNixGarbageJob({
super.status,
super.message,
super.id,
}) : super(title: 'jobs.collect_nix_garbage'.tr());
@override
bool canAddTo(final List<ClientJob> jobs) =>
!jobs.any((final job) => job is CollectNixGarbageJob);
@override
Future<(bool, String)> execute() async {
final result =
await getIt<ApiConnectionRepository>().api.collectNixGarbage();
return (result.success, result.message ?? '');
}
@override
CollectNixGarbageJob copyWithNewStatus({
required final JobStatusEnum status,
final String? message,
}) =>
CollectNixGarbageJob(
status: status,
message: message,
id: id,
);
}
class RebootServerJob extends ClientJob {
RebootServerJob({
super.status,

View file

@ -65,14 +65,34 @@ class DigitalOceanLocation {
emoji = '🇮🇳';
break;
case 'syd':
emoji = '🇦🇺';
break;
case 'nyc':
case 'sfo':
emoji = '🇺🇸';
break;
}
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 {
final countryName = _townPrefixToCountryMap[slug.substring(0, 3)] ?? slug;
return 'countries.$countryName';
}
}
@JsonSerializable()

View file

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

View file

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

View file

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

View file

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

View file

@ -155,6 +155,27 @@ class HetznerLocation {
}
return emoji;
}
String get countryDisplayKey {
String displayKey = 'countries.';
switch (country.substring(0, 2)) {
case 'DE':
displayKey += 'germany';
break;
case 'FI':
displayKey += 'finland';
break;
case 'US':
displayKey += 'united_states';
break;
default:
displayKey = country;
}
return displayKey;
}
}
/// A Volume is a highly-available, scalable, and SSD-based block storage for Servers.

View file

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

View file

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

View file

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

View file

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

View file

@ -2,12 +2,14 @@ class ServerProviderLocation {
ServerProviderLocation({
required this.title,
required this.identifier,
required this.countryDisplayKey,
this.description,
this.flag = '',
});
final String title;
final String identifier;
final String countryDisplayKey;
final String? description;
final String flag;
}

View file

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

View file

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

View file

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

View file

@ -38,9 +38,9 @@ class DigitalOceanServerProvider extends ServerProvider {
DigitalOceanServerProvider() : _adapter = ApiAdapter();
DigitalOceanServerProvider.load(
final String? location,
final bool isAuthotized,
final bool isAuthorized,
) : _adapter = ApiAdapter(
isWithToken: isAuthotized,
isWithToken: isAuthorized,
region: location,
);
@ -438,6 +438,7 @@ class DigitalOceanServerProvider extends ServerProvider {
description: rawLocation.name,
flag: rawLocation.flag,
identifier: rawLocation.slug,
countryDisplayKey: rawLocation.countryDisplayKey,
);
} catch (e) {
continue;

View file

@ -38,9 +38,9 @@ class HetznerServerProvider extends ServerProvider {
HetznerServerProvider() : _adapter = ApiAdapter();
HetznerServerProvider.load(
final String? location,
final bool isAuthotized,
final bool isAuthorized,
) : _adapter = ApiAdapter(
isWithToken: isAuthotized,
isWithToken: isAuthorized,
region: location,
);
@ -156,6 +156,7 @@ class HetznerServerProvider extends ServerProvider {
description: server.location.description,
flag: server.location.flag,
identifier: server.location.name,
countryDisplayKey: server.location.countryDisplayKey,
),
),
);
@ -456,6 +457,7 @@ class HetznerServerProvider extends ServerProvider {
description: rawLocation.description,
flag: rawLocation.flag,
identifier: rawLocation.name,
countryDisplayKey: rawLocation.countryDisplayKey,
);
} catch (e) {
continue;

View file

@ -1,21 +1,21 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:selfprivacy/config/app_controller/inherited_app_controller.dart';
import 'package:selfprivacy/config/bloc_config.dart';
import 'package:selfprivacy/config/bloc_observer.dart';
import 'package:selfprivacy/config/brand_colors.dart';
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/config/hive_config.dart';
import 'package:selfprivacy/config/localization.dart';
import 'package:selfprivacy/logic/cubit/app_settings/app_settings_cubit.dart';
import 'package:selfprivacy/theming/factory/app_theme_factory.dart';
import 'package:selfprivacy/config/preferences_repository/datasources/preferences_hive_datasource.dart';
import 'package:selfprivacy/config/preferences_repository/inherited_preferences_repository.dart';
import 'package:selfprivacy/ui/pages/errors/failed_to_init_secure_storage.dart';
import 'package:selfprivacy/ui/router/router.dart';
// import 'package:wakelock/wakelock.dart';
import 'package:timezone/data/latest.dart' as tz;
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await HiveConfig.init();
// await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
// try {
@ -26,85 +26,117 @@ void main() async {
// print(e);
// }
await getItSetup();
await EasyLocalization.ensureInitialized();
tz.initializeTimeZones();
try {
await Future.wait(
<Future<void>>[
HiveConfig.init(),
EasyLocalization.ensureInitialized(),
],
);
await getItSetup();
} on PlatformException catch (e) {
runApp(
FailedToInitSecureStorageScreen(e: e),
);
}
final ThemeData lightThemeData = await AppThemeFactory.create(
isDark: false,
fallbackColor: BrandColors.primary,
);
final ThemeData darkThemeData = await AppThemeFactory.create(
isDark: true,
fallbackColor: BrandColors.primary,
);
tz.initializeTimeZones();
Bloc.observer = SimpleBlocObserver();
runApp(
Localization(
child: SelfprivacyApp(
lightThemeData: lightThemeData,
darkThemeData: darkThemeData,
child: InheritedPreferencesRepository(
dataSource: PreferencesHiveDataSource(),
child: const InheritedAppController(
child: AppBuilder(),
),
),
),
);
}
class SelfprivacyApp extends StatelessWidget {
SelfprivacyApp({
required this.lightThemeData,
required this.darkThemeData,
super.key,
});
final ThemeData lightThemeData;
final ThemeData darkThemeData;
final _appRouter = RootRouter(getIt.get<NavigationService>().navigatorKey);
class AppBuilder extends StatelessWidget {
const AppBuilder({super.key});
@override
Widget build(final BuildContext context) => Localization(
child: BlocAndProviderConfig(
child: BlocBuilder<AppSettingsCubit, AppSettingsState>(
builder: (
final BuildContext context,
final AppSettingsState appSettings,
) {
getIt.get<ApiConfigModel>().setLocaleCode(
context.locale.languageCode,
);
return MaterialApp.router(
routeInformationParser: _appRouter.defaultRouteParser(),
routerDelegate: _appRouter.delegate(),
scaffoldMessengerKey:
getIt.get<NavigationService>().scaffoldMessengerKey,
localizationsDelegates: context.localizationDelegates,
supportedLocales: context.supportedLocales,
locale: context.locale,
debugShowCheckedModeBanner: false,
title: 'SelfPrivacy',
theme: lightThemeData,
darkTheme: darkThemeData,
themeMode: appSettings.isAutoDarkModeOn
? ThemeMode.system
: appSettings.isDarkModeOn
? ThemeMode.dark
: ThemeMode.light,
builder: (final BuildContext context, final Widget? widget) {
Widget error =
const Center(child: Text('...rendering error...'));
if (widget is Scaffold || widget is Navigator) {
error = Scaffold(body: error);
}
ErrorWidget.builder =
(final FlutterErrorDetails errorDetails) => error;
Widget build(final BuildContext context) {
final appController = InheritedAppController.of(context);
return widget ?? error;
},
);
},
if (appController.loaded) {
return const SelfprivacyApp();
}
return const SplashScreen();
}
}
/// Widget to be shown
/// until essential app initialization is completed
class SplashScreen extends StatelessWidget {
const SplashScreen({super.key});
@override
Widget build(final BuildContext context) => const ColoredBox(
color: Colors.white,
child: Center(
child: CircularProgressIndicator.adaptive(
valueColor: AlwaysStoppedAnimation(BrandColors.primary),
),
),
);
}
class SelfprivacyApp extends StatefulWidget {
const SelfprivacyApp({
super.key,
});
@override
State<SelfprivacyApp> createState() => _SelfprivacyAppState();
}
class _SelfprivacyAppState extends State<SelfprivacyApp> {
final appKey = UniqueKey();
final _appRouter = RootRouter(getIt.get<NavigationService>().navigatorKey);
@override
Widget build(final BuildContext context) {
final appController = InheritedAppController.of(context);
return BlocAndProviderConfig(
child: MaterialApp.router(
key: appKey,
title: 'SelfPrivacy',
// routing
routeInformationParser: _appRouter.defaultRouteParser(),
routerDelegate: _appRouter.delegate(),
scaffoldMessengerKey:
getIt.get<NavigationService>().scaffoldMessengerKey,
// localization settings
locale: context.locale,
supportedLocales: context.supportedLocales,
localizationsDelegates: context.localizationDelegates,
// theme settings
themeMode: appController.themeMode,
theme: appController.lightTheme,
darkTheme: appController.darkTheme,
// other preferences
debugShowCheckedModeBanner: false,
scrollBehavior:
const MaterialScrollBehavior().copyWith(scrollbars: false),
builder: _builder,
),
);
}
Widget _builder(final BuildContext context, final Widget? widget) {
Widget error = const Center(child: Text('...rendering error...'));
if (widget is Scaffold || widget is Navigator) {
error = Scaffold(body: error);
}
ErrorWidget.builder = (final FlutterErrorDetails errorDetails) => error;
return widget ?? error;
}
}

View file

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

View file

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

View file

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

View file

@ -2,6 +2,7 @@ import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:gap/gap.dart';
import 'package:selfprivacy/config/brand_theme.dart';
import 'package:selfprivacy/logic/bloc/server_jobs/server_jobs_bloc.dart';
import 'package:selfprivacy/logic/cubit/client_jobs/client_jobs_cubit.dart';
@ -63,7 +64,7 @@ class JobsContent extends StatelessWidget {
context.read<ServerInstallationCubit>().state;
if (state is JobsStateEmpty) {
widgets = [
const SizedBox(height: 80),
const Gap(80),
Center(
child: Text(
'jobs.empty'.tr(),
@ -75,12 +76,12 @@ class JobsContent extends StatelessWidget {
if (installationState is ServerInstallationFinished) {
widgets = [
...widgets,
const SizedBox(height: 80),
const Gap(80),
BrandButton.rised(
onPressed: () => context.read<JobsCubit>().upgradeServer(),
text: 'jobs.upgrade_server'.tr(),
),
const SizedBox(height: 10),
const Gap(10),
BrandButton.text(
title: 'jobs.reboot_server'.tr(),
onPressed: () {
@ -189,7 +190,7 @@ class JobsContent extends StatelessWidget {
style:
Theme.of(context).textTheme.labelSmall,
),
const SizedBox(height: 8),
const Gap(8),
LinearProgressIndicator(
value: rebuildJob?.progress == null
? 0.0
@ -206,7 +207,7 @@ class JobsContent extends StatelessWidget {
minHeight: 7.0,
borderRadius: BorderRadius.circular(7.0),
),
const SizedBox(height: 8),
const Gap(8),
if (rebuildJob?.error != null ||
rebuildJob?.result != null ||
rebuildJob?.statusText != null)
@ -282,7 +283,7 @@ class JobsContent extends StatelessWidget {
(final job) => job.uid == state.rebuildJobUid,
);
if (rebuildJob == null) {
return const SizedBox();
return const SizedBox.shrink();
}
return Row(
children: [
@ -322,7 +323,7 @@ class JobsContent extends StatelessWidget {
rebuildJob.description,
style: Theme.of(context).textTheme.labelSmall,
),
const SizedBox(height: 8),
const Gap(8),
LinearProgressIndicator(
value: rebuildJob.progress == null
? 0.0
@ -339,7 +340,7 @@ class JobsContent extends StatelessWidget {
minHeight: 7.0,
borderRadius: BorderRadius.circular(7.0),
),
const SizedBox(height: 8),
const Gap(8),
if (rebuildJob.error != null ||
rebuildJob.result != null ||
rebuildJob.statusText != null)
@ -360,7 +361,7 @@ class JobsContent extends StatelessWidget {
);
},
),
const SizedBox(height: 16),
const Gap(16),
BrandButton.rised(
onPressed: () => context.read<JobsCubit>().acknowledgeFinished(),
text: 'basis.done'.tr(),
@ -403,7 +404,7 @@ class JobsContent extends StatelessWidget {
),
),
),
const SizedBox(width: 8),
const Gap(8),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor:
@ -423,7 +424,7 @@ class JobsContent extends StatelessWidget {
],
),
),
const SizedBox(height: 16),
const Gap(16),
BrandButton.rised(
onPressed: hasBlockingJobs
? null
@ -436,18 +437,18 @@ class JobsContent extends StatelessWidget {
controller: controller,
padding: paddingH15V0,
children: [
const SizedBox(height: 16),
const Gap(16),
Center(
child: Text(
'jobs.title'.tr(),
style: Theme.of(context).textTheme.headlineSmall,
),
),
const SizedBox(height: 20),
const Gap(20),
...widgets,
const SizedBox(height: 8),
const Gap(8),
const Divider(height: 0),
const SizedBox(height: 8),
const Gap(8),
if (serverJobs.isNotEmpty)
Padding(
padding: const EdgeInsets.all(8.0),
@ -489,7 +490,7 @@ class JobsContent extends StatelessWidget {
},
),
),
const SizedBox(height: 24),
const Gap(24),
],
);
},

View file

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

View file

@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:selfprivacy/ui/components/not_ready_card/not_ready_card.dart';
class EmptyPagePlaceholder extends StatelessWidget {
@ -10,50 +11,72 @@ class EmptyPagePlaceholder extends StatelessWidget {
super.key,
});
final bool showReadyCard;
final IconData iconData;
final String title;
final String description;
final IconData iconData;
final bool showReadyCard;
@override
Widget build(final BuildContext context) => !showReadyCard
? _expandedContent(context)
: Column(
Widget build(final BuildContext context) => showReadyCard
? Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Padding(
padding: EdgeInsets.symmetric(horizontal: 15),
child: NotReadyCard(),
),
Expanded(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: Center(
child: _expandedContent(context),
if (showReadyCard)
const Padding(
padding: EdgeInsets.symmetric(
vertical: 15,
horizontal: 15,
),
child: NotReadyCard(),
),
Expanded(
child: _ContentWidget(
iconData: iconData,
title: title,
description: description,
),
),
],
)
: _ContentWidget(
iconData: iconData,
title: title,
description: description,
);
}
Widget _expandedContent(final BuildContext context) => Padding(
class _ContentWidget extends StatelessWidget {
const _ContentWidget({
required this.iconData,
required this.title,
required this.description,
});
final IconData iconData;
final String title;
final String description;
@override
Widget build(final BuildContext context) => Container(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(
iconData,
size: 50,
color: Theme.of(context).colorScheme.onBackground,
),
const SizedBox(height: 16),
const Gap(16),
Text(
title,
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
color: Theme.of(context).colorScheme.onBackground,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
const Gap(8),
Text(
description,
textAlign: TextAlign.center,

View file

@ -127,7 +127,9 @@ class _HeroSliverAppBarState extends State<HeroSliverAppBar> {
Widget build(final BuildContext context) {
final isMobile =
widget.ignoreBreakpoints ? true : Breakpoints.small.isActive(context);
final isJobsListEmpty = context.watch<JobsCubit>().state is JobsStateEmpty;
final isJobsListEmpty = widget.hasFlashButton
? context.watch<JobsCubit>().state is JobsStateEmpty
: true;
return SliverAppBar(
expandedHeight:
widget.hasHeroIcon ? 148.0 + _size.height : 72.0 + _size.height,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -38,8 +38,14 @@ class BackupDetailsPage extends StatelessWidget {
: StateType.uninitialized;
final bool preventActions = backupsState.preventActions;
final List<Backup> backups = backupsState.backups;
final List<Service> services =
context.watch<ServicesBloc>().state.servicesThatCanBeBackedUp;
final List<Service> services = context
.watch<ServicesBloc>()
.state
.servicesThatCanBeBackedUp
.where(
(final service) => service.isEnabled,
)
.toList();
final Duration? autobackupPeriod = backupsState.autobackupPeriod;
final List<ServerJob> backupJobs = context
.watch<ServerJobsBloc>()

View file

@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:selfprivacy/logic/bloc/backups/backups_bloc.dart';
import 'package:selfprivacy/logic/bloc/server_jobs/server_jobs_bloc.dart';
import 'package:selfprivacy/logic/bloc/volumes/volumes_bloc.dart';
import 'package:selfprivacy/logic/models/json/server_job.dart';
import 'package:selfprivacy/logic/models/service.dart';
@ -103,6 +104,29 @@ class _CreateBackupsModalState extends State<CreateBackupsModal> {
...widget.services.map(
(final Service service) {
final bool busy = busyServices.contains(service.id);
final List<Widget> descriptionWidgets = [];
if (busy) {
descriptionWidgets.add(Text('backup.service_busy'.tr()));
} else {
descriptionWidgets.add(
Text(
'service_page.uses'.tr(
namedArgs: {
'usage': service.storageUsage.used.toString(),
'volume': context
.read<VolumesBloc>()
.state
.getVolume(service.storageUsage.volume ?? '')
.displayName,
},
),
style: Theme.of(context).textTheme.labelMedium,
),
);
descriptionWidgets.add(
Text(service.backupDescription),
);
}
return CheckboxListTile.adaptive(
onChanged: !busy
? (final bool? value) {
@ -122,8 +146,9 @@ class _CreateBackupsModalState extends State<CreateBackupsModal> {
title: Text(
service.displayName,
),
subtitle: Text(
busy ? 'backup.service_busy'.tr() : service.backupDescription,
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: descriptionWidgets,
),
secondary: SvgPicture.string(
service.svgIcon,

View file

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

View file

@ -0,0 +1,71 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:gap/gap.dart';
import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart';
import 'package:selfprivacy/ui/pages/more/about_application.dart';
class FailedToInitSecureStorageScreen extends StatelessWidget {
const FailedToInitSecureStorageScreen({
required this.e,
super.key,
});
final PlatformException e;
@override
Widget build(final BuildContext context) => MaterialApp(
home: BrandHeroScreen(
heroIcon: Icons.error_outline,
heroTitle: 'Failed to initialize secure storage',
hasBackButton: false,
children: [
const Text(
'SelfPrivacy requires a secure storage provided by your operating system to encrypt sensitive data, but it failed to initialize.',
),
if (Platform.isLinux)
const Text(
'Please make sure that the libsecret library is installed.',
),
const Gap(16),
Text('Error: ${e.message}'),
const Gap(16),
const Divider(),
const Gap(16),
const LinkListTile(
title: 'Our website',
subtitle: 'selfprivacy.org',
uri: 'https://selfprivacy.org/',
icon: Icons.language_outlined,
),
const LinkListTile(
title: 'Documentation',
subtitle: 'selfprivacy.org/docs',
uri: 'https://selfprivacy.org/docs/',
icon: Icons.library_books_outlined,
),
const LinkListTile(
title: 'Privacy Policy',
subtitle: 'selfprivacy.org/privacy-policy',
uri: 'https://selfprivacy.org/privacy-policy/',
icon: Icons.policy_outlined,
),
const LinkListTile(
title: 'Matrix support chat',
subtitle: '#chat:selfprivacy.org',
uri: 'https://matrix.to/#/#chat:selfprivacy.org',
icon: Icons.question_answer_outlined,
longPressText: '#chat:selfprivacy.org',
),
const LinkListTile(
title: 'Telegram support chat',
subtitle: '@selfprivacy_chat',
uri: 'https://t.me/selfprivacy_chat',
icon: Icons.question_answer_outlined,
longPressText: '@selfprivacy_chat',
),
],
),
);
}

View file

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

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