Merge branch 'update_description' of git.selfprivacy.org:SelfPrivacy/selfprivacy.org.app into update_description
|
@ -0,0 +1,68 @@
|
||||||
|
name: Bug report
|
||||||
|
about: File a bug report
|
||||||
|
labels:
|
||||||
|
- Bug
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Thanks for taking the time to fill out this bug report! Please provide a short but a descriptive title for your issue.
|
||||||
|
- type: textarea
|
||||||
|
id: expected-behaviour
|
||||||
|
attributes:
|
||||||
|
label: Expected Behavior
|
||||||
|
description: What did you expect to happen?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: actual-behaviour
|
||||||
|
attributes:
|
||||||
|
label: Actual Behavior
|
||||||
|
description: What actually happened?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: steps-to-reproduce
|
||||||
|
attributes:
|
||||||
|
label: Steps to Reproduce
|
||||||
|
description: What steps can we follow to reproduce this issue?
|
||||||
|
placeholder: |
|
||||||
|
1. First step
|
||||||
|
2. Second step
|
||||||
|
3. and so on...
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: context
|
||||||
|
attributes:
|
||||||
|
label: Context and notes
|
||||||
|
description: Additional information about environment or what were you trying to do. If you have an idea how to fix this issue, please describe it here too.
|
||||||
|
- type: textarea
|
||||||
|
id: logs
|
||||||
|
attributes:
|
||||||
|
label: Relevant log output
|
||||||
|
description: Please copy and paste any relevant log output, if you have any. This will be automatically formatted into code, so no need for backticks.
|
||||||
|
render: shell
|
||||||
|
- type: input
|
||||||
|
id: app-version
|
||||||
|
attributes:
|
||||||
|
label: App Version
|
||||||
|
description: What version of SelfPrivacy app are you running? You can find it in the "About" section of the app.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
id: api-version
|
||||||
|
attributes:
|
||||||
|
label: Server API Version
|
||||||
|
description: What version of SelfPrivacy API are you running? You can find it in the "About" section of the app. Leave it empty if your app is not connected to the server yet.
|
||||||
|
- type: dropdown
|
||||||
|
id: os
|
||||||
|
attributes:
|
||||||
|
label: Operating System
|
||||||
|
description: What operating system are you using?
|
||||||
|
options:
|
||||||
|
- Android
|
||||||
|
- iOS
|
||||||
|
- Linux
|
||||||
|
- macOS
|
||||||
|
- Windows
|
|
@ -0,0 +1,23 @@
|
||||||
|
name: Feature request
|
||||||
|
about: Suggest an idea for this project
|
||||||
|
label:
|
||||||
|
- Feature request
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Thanks for taking the time to fill out this feature request! Please provide a short but a descriptive title for your issue.
|
||||||
|
- type: textarea
|
||||||
|
id: description
|
||||||
|
attributes:
|
||||||
|
label: Description
|
||||||
|
description: Describe the feature you'd like to see.
|
||||||
|
placeholder: |
|
||||||
|
As a user, I want to be able to...
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: context
|
||||||
|
attributes:
|
||||||
|
label: Context and notes
|
||||||
|
description: Additional information about environment and what were you trying to do. If you have an idea how to implement this feature, please describe it here too.
|
|
@ -0,0 +1,29 @@
|
||||||
|
name: Translation issue
|
||||||
|
about: File a translation (localization) issue
|
||||||
|
labels:
|
||||||
|
- Translations
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Translations can be modified and discussed on [Weblate](https://weblate.selfprivacy.org/projects/selfprivacy/). You can fix the mistranslation issue yourself there. Using the search, you can also find the string ID of the mistranslated string. If your issue is more complex, please file it here
|
||||||
|
|
||||||
|
If you are a member of SelfPrivacy core team, you **must** fix the issue yourself on Weblate.
|
||||||
|
- type: input
|
||||||
|
id: language
|
||||||
|
attributes:
|
||||||
|
label: Language
|
||||||
|
description: What language is affected?
|
||||||
|
placeholder: |
|
||||||
|
English
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: description
|
||||||
|
attributes:
|
||||||
|
label: Description
|
||||||
|
description: Describe the issue in detail. If you have an idea how to fix this issue, please describe it here too. Include the string ID of the mistranslated string, if possible.
|
||||||
|
placeholder: |
|
||||||
|
The string `string.id` is translated as "foo", but it should be "bar".
|
||||||
|
validations:
|
||||||
|
required: true
|
|
@ -10,7 +10,7 @@ AppDir:
|
||||||
id: org.selfprivacy.app
|
id: org.selfprivacy.app
|
||||||
name: SelfPrivacy
|
name: SelfPrivacy
|
||||||
icon: org.selfprivacy.app
|
icon: org.selfprivacy.app
|
||||||
version: 0.8.0
|
version: 0.9.1
|
||||||
exec: selfprivacy
|
exec: selfprivacy
|
||||||
exec_args: $@
|
exec_args: $@
|
||||||
apt:
|
apt:
|
||||||
|
|
|
@ -123,7 +123,6 @@
|
||||||
"disk": "Disk",
|
"disk": "Disk",
|
||||||
"monthly_cost": "Aylıq xərc",
|
"monthly_cost": "Aylıq xərc",
|
||||||
"location": "Yerləşdirmə",
|
"location": "Yerləşdirmə",
|
||||||
"provider": "Provayder",
|
|
||||||
"core_count": {
|
"core_count": {
|
||||||
"one": "{} nüvəs",
|
"one": "{} nüvəs",
|
||||||
"two": "{} nüvələr",
|
"two": "{} nüvələr",
|
||||||
|
@ -300,7 +299,6 @@
|
||||||
"manage_domain_dns": "Domeninizin DNS-ni idarə etmək üçün",
|
"manage_domain_dns": "Domeninizin DNS-ni idarə etmək üçün",
|
||||||
"use_this_domain": "Biz bu domendən istifadə edirik?",
|
"use_this_domain": "Biz bu domendən istifadə edirik?",
|
||||||
"use_this_domain_text": "Göstərdiyiniz token bu domen üzərində nəzarəti təmin edir",
|
"use_this_domain_text": "Göstərdiyiniz token bu domen üzərində nəzarəti təmin edir",
|
||||||
"cloudflare_api_token": "CloudFlare API Açarı",
|
|
||||||
"connect_backblaze_storage": "Backblaze bulud yaddaşınızı birləşdirin",
|
"connect_backblaze_storage": "Backblaze bulud yaddaşınızı birləşdirin",
|
||||||
"no_connected_domains": "Hazırda heç bir bağlı domen yoxdur",
|
"no_connected_domains": "Hazırda heç bir bağlı domen yoxdur",
|
||||||
"loading_domain_list": "Domenlərin siyahısı yüklənir",
|
"loading_domain_list": "Domenlərin siyahısı yüklənir",
|
||||||
|
@ -500,4 +498,4 @@
|
||||||
"reset_onboarding_description": "Enerji ekranını yenidən göstərmək üçün güc açarının sıfırlanması",
|
"reset_onboarding_description": "Enerji ekranını yenidən göstərmək üçün güc açarının sıfırlanması",
|
||||||
"cubit_statuses": "Yükləmə kubitlərinin cari vəziyyəti"
|
"cubit_statuses": "Yükləmə kubitlərinin cari vəziyyəti"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,7 +32,6 @@
|
||||||
"manage_domain_dns": "Для кіравання DNS вашага дамена",
|
"manage_domain_dns": "Для кіравання DNS вашага дамена",
|
||||||
"use_this_domain": "Ужываем гэты дамен?",
|
"use_this_domain": "Ужываем гэты дамен?",
|
||||||
"use_this_domain_text": "Указаны вамі токен дае кантроль над гэтым даменам",
|
"use_this_domain_text": "Указаны вамі токен дае кантроль над гэтым даменам",
|
||||||
"cloudflare_api_token": "API ключ DNS правайдэра",
|
|
||||||
"connect_backblaze_storage": "Падлучыце хмарнае сховішча Backblaze",
|
"connect_backblaze_storage": "Падлучыце хмарнае сховішча Backblaze",
|
||||||
"no_connected_domains": "У дадзены момант падлучаных даменаў няма",
|
"no_connected_domains": "У дадзены момант падлучаных даменаў няма",
|
||||||
"loading_domain_list": "Загружаем спіс даменаў",
|
"loading_domain_list": "Загружаем спіс даменаў",
|
||||||
|
@ -425,7 +424,6 @@
|
||||||
"disk": "Дыск",
|
"disk": "Дыск",
|
||||||
"monthly_cost": "Штомесячны кошт",
|
"monthly_cost": "Штомесячны кошт",
|
||||||
"location": "Размяшчэнне",
|
"location": "Размяшчэнне",
|
||||||
"provider": "Правайдэр",
|
|
||||||
"core_count": {
|
"core_count": {
|
||||||
"one": "{} ядро",
|
"one": "{} ядро",
|
||||||
"two": "{} ядра",
|
"two": "{} ядра",
|
||||||
|
@ -510,4 +508,4 @@
|
||||||
"support": {
|
"support": {
|
||||||
"title": "Падтрымка SelfPrivacy"
|
"title": "Падтрымка SelfPrivacy"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -118,7 +118,6 @@
|
||||||
"disk": "Místní disk",
|
"disk": "Místní disk",
|
||||||
"monthly_cost": "Měsíční náklady",
|
"monthly_cost": "Měsíční náklady",
|
||||||
"location": "Umístění",
|
"location": "Umístění",
|
||||||
"provider": "Poskytovatel",
|
|
||||||
"core_count": {
|
"core_count": {
|
||||||
"two": "{} jádra",
|
"two": "{} jádra",
|
||||||
"few": "{} jádra",
|
"few": "{} jádra",
|
||||||
|
@ -144,7 +143,6 @@
|
||||||
"found_more_domains": "Nalezeno více než jedna doména. V zájmu vlastní bezpečnosti vás prosíme o odstranění nepotřebných domén",
|
"found_more_domains": "Nalezeno více než jedna doména. V zájmu vlastní bezpečnosti vás prosíme o odstranění nepotřebných domén",
|
||||||
"server_created": "Vytvořený server. Probíhá kontrola DNS a spouštění serveru…",
|
"server_created": "Vytvořený server. Probíhá kontrola DNS a spouštění serveru…",
|
||||||
"choose_server_type_notice": "Důležité je zaměřit se na procesor a paměť RAM. Data vašich služeb budou uložena na připojeném svazku, který lze snadno rozšířit a za který se platí zvlášť.",
|
"choose_server_type_notice": "Důležité je zaměřit se na procesor a paměť RAM. Data vašich služeb budou uložena na připojeném svazku, který lze snadno rozšířit a za který se platí zvlášť.",
|
||||||
"cloudflare_api_token": "Klíč API poskytovatele DNS",
|
|
||||||
"connect_backblaze_storage": "Připojení úložiště Backblaze",
|
"connect_backblaze_storage": "Připojení úložiště Backblaze",
|
||||||
"save_domain": "Uložit doménu",
|
"save_domain": "Uložit doménu",
|
||||||
"final": "Závěrečný krok",
|
"final": "Závěrečný krok",
|
||||||
|
@ -510,4 +508,4 @@
|
||||||
"ignore_tls": "Nekontrolujte certifikáty TLS",
|
"ignore_tls": "Nekontrolujte certifikáty TLS",
|
||||||
"ignore_tls_description": "Aplikace nebude při připojování k serveru ověřovat certifikáty TLS."
|
"ignore_tls_description": "Aplikace nebude při připojování k serveru ověřovat certifikáty TLS."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -123,7 +123,6 @@
|
||||||
"disk": "Festplatte",
|
"disk": "Festplatte",
|
||||||
"monthly_cost": "Monatliche Kosten",
|
"monthly_cost": "Monatliche Kosten",
|
||||||
"location": "Standort",
|
"location": "Standort",
|
||||||
"provider": "Provider",
|
|
||||||
"core_count": {
|
"core_count": {
|
||||||
"one": "{} Kern",
|
"one": "{} Kern",
|
||||||
"two": "{} Kerne",
|
"two": "{} Kerne",
|
||||||
|
@ -278,7 +277,6 @@
|
||||||
"manage_domain_dns": "Zum Verwalten des DNS Ihrer Domain",
|
"manage_domain_dns": "Zum Verwalten des DNS Ihrer Domain",
|
||||||
"use_this_domain": "Diese Domäne verwenden?",
|
"use_this_domain": "Diese Domäne verwenden?",
|
||||||
"use_this_domain_text": "Das von Ihnen bereitgestellte Token gewährt Zugriff auf die folgende Domäne",
|
"use_this_domain_text": "Das von Ihnen bereitgestellte Token gewährt Zugriff auf die folgende Domäne",
|
||||||
"cloudflare_api_token": "API-Schlüssel des DNS-Anbieters",
|
|
||||||
"connect_backblaze_storage": "Backblaze-Speicher verbinden",
|
"connect_backblaze_storage": "Backblaze-Speicher verbinden",
|
||||||
"no_connected_domains": "Derzeit keine verbundenen Domains",
|
"no_connected_domains": "Derzeit keine verbundenen Domains",
|
||||||
"loading_domain_list": "Domänenliste wird geladen",
|
"loading_domain_list": "Domänenliste wird geladen",
|
||||||
|
@ -510,4 +508,4 @@
|
||||||
"ignore_tls": "Überprüfen Sie keine TLS-Zertifikate",
|
"ignore_tls": "Überprüfen Sie keine TLS-Zertifikate",
|
||||||
"ignore_tls_description": "Die Anwendung validiert TLS-Zertifikate nicht, wenn sie eine Verbindung zum Server herstellt."
|
"ignore_tls_description": "Die Anwendung validiert TLS-Zertifikate nicht, wenn sie eine Verbindung zum Server herstellt."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,7 +35,8 @@
|
||||||
"done": "Done",
|
"done": "Done",
|
||||||
"continue": "Continue",
|
"continue": "Continue",
|
||||||
"alert": "Alert",
|
"alert": "Alert",
|
||||||
"copied_to_clipboard": "Copied to clipboard!"
|
"copied_to_clipboard": "Copied to clipboard!",
|
||||||
|
"please_connect": "Please connect your server, domain and DNS provider to dive in!"
|
||||||
},
|
},
|
||||||
"more_page": {
|
"more_page": {
|
||||||
"configuration_wizard": "Setup wizard",
|
"configuration_wizard": "Setup wizard",
|
||||||
|
@ -82,7 +83,7 @@
|
||||||
"no_key_name": "Unnamed key",
|
"no_key_name": "Unnamed key",
|
||||||
"root_title": "These are superuser keys",
|
"root_title": "These are superuser keys",
|
||||||
"root_subtitle": "Owners of these keys get full access to the server and can do anything on it. Only add your own keys to the server.",
|
"root_subtitle": "Owners of these keys get full access to the server and can do anything on it. Only add your own keys to the server.",
|
||||||
"input_label": "Public ED25519 or RSA key"
|
"input_label": "Public ED25519, ECDSA or RSA key"
|
||||||
},
|
},
|
||||||
"onboarding": {
|
"onboarding": {
|
||||||
"page1_title": "Digital independence, available to all of us",
|
"page1_title": "Digital independence, available to all of us",
|
||||||
|
@ -124,6 +125,7 @@
|
||||||
"disk": "Disk local",
|
"disk": "Disk local",
|
||||||
"monthly_cost": "Monthly cost",
|
"monthly_cost": "Monthly cost",
|
||||||
"location": "Location",
|
"location": "Location",
|
||||||
|
"pricing_error": "Couldn't fetch provider prices",
|
||||||
"server_provider": "Server Provider",
|
"server_provider": "Server Provider",
|
||||||
"dns_provider": "DNS Provider",
|
"dns_provider": "DNS Provider",
|
||||||
"core_count": {
|
"core_count": {
|
||||||
|
@ -219,7 +221,73 @@
|
||||||
"snapshot_modal_inplace_option_title": "Replace in place",
|
"snapshot_modal_inplace_option_title": "Replace in place",
|
||||||
"snapshot_modal_inplace_option_description": "Less free space needed, but more risk. Replaces current data with the snapshot data during the download.",
|
"snapshot_modal_inplace_option_description": "Less free space needed, but more risk. Replaces current data with the snapshot data during the download.",
|
||||||
"snapshot_modal_service_not_found": "This is a snapshot of a service you don't have on your server anymore. Usually this shouldn't happen, and we cannot do the automatic restore. You can still download the snapshot and restore it manually. Contact SelfPrivacy support if you need help.",
|
"snapshot_modal_service_not_found": "This is a snapshot of a service you don't have on your server anymore. Usually this shouldn't happen, and we cannot do the automatic restore. You can still download the snapshot and restore it manually. Contact SelfPrivacy support if you need help.",
|
||||||
"restore_started": "Restore started, check the jobs list for the current status"
|
"restore_started": "Restore started, check the jobs list for the current status",
|
||||||
|
"snapshot_reason_title": "Creation reason",
|
||||||
|
"snapshot_reasons": {
|
||||||
|
"auto": "Created automatically",
|
||||||
|
"explicit": "Created by your explicit request",
|
||||||
|
"pre_restore": "Created as a precaution before risky restore",
|
||||||
|
"unknown": "Unknown"
|
||||||
|
},
|
||||||
|
"rotation_quotas_title": "Snapshot rotation settings",
|
||||||
|
"set_rotation_quotas": "Set new rotation quotas",
|
||||||
|
"quotas_set": "New backup rotation quotas set",
|
||||||
|
"quotas_only_applied_to_autobackups": "These settings are only applied to automatic backups. Manual backups won't get deleted.",
|
||||||
|
"quota_titles": {
|
||||||
|
"last": "How many latest backups to keep",
|
||||||
|
"daily": "How many daily backups to keep",
|
||||||
|
"weekly": "How many weekly backups to keep",
|
||||||
|
"monthly": "How many monthly backups to keep",
|
||||||
|
"yearly": "How many yearly backups to keep"
|
||||||
|
},
|
||||||
|
"quota_subtitles": {
|
||||||
|
"no_effect": "This rule has no effect because another rule will keep more backups",
|
||||||
|
"last": {
|
||||||
|
"zero": "Rule is disabled",
|
||||||
|
"one": "Last {} backup will be kept regardless of its age",
|
||||||
|
"two": "Last {} backups will be kept regardless of their age",
|
||||||
|
"few": "Last {} backups will be kept regardless of their age",
|
||||||
|
"many": "Last {} backups will be kept regardless of their age",
|
||||||
|
"other": "Last {} backups will be kept regardless of their age"
|
||||||
|
},
|
||||||
|
"last_infinite": "All backups will be kept",
|
||||||
|
"daily": {
|
||||||
|
"zero": "Rule is disabled",
|
||||||
|
"one": "Last {} daily backup will be kept",
|
||||||
|
"two": "Last {} daily backups will be kept",
|
||||||
|
"few": "Last {} daily backups will be kept",
|
||||||
|
"many": "Last {} daily backups will be kept",
|
||||||
|
"other": "Last {} daily backups will be kept"
|
||||||
|
},
|
||||||
|
"daily_infinite": "All daily backups will be kept",
|
||||||
|
"weekly": {
|
||||||
|
"zero": "Rule is disabled",
|
||||||
|
"one": "Last {} weekly backup will be kept",
|
||||||
|
"two": "Last {} weekly backups will be kept",
|
||||||
|
"few": "Last {} weekly backups will be kept",
|
||||||
|
"many": "Last {} weekly backups will be kept",
|
||||||
|
"other": "Last {} weekly backups will be kept"
|
||||||
|
},
|
||||||
|
"weekly_infinite": "All weekly backups will be kept",
|
||||||
|
"monthly": {
|
||||||
|
"zero": "Rule is disabled",
|
||||||
|
"one": "Last {} monthly backup will be kept",
|
||||||
|
"two": "Last {} monthly backups will be kept",
|
||||||
|
"few": "Last {} monthly backups will be kept",
|
||||||
|
"many": "Last {} monthly backups will be kept",
|
||||||
|
"other": "Last {} monthly backups will be kept"
|
||||||
|
},
|
||||||
|
"monthly_infinite": "All monthly backups will be kept",
|
||||||
|
"yearly": {
|
||||||
|
"zero": "Rule is disabled",
|
||||||
|
"one": "Last {} yearly backup will be kept",
|
||||||
|
"two": "Last {} yearly backups will be kept",
|
||||||
|
"few": "Last {} yearly backups will be kept",
|
||||||
|
"many": "Last {} yearly backups will be kept",
|
||||||
|
"other": "Last {} yearly backups will be kept"
|
||||||
|
},
|
||||||
|
"yearly_infinite": "All yearly backups will be kept"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"storage": {
|
"storage": {
|
||||||
"card_title": "Server Storage",
|
"card_title": "Server Storage",
|
||||||
|
@ -236,7 +304,9 @@
|
||||||
"extending_volume_description": "Resizing volume will allow you to store more data on your server without extending the server itself. Volume can only be extended: shrinking is not possible.",
|
"extending_volume_description": "Resizing volume will allow you to store more data on your server without extending the server itself. Volume can only be extended: shrinking is not possible.",
|
||||||
"extending_volume_price_info": "Price includes VAT and is estimated from pricing data provided by your server provider. Server will be rebooted after resizing.",
|
"extending_volume_price_info": "Price includes VAT and is estimated from pricing data provided by your server provider. Server will be rebooted after resizing.",
|
||||||
"extending_volume_error": "Couldn't initialize volume extending.",
|
"extending_volume_error": "Couldn't initialize volume extending.",
|
||||||
|
"extending_volume_modal_description": "Upgrade to {} for {} plan per month.",
|
||||||
"size": "Size",
|
"size": "Size",
|
||||||
|
"price": "Price",
|
||||||
"data_migration_title": "Data migration",
|
"data_migration_title": "Data migration",
|
||||||
"data_migration_notice": "During migration all services will be turned off.",
|
"data_migration_notice": "During migration all services will be turned off.",
|
||||||
"start_migration_button": "Start migration",
|
"start_migration_button": "Start migration",
|
||||||
|
@ -247,6 +317,7 @@
|
||||||
"in_menu": "Server is not set up yet. Please finish setup using setup wizard for further work."
|
"in_menu": "Server is not set up yet. Please finish setup using setup wizard for further work."
|
||||||
},
|
},
|
||||||
"service_page": {
|
"service_page": {
|
||||||
|
"nothing_here": "Nothing here",
|
||||||
"open_in_browser": "Open in browser",
|
"open_in_browser": "Open in browser",
|
||||||
"restart": "Restart service",
|
"restart": "Restart service",
|
||||||
"disable": "Disable service",
|
"disable": "Disable service",
|
||||||
|
@ -303,7 +374,6 @@
|
||||||
"add_new_user": "Add a first user",
|
"add_new_user": "Add a first user",
|
||||||
"new_user": "New user",
|
"new_user": "New user",
|
||||||
"delete_user": "Delete user",
|
"delete_user": "Delete user",
|
||||||
"not_ready": "Please connect server, domain and DNS in the Providers tab, to be able to add a first user",
|
|
||||||
"nobody_here": "Nobody here",
|
"nobody_here": "Nobody here",
|
||||||
"login": "Login",
|
"login": "Login",
|
||||||
"new_user_info_note": "New user will automatically be granted an access to all of the services",
|
"new_user_info_note": "New user will automatically be granted an access to all of the services",
|
||||||
|
@ -357,6 +427,9 @@
|
||||||
"choose_server_type_ram": "{} GB of RAM",
|
"choose_server_type_ram": "{} GB of RAM",
|
||||||
"choose_server_type_storage": "{} GB of system storage",
|
"choose_server_type_storage": "{} GB of system storage",
|
||||||
"choose_server_type_payment_per_month": "{} per month",
|
"choose_server_type_payment_per_month": "{} per month",
|
||||||
|
"choose_server_type_payment_server": "{} for the server",
|
||||||
|
"choose_server_type_payment_storage": "{} for additional storage",
|
||||||
|
"choose_server_type_payment_ip": "{} for the public IPv4 address",
|
||||||
"no_server_types_found": "No available server types found. Make sure your account is accessible and try to change your server location.",
|
"no_server_types_found": "No available server types found. Make sure your account is accessible and try to change your server location.",
|
||||||
"dns_provider_bad_key_error": "API key is invalid",
|
"dns_provider_bad_key_error": "API key is invalid",
|
||||||
"backblaze_bad_key_error": "Backblaze storage information is invalid",
|
"backblaze_bad_key_error": "Backblaze storage information is invalid",
|
||||||
|
@ -366,7 +439,8 @@
|
||||||
"manage_domain_dns": "To manage your domain's DNS",
|
"manage_domain_dns": "To manage your domain's DNS",
|
||||||
"use_this_domain": "Use this domain?",
|
"use_this_domain": "Use this domain?",
|
||||||
"use_this_domain_text": "The token you provided gives access to the following domain",
|
"use_this_domain_text": "The token you provided gives access to the following domain",
|
||||||
"cloudflare_api_token": "DNS Provider API Token",
|
"multiple_domains_found": "Multiple domains found",
|
||||||
|
"multiple_domains_found_text": "The token you provided gives access to the following domains. Please select the one you want to use. For the security of your other domains, you should restrict this token's access to only the domain you want to use with SelfPrivacy.",
|
||||||
"connect_backblaze_storage": "Connect Backblaze storage",
|
"connect_backblaze_storage": "Connect Backblaze storage",
|
||||||
"no_connected_domains": "No connected domains at the moment",
|
"no_connected_domains": "No connected domains at the moment",
|
||||||
"loading_domain_list": "Loading domain list",
|
"loading_domain_list": "Loading domain list",
|
||||||
|
@ -554,4 +628,4 @@
|
||||||
"reset_onboarding_description": "Reset onboarding switch to show onboarding screen again",
|
"reset_onboarding_description": "Reset onboarding switch to show onboarding screen again",
|
||||||
"cubit_statuses": "Cubit loading statuses"
|
"cubit_statuses": "Cubit loading statuses"
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -111,7 +111,6 @@
|
||||||
"disk": "Disque local",
|
"disk": "Disque local",
|
||||||
"monthly_cost": "Coût mensuel",
|
"monthly_cost": "Coût mensuel",
|
||||||
"location": "Localisation",
|
"location": "Localisation",
|
||||||
"provider": "Fournisseur",
|
|
||||||
"core_count": {
|
"core_count": {
|
||||||
"one": "{} cœur",
|
"one": "{} cœur",
|
||||||
"two": "{} cœurs",
|
"two": "{} cœurs",
|
||||||
|
@ -286,4 +285,4 @@
|
||||||
"title": "Serveur VPN",
|
"title": "Serveur VPN",
|
||||||
"subtitle": "Serveur VPN privé"
|
"subtitle": "Serveur VPN privé"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -121,7 +121,6 @@
|
||||||
"disk": "Disks lokāls",
|
"disk": "Disks lokāls",
|
||||||
"monthly_cost": "Mēneša maksa",
|
"monthly_cost": "Mēneša maksa",
|
||||||
"location": "Vieta",
|
"location": "Vieta",
|
||||||
"provider": "Sniedzējs",
|
|
||||||
"core_count": {
|
"core_count": {
|
||||||
"one": "{} kodols",
|
"one": "{} kodols",
|
||||||
"two": "{} kodoli",
|
"two": "{} kodoli",
|
||||||
|
|
|
@ -118,7 +118,6 @@
|
||||||
"disk": "Dysk lokalny",
|
"disk": "Dysk lokalny",
|
||||||
"monthly_cost": "Koszt miesięczny",
|
"monthly_cost": "Koszt miesięczny",
|
||||||
"location": "Lokalizacja danych",
|
"location": "Lokalizacja danych",
|
||||||
"provider": "Dostawca",
|
|
||||||
"core_count": {
|
"core_count": {
|
||||||
"one": "{} jądro",
|
"one": "{} jądro",
|
||||||
"two": "{} jądra",
|
"two": "{} jądra",
|
||||||
|
@ -321,7 +320,6 @@
|
||||||
"choose_server_type_payment_per_month": "{} miesięcznie",
|
"choose_server_type_payment_per_month": "{} miesięcznie",
|
||||||
"no_server_types_found": "Nie znaleziono dostępnych typów serwerów! Proszę upewnić się, że masz dostęp do dostawcy serwera...",
|
"no_server_types_found": "Nie znaleziono dostępnych typów serwerów! Proszę upewnić się, że masz dostęp do dostawcy serwera...",
|
||||||
"use_this_domain": "Kto używa ten domen?",
|
"use_this_domain": "Kto używa ten domen?",
|
||||||
"cloudflare_api_token": "Klucz API dostawcy DNS",
|
|
||||||
"connect_backblaze_storage": "Dodajcie Blackblaze",
|
"connect_backblaze_storage": "Dodajcie Blackblaze",
|
||||||
"no_connected_domains": "Niema podłączonych domenów",
|
"no_connected_domains": "Niema podłączonych domenów",
|
||||||
"what": "Co to znaczy?",
|
"what": "Co to znaczy?",
|
||||||
|
@ -509,4 +507,4 @@
|
||||||
"cubit_statuses": "Aktualny stan qubitów ładujących",
|
"cubit_statuses": "Aktualny stan qubitów ładujących",
|
||||||
"ignore_tls": "Używane podczas konfigurowania nowego serwera."
|
"ignore_tls": "Używane podczas konfigurowania nowego serwera."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -68,8 +68,8 @@
|
||||||
"reset_config_description": "Сбросить API ключи и root пользователя.",
|
"reset_config_description": "Сбросить API ключи и root пользователя.",
|
||||||
"delete_server_title": "Удалить сервер",
|
"delete_server_title": "Удалить сервер",
|
||||||
"delete_server_description": "Действие приведёт к удалению сервера. После этого он будет недоступен.",
|
"delete_server_description": "Действие приведёт к удалению сервера. После этого он будет недоступен.",
|
||||||
"system_dark_theme_title": "Системная тема по-умолчанию",
|
"system_dark_theme_title": "Системная тема",
|
||||||
"system_dark_theme_description": "Используйте светлую или темную темы в зависимости от системных настроек",
|
"system_dark_theme_description": "Будет использована светлая или тёмная тема в зависимости от системных настроек",
|
||||||
"dangerous_settings": "Опасные настройки"
|
"dangerous_settings": "Опасные настройки"
|
||||||
},
|
},
|
||||||
"ssh": {
|
"ssh": {
|
||||||
|
@ -124,14 +124,16 @@
|
||||||
"disk": "Диск",
|
"disk": "Диск",
|
||||||
"monthly_cost": "Ежемесячная стоимость",
|
"monthly_cost": "Ежемесячная стоимость",
|
||||||
"location": "Размещение",
|
"location": "Размещение",
|
||||||
"provider": "Провайдер",
|
"pricing_error": "Не удалось получить цены провайдера",
|
||||||
"core_count": {
|
"core_count": {
|
||||||
"one": "{} ядро",
|
"one": "{} ядро",
|
||||||
"two": "{} ядра",
|
"two": "{} ядра",
|
||||||
"few": "{} ядра",
|
"few": "{} ядра",
|
||||||
"many": "{} ядер",
|
"many": "{} ядер",
|
||||||
"other": "{} ядер"
|
"other": "{} ядер"
|
||||||
}
|
},
|
||||||
|
"server_provider": "Провайдер сервера",
|
||||||
|
"dns_provider": "Провайдер DNS"
|
||||||
},
|
},
|
||||||
"record": {
|
"record": {
|
||||||
"root": "Корневой домен",
|
"root": "Корневой домен",
|
||||||
|
@ -181,7 +183,7 @@
|
||||||
"reupload_key_subtitle": "Ещё раз проинициализирует хранилище резервных копий. Используйте, если что-то сломалось.",
|
"reupload_key_subtitle": "Ещё раз проинициализирует хранилище резервных копий. Используйте, если что-то сломалось.",
|
||||||
"service_busy": "Сейчас создаются другие резервные копии",
|
"service_busy": "Сейчас создаются другие резервные копии",
|
||||||
"autobackup_period_never": "Автоматическое копирование отключено",
|
"autobackup_period_never": "Автоматическое копирование отключено",
|
||||||
"pending_jobs": "Активные задачи на копирование",
|
"pending_jobs": "Активные задачи резервного копирования",
|
||||||
"card_subtitle": "Управляйте резервными копиями",
|
"card_subtitle": "Управляйте резервными копиями",
|
||||||
"refetch_backups_subtitle": "Сбросить кэш и запросить данные у провайдера. Может повлечь дополнительные расходы.",
|
"refetch_backups_subtitle": "Сбросить кэш и запросить данные у провайдера. Может повлечь дополнительные расходы.",
|
||||||
"select_all": "Копировать всё",
|
"select_all": "Копировать всё",
|
||||||
|
@ -200,7 +202,91 @@
|
||||||
"autobackup_period_set": "Период установлен",
|
"autobackup_period_set": "Период установлен",
|
||||||
"backups_encryption_key": "Ключ шифрования",
|
"backups_encryption_key": "Ключ шифрования",
|
||||||
"snapshots_title": "Список снимков",
|
"snapshots_title": "Список снимков",
|
||||||
"forget_snapshot_error": "Не удалось забыть снимок"
|
"forget_snapshot_error": "Не удалось забыть снимок",
|
||||||
|
"backups_encryption_key_not_found": "Ключ шифрования пока не найден, повторите попытку позже.",
|
||||||
|
"forget_snapshot_alert": "Вы уверены что хотите удалить этот снимок? Это действие обычно нельзя отменить.",
|
||||||
|
"snapshot_modal_select_strategy": "Выберите стратегию восстановления",
|
||||||
|
"snapshot_modal_download_verify_option_description": "Меньше риск, но требуется больше свободного места. Загрузка всей резервной копии во временное хранилище, проверка целостности копии, и последующая замена текущих данных.",
|
||||||
|
"snapshot_modal_service_not_found": "Это снимок сервиса, которого больше нет на вашем сервере. Обычно этого не должно происходить, и мы не сможем выполнить автоматическое восстановление. Вы можете загрузить снимок и восстановить его вручную. Обратитесь в службу поддержки SelfPrivacy, если вам нужна помощь.",
|
||||||
|
"backups_encryption_key_subtitle": "Храните его в безопасном месте.",
|
||||||
|
"backups_encryption_key_copy": "Скопируйте ключ шифрования",
|
||||||
|
"backups_encryption_key_show": "Показать ключ шифрования",
|
||||||
|
"backups_encryption_key_description": "Этот ключ используется для шифрования резервных копий. Если вы его потеряете, то не сможете восстановить данные из резервной копии. Храните его в надежном месте. Он может пригодиться, если придётся восстанавливать данные вручную.",
|
||||||
|
"forget_snapshot": "Забудьте о моментальном снимке",
|
||||||
|
"snapshot_modal_heading": "Сведения о снимке",
|
||||||
|
"snapshot_service_title": "Сервис",
|
||||||
|
"snapshot_creation_time_title": "Время создания",
|
||||||
|
"snapshot_id_title": "ID снимка",
|
||||||
|
"snapshot_modal_download_verify_option_title": "Загрузить, проверить, и затем заменить",
|
||||||
|
"snapshot_modal_inplace_option_title": "Заменить на месте",
|
||||||
|
"snapshot_modal_inplace_option_description": "Требуется меньше свободного места, но выше риск. При загрузке данных из резервной копии заменяют текущие данные сразу.",
|
||||||
|
"restore_started": "Восстановление началось, проверьте текущий статус в списке заданий",
|
||||||
|
"quota_subtitles": {
|
||||||
|
"no_effect": "Это правило не имеет эффекта, так ак перекрыто другим правилом",
|
||||||
|
"last": {
|
||||||
|
"two": "Последние {} снимка будут сохраняться вне зависимости от даты создания",
|
||||||
|
"many": "Последние {} снимков будут сохраняться вне зависимости от даты создания",
|
||||||
|
"other": "Последние {} снимков будут сохраняться вне зависимости от даты создания",
|
||||||
|
"zero": "Правило отключено",
|
||||||
|
"one": "Последний {} снимок будет сохраняться вне зависимости от даты создания",
|
||||||
|
"few": "Последние {} снимка будут сохраняться вне зависимости от даты создания"
|
||||||
|
},
|
||||||
|
"daily": {
|
||||||
|
"two": "Последние {} ежедневных снимка будут сохраняться",
|
||||||
|
"other": "Последние {} ежедневных снимков будут сохраняться",
|
||||||
|
"zero": "Правило отключено",
|
||||||
|
"one": "Последний {} ежедневный снимок будет сохраняться",
|
||||||
|
"few": "Последние {} ежедневных снимка будут сохраняться",
|
||||||
|
"many": "Последние {} ежедневных снимков будут сохраняться"
|
||||||
|
},
|
||||||
|
"weekly": {
|
||||||
|
"two": "Последние {} еженедельных снимка будут сохраняться",
|
||||||
|
"other": "Последние {} еженедельных снимков будут сохраняться",
|
||||||
|
"zero": "Правило отключено",
|
||||||
|
"one": "Последний {} еженедельный снимок будет сохраняться",
|
||||||
|
"few": "Последние {} еженедельных снимка будут сохраняться",
|
||||||
|
"many": "Последние {} еженедельных снимков будут сохраняться"
|
||||||
|
},
|
||||||
|
"monthly": {
|
||||||
|
"two": "Последние {} ежемесячных снимка будут сохраняться",
|
||||||
|
"other": "Последние {} ежемесячных снимков будут сохраняться",
|
||||||
|
"zero": "Правило отключено",
|
||||||
|
"one": "Последний {} ежемесячный снимок будет сохраняться",
|
||||||
|
"few": "Последние {} ежемесячных снимка будут сохраняться",
|
||||||
|
"many": "Последние {} ежемесячных снимков будут сохраняться"
|
||||||
|
},
|
||||||
|
"yearly": {
|
||||||
|
"two": "Последние {} ежегодных снимка будут сохраняться",
|
||||||
|
"many": "Последние {} ежегодных снимков будут сохраняться",
|
||||||
|
"zero": "Правило отключено",
|
||||||
|
"one": "Последний {} ежегодный снимок будет сохраняться",
|
||||||
|
"few": "Последние {} ежегодных снимка будут сохраняться",
|
||||||
|
"other": "Последние {} ежегодных снимков будут сохраняться"
|
||||||
|
},
|
||||||
|
"last_infinite": "Все снимки будут сохранены",
|
||||||
|
"daily_infinite": "Все ежедневные снимки будут сохраняться",
|
||||||
|
"weekly_infinite": "Все еженедельные снимки будут сохраняться",
|
||||||
|
"monthly_infinite": "Все ежемесячные снимки будут сохраняться",
|
||||||
|
"yearly_infinite": "Все ежегодные снимки будут сохраняться"
|
||||||
|
},
|
||||||
|
"snapshot_reason_title": "Причина создания",
|
||||||
|
"snapshot_reasons": {
|
||||||
|
"auto": "Создано автоматически",
|
||||||
|
"explicit": "Создано по вашему явному запросу",
|
||||||
|
"pre_restore": "Создано в качестве меры предосторожности перед рискованным восстановлением",
|
||||||
|
"unknown": "Неизвестно"
|
||||||
|
},
|
||||||
|
"rotation_quotas_title": "Настройки ротации снимков",
|
||||||
|
"set_rotation_quotas": "Задать новые квоты ротации",
|
||||||
|
"quotas_set": "Новые квоты ротации резервных копий заданы",
|
||||||
|
"quota_titles": {
|
||||||
|
"last": "Сколько последних снимков сохранять",
|
||||||
|
"daily": "Сколько ежедневных снимков сохранять",
|
||||||
|
"weekly": "Сколько еженедельных снимков сохранять",
|
||||||
|
"monthly": "Сколько ежемесячных снимков сохранять",
|
||||||
|
"yearly": "Сколько ежегодных снимков сохранять"
|
||||||
|
},
|
||||||
|
"quotas_only_applied_to_autobackups": "Эти настройки применяются только к резервным копиям, созданным автоматически. Созданные вручную резервные копии не будут удалены этими правилами."
|
||||||
},
|
},
|
||||||
"storage": {
|
"storage": {
|
||||||
"card_title": "Хранилище",
|
"card_title": "Хранилище",
|
||||||
|
@ -300,7 +386,7 @@
|
||||||
"username_rule": "Имя может содержать только маленькие латинские буквы, цифры, подчёркивания, не может начинаться с цифр",
|
"username_rule": "Имя может содержать только маленькие латинские буквы, цифры, подчёркивания, не может начинаться с цифр",
|
||||||
"email_login": "Авторизация по Email",
|
"email_login": "Авторизация по Email",
|
||||||
"no_ssh_notice": "Для этого пользователя созданы только SSH и Email аккаунт. Единая авторизация для всех сервисов ещё не реализована.",
|
"no_ssh_notice": "Для этого пользователя созданы только SSH и Email аккаунт. Единая авторизация для всех сервисов ещё не реализована.",
|
||||||
"details_title": "Пользовательские данные"
|
"details_title": "Пользователь"
|
||||||
},
|
},
|
||||||
"initializing": {
|
"initializing": {
|
||||||
"dns_provider_description": "Это позволит связать ваш домен с IP адресом:",
|
"dns_provider_description": "Это позволит связать ваш домен с IP адресом:",
|
||||||
|
@ -337,6 +423,9 @@
|
||||||
"choose_server_type_ram": "{} GB у RAM",
|
"choose_server_type_ram": "{} GB у RAM",
|
||||||
"choose_server_type_storage": "{} GB системного хранилища",
|
"choose_server_type_storage": "{} GB системного хранилища",
|
||||||
"choose_server_type_payment_per_month": "{} в месяц",
|
"choose_server_type_payment_per_month": "{} в месяц",
|
||||||
|
"choose_server_type_payment_server": "{} за сам сервер",
|
||||||
|
"choose_server_type_payment_storage": "{} за расширяемое хранилище",
|
||||||
|
"choose_server_type_payment_ip": "{} за публичный IPv4",
|
||||||
"no_server_types_found": "Не найдено доступных типов сервера! Пожалуйста, убедитесь, что у вас есть доступ к провайдеру сервера...",
|
"no_server_types_found": "Не найдено доступных типов сервера! Пожалуйста, убедитесь, что у вас есть доступ к провайдеру сервера...",
|
||||||
"dns_provider_bad_key_error": "API ключ неверен",
|
"dns_provider_bad_key_error": "API ключ неверен",
|
||||||
"backblaze_bad_key_error": "Информация о Backblaze хранилище неверна",
|
"backblaze_bad_key_error": "Информация о Backblaze хранилище неверна",
|
||||||
|
@ -345,7 +434,6 @@
|
||||||
"manage_domain_dns": "Для управления DNS вашего домена",
|
"manage_domain_dns": "Для управления DNS вашего домена",
|
||||||
"use_this_domain": "Используем этот домен?",
|
"use_this_domain": "Используем этот домен?",
|
||||||
"use_this_domain_text": "Указанный вами токен даёт контроль над этим доменом",
|
"use_this_domain_text": "Указанный вами токен даёт контроль над этим доменом",
|
||||||
"cloudflare_api_token": "API ключ DNS провайдера",
|
|
||||||
"connect_backblaze_storage": "Подключите облачное хранилище Backblaze",
|
"connect_backblaze_storage": "Подключите облачное хранилище Backblaze",
|
||||||
"no_connected_domains": "На данный момент подлюченных доменов нет",
|
"no_connected_domains": "На данный момент подлюченных доменов нет",
|
||||||
"loading_domain_list": "Загружаем список доменов",
|
"loading_domain_list": "Загружаем список доменов",
|
||||||
|
@ -370,15 +458,17 @@
|
||||||
"server_type": "Тип сервера",
|
"server_type": "Тип сервера",
|
||||||
"nixos_installation": "Установка NixOS",
|
"nixos_installation": "Установка NixOS",
|
||||||
"dns_provider": "DNS провайдер",
|
"dns_provider": "DNS провайдер",
|
||||||
"backups_provider": "Резервные копии",
|
"backups_provider": "Резервное копирование",
|
||||||
"domain": "Домен",
|
"domain": "Домен",
|
||||||
"master_account": "Мастер аккаунт",
|
"master_account": "Главная учетная запись",
|
||||||
"server": "Сервер",
|
"server": "Сервер",
|
||||||
"dns_setup": "Установка DNS",
|
"dns_setup": "Установка DNS",
|
||||||
"server_reboot": "Перезагрузка сервера",
|
"server_reboot": "Перезагрузка сервера",
|
||||||
"final_checks": "Финальные проверки"
|
"final_checks": "Финальные проверки"
|
||||||
},
|
},
|
||||||
"server_provider_description": "Место, где будут находиться ваши данные и службы SelfPrivacy:"
|
"server_provider_description": "Место, где будут находиться ваши данные и сервисы SelfPrivacy:",
|
||||||
|
"multiple_domains_found": "Найдено несколько доменов",
|
||||||
|
"multiple_domains_found_text": "Предоставленный токен дает доступ к следующим доменам. Пожалуйста, выберите тот, который вы хотите использовать. Для обеспечения безопасности других доменов следует ограничить доступ этого токена только тем доменом, который вы хотите использовать с SelfPrivacy."
|
||||||
},
|
},
|
||||||
"recovering": {
|
"recovering": {
|
||||||
"generic_error": "Ошибка проведения операции, попробуйте ещё раз.",
|
"generic_error": "Ошибка проведения операции, попробуйте ещё раз.",
|
||||||
|
@ -481,7 +571,7 @@
|
||||||
"you_cant_use_this_api": "Нельзя использовать этот API для доменом с подобным TLD.",
|
"you_cant_use_this_api": "Нельзя использовать этот API для доменом с подобным TLD.",
|
||||||
"yes": "Да",
|
"yes": "Да",
|
||||||
"no": "Нет",
|
"no": "Нет",
|
||||||
"volume_creation_error": "Не удалось создать том."
|
"volume_creation_error": "Не удалось создать хранилище."
|
||||||
},
|
},
|
||||||
"timer": {
|
"timer": {
|
||||||
"sec": "{} сек"
|
"sec": "{} сек"
|
||||||
|
@ -528,11 +618,11 @@
|
||||||
"server_setup": "Мастер установки сервера",
|
"server_setup": "Мастер установки сервера",
|
||||||
"use_staging_acme": "Использование тестового ACME сервера",
|
"use_staging_acme": "Использование тестового ACME сервера",
|
||||||
"use_staging_acme_description": "Применяется при настройке нового сервера.",
|
"use_staging_acme_description": "Применяется при настройке нового сервера.",
|
||||||
"routing": "Маршрутизация приложений",
|
"routing": "Роутинг приложения",
|
||||||
"reset_onboarding": "Сбросить флаг посещения приветствия",
|
"reset_onboarding": "Сбросить флаг посещения приветствия",
|
||||||
"cubit_statuses": "Текущий статут кубитов загрузки",
|
"cubit_statuses": "Текущий статут кубитов загрузки",
|
||||||
"reset_onboarding_description": "Сброс переключателя включения для повторного отображения экрана включения",
|
"reset_onboarding_description": "Принудить показ приветственного экрана",
|
||||||
"ignore_tls_description": "Приложение не будет проверять сертификаты TLS при подключении к серверу.",
|
"ignore_tls_description": "Приложение не будет проверять сертификаты TLS при подключении к серверу.",
|
||||||
"ignore_tls": "Не проверять сертификаты TLS"
|
"ignore_tls": "Не проверять сертификаты TLS"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -173,7 +173,6 @@
|
||||||
"disk": "Lokálny disk",
|
"disk": "Lokálny disk",
|
||||||
"monthly_cost": "Mesačná cena",
|
"monthly_cost": "Mesačná cena",
|
||||||
"location": "Lokalita",
|
"location": "Lokalita",
|
||||||
"provider": "Poskytovateľ",
|
|
||||||
"core_count": {
|
"core_count": {
|
||||||
"one": "{} jadro",
|
"one": "{} jadro",
|
||||||
"two": "{} jadrá",
|
"two": "{} jadrá",
|
||||||
|
@ -280,7 +279,6 @@
|
||||||
"enter_username_and_password": "Zadajte používateľské meno a zložité heslo",
|
"enter_username_and_password": "Zadajte používateľské meno a zložité heslo",
|
||||||
"finish": "Všetko je inicializované",
|
"finish": "Všetko je inicializované",
|
||||||
"use_this_domain_text": "Token, ktorý ste poskytli, poskytuje prístup k nasledujúcej doméne",
|
"use_this_domain_text": "Token, ktorý ste poskytli, poskytuje prístup k nasledujúcej doméne",
|
||||||
"cloudflare_api_token": "CloudFlare API Token",
|
|
||||||
"connect_backblaze_storage": "Pripojte svoje cloudové úložisko Backblaze",
|
"connect_backblaze_storage": "Pripojte svoje cloudové úložisko Backblaze",
|
||||||
"no_connected_domains": "Momentálne nie sú pripojené žiadne domény",
|
"no_connected_domains": "Momentálne nie sú pripojené žiadne domény",
|
||||||
"loading_domain_list": "Načítava sa zoznam domén",
|
"loading_domain_list": "Načítava sa zoznam domén",
|
||||||
|
@ -500,4 +498,4 @@
|
||||||
"reset_onboarding_description": "Resetovanie vypínača na opätovné zobrazenie obrazovky zapnutia",
|
"reset_onboarding_description": "Resetovanie vypínača na opätovné zobrazenie obrazovky zapnutia",
|
||||||
"cubit_statuses": "Aktuálny stav načítavania qubitov"
|
"cubit_statuses": "Aktuálny stav načítavania qubitov"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -116,8 +116,7 @@
|
||||||
"ram": "Glavni pomnilnik",
|
"ram": "Glavni pomnilnik",
|
||||||
"disk": "Lokalni disk",
|
"disk": "Lokalni disk",
|
||||||
"monthly_cost": "Mesečni stroški",
|
"monthly_cost": "Mesečni stroški",
|
||||||
"location": "Lokacija",
|
"location": "Lokacija"
|
||||||
"provider": "Ponudnik"
|
|
||||||
},
|
},
|
||||||
"ssh": {
|
"ssh": {
|
||||||
"root_subtitle": "Lastniki tukaj navedenih ključev imajo popoln dostop do podatkov in nastavitev strežnika. Dodajte samo svoje ključe.",
|
"root_subtitle": "Lastniki tukaj navedenih ključev imajo popoln dostop do podatkov in nastavitev strežnika. Dodajte samo svoje ključe.",
|
||||||
|
|
|
@ -149,7 +149,6 @@
|
||||||
"ram": "หน่วยความจำ",
|
"ram": "หน่วยความจำ",
|
||||||
"monthly_cost": "รายจ่ายต่อเดือน",
|
"monthly_cost": "รายจ่ายต่อเดือน",
|
||||||
"location": "สถานที่",
|
"location": "สถานที่",
|
||||||
"provider": "ผู้ให้บริการ",
|
|
||||||
"core_count": {
|
"core_count": {
|
||||||
"one": "{} core",
|
"one": "{} core",
|
||||||
"two": "{} จำนวนคอร์",
|
"two": "{} จำนวนคอร์",
|
||||||
|
@ -293,4 +292,4 @@
|
||||||
"title": "เซิฟเวอร์ VPN",
|
"title": "เซิฟเวอร์ VPN",
|
||||||
"subtitle": "เซิฟเวอร์ VPN ส่วนตัว"
|
"subtitle": "เซิฟเวอร์ VPN ส่วนตัว"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,7 +33,8 @@
|
||||||
"delete": "Видалити",
|
"delete": "Видалити",
|
||||||
"close": "Закрити",
|
"close": "Закрити",
|
||||||
"connect": "Підключіться",
|
"connect": "Підключіться",
|
||||||
"app_name": "SelfPrivacy"
|
"app_name": "SelfPrivacy",
|
||||||
|
"copied_to_clipboard": "Скопійовано в буфер обміну!"
|
||||||
},
|
},
|
||||||
"locale": "ua",
|
"locale": "ua",
|
||||||
"application_settings": {
|
"application_settings": {
|
||||||
|
@ -43,7 +44,10 @@
|
||||||
"dark_theme_description": "Змінити тему додатка",
|
"dark_theme_description": "Змінити тему додатка",
|
||||||
"reset_config_description": "Скинути API ключі та root користувача.",
|
"reset_config_description": "Скинути API ключі та root користувача.",
|
||||||
"delete_server_title": "Видалити сервер",
|
"delete_server_title": "Видалити сервер",
|
||||||
"delete_server_description": "Це видалить ваш сервер. Він більше не буде доступний."
|
"delete_server_description": "Це видалить ваш сервер. Він більше не буде доступний.",
|
||||||
|
"system_dark_theme_title": "Системна тема за замовчуванням",
|
||||||
|
"system_dark_theme_description": "Використовуйте світлу або темну теми залежно від системних налаштувань",
|
||||||
|
"dangerous_settings": "Небезпечні налаштування"
|
||||||
},
|
},
|
||||||
"ssh": {
|
"ssh": {
|
||||||
"delete_confirm_question": "Ви впевнені, що хочете видалити SSH-ключ?",
|
"delete_confirm_question": "Ви впевнені, що хочете видалити SSH-ключ?",
|
||||||
|
@ -134,7 +138,6 @@
|
||||||
"select_dns": "Тепер давайте оберемо DNS-провайдера",
|
"select_dns": "Тепер давайте оберемо DNS-провайдера",
|
||||||
"manage_domain_dns": "Для управління DNS домену",
|
"manage_domain_dns": "Для управління DNS домену",
|
||||||
"use_this_domain": "Скористатися цим доменом?",
|
"use_this_domain": "Скористатися цим доменом?",
|
||||||
"cloudflare_api_token": "CloudFlare API токен",
|
|
||||||
"connect_backblaze_storage": "Підключити Backblaze сховище",
|
"connect_backblaze_storage": "Підключити Backblaze сховище",
|
||||||
"no_connected_domains": "Наразі немає пов'язаних доменів",
|
"no_connected_domains": "Наразі немає пов'язаних доменів",
|
||||||
"save_domain": "Зберегти домен",
|
"save_domain": "Зберегти домен",
|
||||||
|
@ -210,7 +213,6 @@
|
||||||
"server_id": "Сервер ID",
|
"server_id": "Сервер ID",
|
||||||
"cpu": "Процессор",
|
"cpu": "Процессор",
|
||||||
"ram": "Пам'ять",
|
"ram": "Пам'ять",
|
||||||
"provider": "Провайдер",
|
|
||||||
"core_count": {
|
"core_count": {
|
||||||
"one": "{} ядро",
|
"one": "{} ядро",
|
||||||
"few": "{} ядра",
|
"few": "{} ядра",
|
||||||
|
@ -224,7 +226,9 @@
|
||||||
"server_timezone": "Часовий пояс сервера",
|
"server_timezone": "Часовий пояс сервера",
|
||||||
"timezone_search_bar": "Ім'я часового поясу або значення зсуву часу",
|
"timezone_search_bar": "Ім'я часового поясу або значення зсуву часу",
|
||||||
"monthly_cost": "Щомісячна вартість",
|
"monthly_cost": "Щомісячна вартість",
|
||||||
"location": "Місцезнаходження"
|
"location": "Місцезнаходження",
|
||||||
|
"server_provider": "Провайдер сервера",
|
||||||
|
"dns_provider": "Провайдер DNS"
|
||||||
},
|
},
|
||||||
"record": {
|
"record": {
|
||||||
"api": "SelfPrivacy API",
|
"api": "SelfPrivacy API",
|
||||||
|
@ -251,7 +255,7 @@
|
||||||
"email_title": "Електронна пошта",
|
"email_title": "Електронна пошта",
|
||||||
"email_subtitle": "Записи, необхідні для безпечного обміну електронною поштою.",
|
"email_subtitle": "Записи, необхідні для безпечного обміну електронною поштою.",
|
||||||
"update_list": "Лист оновлень",
|
"update_list": "Лист оновлень",
|
||||||
"error_subtitle": "Нажміть сюди, щоб виправити їх",
|
"error_subtitle": "Натисніть тут, щоб виправити їх. При цьому також буде видалено користувацькі записи.",
|
||||||
"services_subtitle": "Введіть \"А\" записи, необхідні для кожної служби."
|
"services_subtitle": "Введіть \"А\" записи, необхідні для кожної служби."
|
||||||
},
|
},
|
||||||
"backup": {
|
"backup": {
|
||||||
|
@ -270,7 +274,29 @@
|
||||||
"description": "Врятує ваш день у разі аварії: хакерська атака, видаленя серверу, тощо.",
|
"description": "Врятує ваш день у разі аварії: хакерська атака, видаленя серверу, тощо.",
|
||||||
"waiting_for_rebuild": "Ви зможете створити свою першу резервну копію через кілька хвилин.",
|
"waiting_for_rebuild": "Ви зможете створити свою першу резервну копію через кілька хвилин.",
|
||||||
"restoring": "Відновлення з резервної копії",
|
"restoring": "Відновлення з резервної копії",
|
||||||
"restore_alert": "Ви збираєтеся відновити з резервної копії. створеної на {}. Усі поточні дані будуть втрачені. Ви згодні?"
|
"restore_alert": "Ви збираєтеся відновити з резервної копії. створеної на {}. Усі поточні дані будуть втрачені. Ви згодні?",
|
||||||
|
"refetch_backups_subtitle": "Скинути кеш і запросити дані у провайдера. Може спричинити додаткові витрати.",
|
||||||
|
"reupload_key_subtitle": "Ще раз проініціалізує сховище резервних копій. Використовуйте, якщо щось зламалося.",
|
||||||
|
"create_new_select_heading": "Вибрати сервіси для копіювання",
|
||||||
|
"start": "Почати створення копій",
|
||||||
|
"service_busy": "Зараз створюються інші резервні копії",
|
||||||
|
"latest_snapshots": "Останні знімки",
|
||||||
|
"latest_snapshots_subtitle": "Останні 15 знімків",
|
||||||
|
"show_more": "Показати ще",
|
||||||
|
"autobackup_period_title": "Період автоматичного копіювання",
|
||||||
|
"autobackup_period_subtitle": "Створення копій раз на {period}",
|
||||||
|
"autobackup_period_never": "Автоматичне копіювання вимкнено",
|
||||||
|
"autobackup_period_every": "Раз у {period}",
|
||||||
|
"autobackup_period_disable": "Вимкнути автоматичні копіювання",
|
||||||
|
"autobackup_custom": "Інше",
|
||||||
|
"autobackup_custom_hint": "Введіть період у хвилинах",
|
||||||
|
"autobackup_set_period": "Встановити період",
|
||||||
|
"autobackup_period_set": "Період встановлено",
|
||||||
|
"backups_encryption_key": "Ключ шифрування",
|
||||||
|
"backups_encryption_key_subtitle": "Зберігайте його в безпечному місці.",
|
||||||
|
"backups_encryption_key_copy": "Скопіюйте ключ шифрування",
|
||||||
|
"card_subtitle": "Керуйте резервними копіями",
|
||||||
|
"select_all": "Копіювати все"
|
||||||
},
|
},
|
||||||
"storage": {
|
"storage": {
|
||||||
"card_title": "Серверне сховище",
|
"card_title": "Серверне сховище",
|
||||||
|
@ -469,4 +495,4 @@
|
||||||
"root_name": "Не може бути 'root'",
|
"root_name": "Не може бути 'root'",
|
||||||
"length_not_equal": "Довжина [], має бути {}"
|
"length_not_equal": "Довжина [], має бути {}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,145 @@
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- New backups implementation ([#228](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/228), [#274](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/274), [#324](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/324), [#325](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/325), [#326](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/326), [#331](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/331), [#332](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/332))
|
||||||
|
- DigitalOcean as a DNS provider ([#213](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/213))
|
||||||
|
- DeSEC as a DNS provider ([#211](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/211))
|
||||||
|
- Support drawer and basic support documentation logic unit ([#203](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/203))
|
||||||
|
- Automatic day/night theme ([#203](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/203))
|
||||||
|
- New router and adaptive layouts ([#203](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/203))
|
||||||
|
- New Material 3 animation curves ([#203](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/203))
|
||||||
|
- Jobs button to the app bar of more screens ([#203](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/203))
|
||||||
|
- Refreshed UI of modal sheets ([#228](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/228))
|
||||||
|
- Support for `XDG_DATA_HOME` storage path on Linux for app data ([#240](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/240))
|
||||||
|
- Accept-Language header for the server API ([#243](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/243), resolves [#205](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/205))
|
||||||
|
- Visible providers names during server recovery ([#264](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/264), resolves [#249](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/249))
|
||||||
|
- Visible volume and IPv4 cost added to overall monthly cost of the server ([#270](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/270), resolves [#115](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/115))
|
||||||
|
- Support for autofocus on text fields for keyboard displaying ([#294](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/294), resolves [#292](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/292))
|
||||||
|
- New dialogue to choose a domain if user DNS token provides access to several ([#330](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/330), resolves [#328](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/328))
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Fix opening URLs from the app ([#213](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/213))
|
||||||
|
- Fix parsing of RAM size with DigitalOcean ([#200](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/200), resolves [#199](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/199))
|
||||||
|
- Devices and Recovery Key cubits couldn't initialize right after server installation ([#203](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/203))
|
||||||
|
- Fix BottomBar showing incorrect animation when navigating from sibling routes ([#203](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/203))
|
||||||
|
- PopUpDialogs couldn't find the context. ([#203](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/203))
|
||||||
|
- Update recovery flow to use new support drawer ([#203](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/203))
|
||||||
|
- New app log console ([#203](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/203))
|
||||||
|
- Improve installation failure dialogues ([#213](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/213))
|
||||||
|
- Privacy policy link pointed at wrong domain ([#207](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/207))
|
||||||
|
- Remove price lists for DNS ([#211](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/211))
|
||||||
|
- Implement better domain id check on DNS restoration ([#211](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/211))
|
||||||
|
- Add forced JSON content type to REST APIs ([#212](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/212))
|
||||||
|
- Remove unneded DNS check depending on CLOUDFLARE ([#212](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/212))
|
||||||
|
- Add background for dialogue pop ups and move them to root navigator ([#233](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/233), resolves [#231](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/231))
|
||||||
|
- Make currency be properly shown again via shortcode ([#234](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/234), related to [#223](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/223))
|
||||||
|
- Add proper server type value loading ([#236](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/236), resolves [#215](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/215))
|
||||||
|
- Implement proper load functions for DNS and Server providers ([#237](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/237), resolves [#220](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/220))
|
||||||
|
- Prevent moving a service if volume is null for some reason ([#245](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/245))
|
||||||
|
- Replace hard reset from server provider with direct server reboot ([#269](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/269), resolves [#266](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/266))
|
||||||
|
- Normalize Hetzner CPU usage percentage by cached amount of cores ([#272](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/272), resolves [#156](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/156))
|
||||||
|
- Change broken validations string for superuser SSH ([#276](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/27))
|
||||||
|
- Don't let service migration to start bif the same volume was picked ([#297](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/297), resolves [#289](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/289))
|
||||||
|
- Wrap DNS check in catch to avoid runtime crash ([#322](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/322))
|
||||||
|
- Implement Backblaze bucket restoration on server recovery ([#324](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/324))
|
||||||
|
|
||||||
|
### Refactor
|
||||||
|
|
||||||
|
- Migrate to Flutter 3.10 and Dart 3.0
|
||||||
|
- Migrate to AutoRouter v6 ([#203](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/203))
|
||||||
|
- Get rid of BrandText and restructure the buttons ([#203](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/203))
|
||||||
|
- Remove brand alert dialogs and bottom sheet ([#203](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/203))
|
||||||
|
- Remove unused UI components ([#203](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/203))
|
||||||
|
- Remove BrandCards ([#203](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/203))
|
||||||
|
- Allow changing values for TLS settings
|
||||||
|
- Replace String shortcode with Currency class ([#226](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/226))
|
||||||
|
- Rearrange Server Provider interface ([#227](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/227))
|
||||||
|
- Remove unused service state getters ([#228](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/228))
|
||||||
|
- Remove unused utils, add duration formatter ([#228](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/228))
|
||||||
|
- Move rest api methods according to their business logic files positions ([#235](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/235), partially resolves [#217](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/217) and [#219](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/219))
|
||||||
|
- Make flag getter a part of server provider location object ([#238](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/238), resolves [#222](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/222))
|
||||||
|
|
||||||
|
|
||||||
|
### Translation contributions
|
||||||
|
|
||||||
|
* Ukrainian
|
||||||
|
|
||||||
|
* FoxMeste (3)
|
||||||
|
* Mithras (31)
|
||||||
|
|
||||||
|
* Latvian
|
||||||
|
|
||||||
|
* Not Telling Lol (183)
|
||||||
|
|
||||||
|
|
||||||
|
* German
|
||||||
|
|
||||||
|
* Mithras (41)
|
||||||
|
* FoxMeste (213)
|
||||||
|
|
||||||
|
|
||||||
|
* Thai
|
||||||
|
|
||||||
|
* FoxMeste (77)
|
||||||
|
|
||||||
|
|
||||||
|
* Polish
|
||||||
|
|
||||||
|
* Mithras (41)
|
||||||
|
* Thary (43)
|
||||||
|
* FoxMeste (163)
|
||||||
|
|
||||||
|
|
||||||
|
* Slovenian
|
||||||
|
|
||||||
|
* Mithras (212)
|
||||||
|
|
||||||
|
|
||||||
|
* Czech
|
||||||
|
|
||||||
|
* NaiJi ✨ (2)
|
||||||
|
* Mithras (109)
|
||||||
|
* FoxMeste (308)
|
||||||
|
|
||||||
|
|
||||||
|
* Russian
|
||||||
|
|
||||||
|
* FoxMeste (4)
|
||||||
|
* Revertron (8)
|
||||||
|
* NaiJi ✨ (23)
|
||||||
|
* Mithras (54)
|
||||||
|
* Inex Code (59)
|
||||||
|
|
||||||
|
|
||||||
|
* Slovak
|
||||||
|
|
||||||
|
* Mithras (29)
|
||||||
|
* Revertron (396)
|
||||||
|
|
||||||
|
|
||||||
|
* Macedonian
|
||||||
|
|
||||||
|
* FoxMeste (7)
|
||||||
|
|
||||||
|
|
||||||
|
* Belarusian
|
||||||
|
|
||||||
|
* Thary (1)
|
||||||
|
* FoxMeste (3)
|
||||||
|
* Mithras (47)
|
||||||
|
|
||||||
|
|
||||||
|
* French
|
||||||
|
|
||||||
|
* Côme (211)
|
||||||
|
|
||||||
|
|
||||||
|
* Spanish
|
||||||
|
|
||||||
|
* FoxMeste (7)
|
||||||
|
|
||||||
|
|
||||||
|
* Azerbaijani
|
||||||
|
|
||||||
|
* Mithras (28)
|
||||||
|
* Ortibexon (403)
|
|
@ -0,0 +1,20 @@
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Fix volume resizing on Digital Ocean ([#368](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/368), resolves [#367](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/367))
|
||||||
|
- Disable the storage card while volume information is being fetched ([#369](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/369), resolves [#317](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/317))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Add copy-to-clipboard for email on user page ([#329](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/329), resolves [#287](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/287))
|
||||||
|
- Add support for ECDSA SSH keys ([#362](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/362), resolves [#319](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/319))
|
||||||
|
- Implement confirmation modal for the volume resize ([#372](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/372), resolves [#308](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/308))
|
||||||
|
|
||||||
|
### Other changes
|
||||||
|
|
||||||
|
- Move service descriptions above login info for service cards ([#342](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/342), resolves [#341](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/341))
|
||||||
|
- Add measure units to 'Extending volume' page ([#344](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/344), resolves [#301](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/301))
|
||||||
|
- Make users to be ordered properly on users page ([#343](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/343), resolves [#340](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/340))
|
||||||
|
- Move service card name to its icon row ([#352](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/352), resolves [#350](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/350))
|
||||||
|
- Reorganize placeholders for empty pages ([#359](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/359), resolves [#348](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/348))
|
||||||
|
- Remove redundant zone id cache for Cloudflare ([#371](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/371))
|
After Width: | Height: | Size: 179 KiB |
|
@ -66,16 +66,16 @@
|
||||||
},
|
},
|
||||||
"nixpkgs_2": {
|
"nixpkgs_2": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1662096612,
|
"lastModified": 1693250523,
|
||||||
"narHash": "sha256-R+Q8l5JuyJryRPdiIaYpO5O3A55rT+/pItBrKcy7LM4=",
|
"narHash": "sha256-y3up5gXMTbnCsXrNEB5j+7TVantDLUYyQLu/ueiXuyg=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "21de2b973f9fee595a7a1ac4693efff791245c34",
|
"rev": "3efb0f6f404ec8dae31bdb1a9b17705ce0d6986e",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"id": "nixpkgs",
|
"id": "nixpkgs",
|
||||||
"ref": "nixpkgs-unstable",
|
"ref": "nixos-unstable",
|
||||||
"type": "indirect"
|
"type": "indirect"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
55
flake.nix
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
nixConfig.bash-prompt = "\[selfprivacy\]$ ";
|
nixConfig.bash-prompt = "\[selfprivacy\]$ ";
|
||||||
|
|
||||||
inputs.nixpkgs.url = "nixpkgs/nixpkgs-unstable";
|
inputs.nixpkgs.url = "nixpkgs/nixos-unstable";
|
||||||
inputs.flake-utils.url = "github:numtide/flake-utils";
|
inputs.flake-utils.url = "github:numtide/flake-utils";
|
||||||
inputs.nixgl.url = "github:guibou/nixGL";
|
inputs.nixgl.url = "github:guibou/nixGL";
|
||||||
|
|
||||||
|
@ -9,19 +9,48 @@
|
||||||
flake-utils.lib.eachDefaultSystem (system:
|
flake-utils.lib.eachDefaultSystem (system:
|
||||||
let
|
let
|
||||||
pkgs = import nixpkgs {
|
pkgs = import nixpkgs {
|
||||||
|
inherit system;
|
||||||
config.allowUnfree = true;
|
config.allowUnfree = true;
|
||||||
config.android_sdk.accept_license = true;
|
config.android_sdk.accept_license = true;
|
||||||
system = "x86_64-linux";
|
|
||||||
overlays = [ nixgl.overlay ];
|
overlays = [ nixgl.overlay ];
|
||||||
};
|
};
|
||||||
|
|
||||||
androidComposition = pkgs.androidenv.composeAndroidPackages {
|
androidComposition = pkgs.androidenv.composeAndroidPackages {
|
||||||
toolsVersion = "26.1.1";
|
platformToolsVersion = "34.0.4";
|
||||||
platformToolsVersion = "33.0.2";
|
buildToolsVersions = [ "34.0.0" ];
|
||||||
buildToolsVersions = [ "30.0.3" ];
|
platformVersions = [ "34" "33" "32" "31" "30" ];
|
||||||
platformVersions = [ "31" "30" "29" ];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
spAndroidStudio = pkgs.symlinkJoin {
|
||||||
|
name = "spAndroidStudio";
|
||||||
|
paths = with pkgs; [
|
||||||
|
android-studio
|
||||||
|
flutter.unwrapped
|
||||||
|
# dart
|
||||||
|
gnumake
|
||||||
|
check
|
||||||
|
pkg-config
|
||||||
|
glibc
|
||||||
|
android-tools
|
||||||
|
jdk
|
||||||
|
git
|
||||||
|
];
|
||||||
|
|
||||||
|
nativeBuildInputs = [ pkgs.makeWrapper ];
|
||||||
|
postBuild = ''
|
||||||
|
wrapProgram $out/bin/flutter \
|
||||||
|
--prefix ANDROID_SDK_ROOT=${androidComposition.androidsdk}/libexec/android-sdk \
|
||||||
|
--prefix ANDROID_HOME=${androidComposition.androidsdk}/libexec/android-sdk \
|
||||||
|
--prefix ANDROID_JAVA_HOME=${pkgs.jdk.home}
|
||||||
|
|
||||||
|
wrapProgram $out/bin/android-studio \
|
||||||
|
--prefix FLUTTER_SDK=${pkgs.flutter.unwrapped} \
|
||||||
|
--prefix ANDROID_SDKz_ROOT=${androidComposition.androidsdk}/libexec/android-sdk \
|
||||||
|
--prefix ANDROID_HOME=${androidComposition.androidsdk}/libexec/android-sdk \
|
||||||
|
--prefix ANDROID_JAVA_HOME=${pkgs.jdk.home}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
buildDeps = with pkgs; [
|
buildDeps = with pkgs; [
|
||||||
gtk3
|
gtk3
|
||||||
glib
|
glib
|
||||||
|
@ -62,23 +91,23 @@
|
||||||
openjdk11_headless
|
openjdk11_headless
|
||||||
clang
|
clang
|
||||||
];
|
];
|
||||||
|
|
||||||
releaseDerivation = pkgs.flutter.mkFlutterApp rec {
|
releaseDerivation = pkgs.flutter.mkFlutterApp rec {
|
||||||
pname = "selfprivacy";
|
pname = "selfprivacy";
|
||||||
version = "0.6.0";
|
version = "0.6.0";
|
||||||
|
|
||||||
vendorHash = "sha256-7cbiAyIlaz3HqEsZN/nZxaLZjseJv5CmiIHqsoGa4ZI=";
|
vendorHash = "sha256-7cbiAyIlaz3HqEsZN/nZxaLZjseJv5CmiIHqsoGa4ZI=";
|
||||||
|
|
||||||
nativeBuildInputs = [ pkgs.nixgl.auto.nixGLDefault ];
|
nativeBuildInputs = [ pkgs.nixgl.auto.nixGLDefault ];
|
||||||
|
|
||||||
src = ./.;
|
src = ./.;
|
||||||
|
|
||||||
desktopItem = pkgs.makeDesktopItem {
|
desktopItem = pkgs.makeDesktopItem {
|
||||||
name = "${pname}";
|
name = "${pname}";
|
||||||
exec = "@out@/bin/${pname}";
|
exec = "@out@/bin/${pname}";
|
||||||
desktopName = "SelfPrivacy";
|
desktopName = "SelfPrivacy";
|
||||||
};
|
};
|
||||||
|
|
||||||
postInstall = ''
|
postInstall = ''
|
||||||
rm $out/bin/$pname
|
rm $out/bin/$pname
|
||||||
|
|
||||||
|
@ -86,7 +115,7 @@
|
||||||
patchShebangs $out/bin/$pname
|
patchShebangs $out/bin/$pname
|
||||||
chmod +x $out/bin/$pname
|
chmod +x $out/bin/$pname
|
||||||
wrapProgram $out/bin/$pname --set PATH ${pkgs.lib.makeBinPath [ pkgs.xdg-user-dirs ]}
|
wrapProgram $out/bin/$pname --set PATH ${pkgs.lib.makeBinPath [ pkgs.xdg-user-dirs ]}
|
||||||
|
|
||||||
mkdir -p $out/share/applications
|
mkdir -p $out/share/applications
|
||||||
cp $desktopItem/share/applications/*.desktop $out/share/applications
|
cp $desktopItem/share/applications/*.desktop $out/share/applications
|
||||||
substituteInPlace $out/share/applications/*.desktop --subst-var out
|
substituteInPlace $out/share/applications/*.desktop --subst-var out
|
||||||
|
|
|
@ -7,6 +7,13 @@ query BackupConfiguration {
|
||||||
locationId
|
locationId
|
||||||
locationName
|
locationName
|
||||||
provider
|
provider
|
||||||
|
autobackupQuotas {
|
||||||
|
last
|
||||||
|
daily
|
||||||
|
weekly
|
||||||
|
monthly
|
||||||
|
yearly
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,6 +27,7 @@ query AllBackupSnapshots {
|
||||||
displayName
|
displayName
|
||||||
id
|
id
|
||||||
}
|
}
|
||||||
|
reason
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -35,6 +43,13 @@ fragment genericBackupConfigReturn on GenericBackupConfigReturn {
|
||||||
autobackupPeriod
|
autobackupPeriod
|
||||||
locationName
|
locationName
|
||||||
locationId
|
locationId
|
||||||
|
autobackupQuotas {
|
||||||
|
last
|
||||||
|
daily
|
||||||
|
weekly
|
||||||
|
monthly
|
||||||
|
yearly
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -65,6 +80,14 @@ mutation SetAutobackupPeriod($period: Int = null) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mutation setAutobackupQuotas($quotas: AutobackupQuotasInput!) {
|
||||||
|
backup {
|
||||||
|
setAutobackupQuotas(quotas: $quotas) {
|
||||||
|
...genericBackupConfigReturn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
mutation RemoveRepository {
|
mutation RemoveRepository {
|
||||||
backup {
|
backup {
|
||||||
removeRepository {
|
removeRepository {
|
||||||
|
@ -98,4 +121,4 @@ mutation ForgetSnapshot($snapshotId: String!) {
|
||||||
...basicMutationReturnFields
|
...basicMutationReturnFields
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -75,6 +75,22 @@ type AutoUpgradeSettingsMutationReturn implements MutationReturnInterface {
|
||||||
allowReboot: Boolean!
|
allowReboot: Boolean!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AutobackupQuotas {
|
||||||
|
last: Int!
|
||||||
|
daily: Int!
|
||||||
|
weekly: Int!
|
||||||
|
monthly: Int!
|
||||||
|
yearly: Int!
|
||||||
|
}
|
||||||
|
|
||||||
|
input AutobackupQuotasInput {
|
||||||
|
last: Int!
|
||||||
|
daily: Int!
|
||||||
|
weekly: Int!
|
||||||
|
monthly: Int!
|
||||||
|
yearly: Int!
|
||||||
|
}
|
||||||
|
|
||||||
type Backup {
|
type Backup {
|
||||||
configuration: BackupConfiguration!
|
configuration: BackupConfiguration!
|
||||||
allSnapshots: [SnapshotInfo!]!
|
allSnapshots: [SnapshotInfo!]!
|
||||||
|
@ -85,6 +101,7 @@ type BackupConfiguration {
|
||||||
encryptionKey: String!
|
encryptionKey: String!
|
||||||
isInitialized: Boolean!
|
isInitialized: Boolean!
|
||||||
autobackupPeriod: Int
|
autobackupPeriod: Int
|
||||||
|
autobackupQuotas: AutobackupQuotas!
|
||||||
locationName: String
|
locationName: String
|
||||||
locationId: String
|
locationId: String
|
||||||
}
|
}
|
||||||
|
@ -93,6 +110,7 @@ type BackupMutations {
|
||||||
initializeRepository(repository: InitializeRepositoryInput!): GenericBackupConfigReturn!
|
initializeRepository(repository: InitializeRepositoryInput!): GenericBackupConfigReturn!
|
||||||
removeRepository: GenericBackupConfigReturn!
|
removeRepository: GenericBackupConfigReturn!
|
||||||
setAutobackupPeriod(period: Int = null): GenericBackupConfigReturn!
|
setAutobackupPeriod(period: Int = null): GenericBackupConfigReturn!
|
||||||
|
setAutobackupQuotas(quotas: AutobackupQuotasInput!): GenericBackupConfigReturn!
|
||||||
startBackup(serviceId: String!): GenericJobMutationReturn!
|
startBackup(serviceId: String!): GenericJobMutationReturn!
|
||||||
restoreBackup(snapshotId: String!, strategy: RestoreStrategy! = DOWNLOAD_VERIFY_OVERWRITE): GenericJobMutationReturn!
|
restoreBackup(snapshotId: String!, strategy: RestoreStrategy! = DOWNLOAD_VERIFY_OVERWRITE): GenericJobMutationReturn!
|
||||||
forgetSnapshot(snapshotId: String!): GenericMutationReturn!
|
forgetSnapshot(snapshotId: String!): GenericMutationReturn!
|
||||||
|
@ -106,6 +124,12 @@ enum BackupProvider {
|
||||||
FILE
|
FILE
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum BackupReason {
|
||||||
|
EXPLICIT
|
||||||
|
AUTO
|
||||||
|
PRE_RESTORE
|
||||||
|
}
|
||||||
|
|
||||||
"""Date with time (isoformat)"""
|
"""Date with time (isoformat)"""
|
||||||
scalar DateTime
|
scalar DateTime
|
||||||
|
|
||||||
|
@ -326,6 +350,7 @@ type SnapshotInfo {
|
||||||
id: String!
|
id: String!
|
||||||
service: Service!
|
service: Service!
|
||||||
createdAt: DateTime!
|
createdAt: DateTime!
|
||||||
|
reason: BackupReason!
|
||||||
}
|
}
|
||||||
|
|
||||||
input SshMutationInput {
|
input SshMutationInput {
|
||||||
|
|
|
@ -141,6 +141,185 @@ class _CopyWithStubImpl$Input$AutoUpgradeSettingsInput<TRes>
|
||||||
_res;
|
_res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class Input$AutobackupQuotasInput {
|
||||||
|
factory Input$AutobackupQuotasInput({
|
||||||
|
required int last,
|
||||||
|
required int daily,
|
||||||
|
required int weekly,
|
||||||
|
required int monthly,
|
||||||
|
required int yearly,
|
||||||
|
}) =>
|
||||||
|
Input$AutobackupQuotasInput._({
|
||||||
|
r'last': last,
|
||||||
|
r'daily': daily,
|
||||||
|
r'weekly': weekly,
|
||||||
|
r'monthly': monthly,
|
||||||
|
r'yearly': yearly,
|
||||||
|
});
|
||||||
|
|
||||||
|
Input$AutobackupQuotasInput._(this._$data);
|
||||||
|
|
||||||
|
factory Input$AutobackupQuotasInput.fromJson(Map<String, dynamic> data) {
|
||||||
|
final result$data = <String, dynamic>{};
|
||||||
|
final l$last = data['last'];
|
||||||
|
result$data['last'] = (l$last as int);
|
||||||
|
final l$daily = data['daily'];
|
||||||
|
result$data['daily'] = (l$daily as int);
|
||||||
|
final l$weekly = data['weekly'];
|
||||||
|
result$data['weekly'] = (l$weekly as int);
|
||||||
|
final l$monthly = data['monthly'];
|
||||||
|
result$data['monthly'] = (l$monthly as int);
|
||||||
|
final l$yearly = data['yearly'];
|
||||||
|
result$data['yearly'] = (l$yearly as int);
|
||||||
|
return Input$AutobackupQuotasInput._(result$data);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> _$data;
|
||||||
|
|
||||||
|
int get last => (_$data['last'] as int);
|
||||||
|
int get daily => (_$data['daily'] as int);
|
||||||
|
int get weekly => (_$data['weekly'] as int);
|
||||||
|
int get monthly => (_$data['monthly'] as int);
|
||||||
|
int get yearly => (_$data['yearly'] as int);
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final result$data = <String, dynamic>{};
|
||||||
|
final l$last = last;
|
||||||
|
result$data['last'] = l$last;
|
||||||
|
final l$daily = daily;
|
||||||
|
result$data['daily'] = l$daily;
|
||||||
|
final l$weekly = weekly;
|
||||||
|
result$data['weekly'] = l$weekly;
|
||||||
|
final l$monthly = monthly;
|
||||||
|
result$data['monthly'] = l$monthly;
|
||||||
|
final l$yearly = yearly;
|
||||||
|
result$data['yearly'] = l$yearly;
|
||||||
|
return result$data;
|
||||||
|
}
|
||||||
|
|
||||||
|
CopyWith$Input$AutobackupQuotasInput<Input$AutobackupQuotasInput>
|
||||||
|
get copyWith => CopyWith$Input$AutobackupQuotasInput(
|
||||||
|
this,
|
||||||
|
(i) => i,
|
||||||
|
);
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (identical(this, other)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!(other is Input$AutobackupQuotasInput) ||
|
||||||
|
runtimeType != other.runtimeType) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final l$last = last;
|
||||||
|
final lOther$last = other.last;
|
||||||
|
if (l$last != lOther$last) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final l$daily = daily;
|
||||||
|
final lOther$daily = other.daily;
|
||||||
|
if (l$daily != lOther$daily) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final l$weekly = weekly;
|
||||||
|
final lOther$weekly = other.weekly;
|
||||||
|
if (l$weekly != lOther$weekly) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final l$monthly = monthly;
|
||||||
|
final lOther$monthly = other.monthly;
|
||||||
|
if (l$monthly != lOther$monthly) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final l$yearly = yearly;
|
||||||
|
final lOther$yearly = other.yearly;
|
||||||
|
if (l$yearly != lOther$yearly) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
final l$last = last;
|
||||||
|
final l$daily = daily;
|
||||||
|
final l$weekly = weekly;
|
||||||
|
final l$monthly = monthly;
|
||||||
|
final l$yearly = yearly;
|
||||||
|
return Object.hashAll([
|
||||||
|
l$last,
|
||||||
|
l$daily,
|
||||||
|
l$weekly,
|
||||||
|
l$monthly,
|
||||||
|
l$yearly,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class CopyWith$Input$AutobackupQuotasInput<TRes> {
|
||||||
|
factory CopyWith$Input$AutobackupQuotasInput(
|
||||||
|
Input$AutobackupQuotasInput instance,
|
||||||
|
TRes Function(Input$AutobackupQuotasInput) then,
|
||||||
|
) = _CopyWithImpl$Input$AutobackupQuotasInput;
|
||||||
|
|
||||||
|
factory CopyWith$Input$AutobackupQuotasInput.stub(TRes res) =
|
||||||
|
_CopyWithStubImpl$Input$AutobackupQuotasInput;
|
||||||
|
|
||||||
|
TRes call({
|
||||||
|
int? last,
|
||||||
|
int? daily,
|
||||||
|
int? weekly,
|
||||||
|
int? monthly,
|
||||||
|
int? yearly,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CopyWithImpl$Input$AutobackupQuotasInput<TRes>
|
||||||
|
implements CopyWith$Input$AutobackupQuotasInput<TRes> {
|
||||||
|
_CopyWithImpl$Input$AutobackupQuotasInput(
|
||||||
|
this._instance,
|
||||||
|
this._then,
|
||||||
|
);
|
||||||
|
|
||||||
|
final Input$AutobackupQuotasInput _instance;
|
||||||
|
|
||||||
|
final TRes Function(Input$AutobackupQuotasInput) _then;
|
||||||
|
|
||||||
|
static const _undefined = <dynamic, dynamic>{};
|
||||||
|
|
||||||
|
TRes call({
|
||||||
|
Object? last = _undefined,
|
||||||
|
Object? daily = _undefined,
|
||||||
|
Object? weekly = _undefined,
|
||||||
|
Object? monthly = _undefined,
|
||||||
|
Object? yearly = _undefined,
|
||||||
|
}) =>
|
||||||
|
_then(Input$AutobackupQuotasInput._({
|
||||||
|
..._instance._$data,
|
||||||
|
if (last != _undefined && last != null) 'last': (last as int),
|
||||||
|
if (daily != _undefined && daily != null) 'daily': (daily as int),
|
||||||
|
if (weekly != _undefined && weekly != null) 'weekly': (weekly as int),
|
||||||
|
if (monthly != _undefined && monthly != null)
|
||||||
|
'monthly': (monthly as int),
|
||||||
|
if (yearly != _undefined && yearly != null) 'yearly': (yearly as int),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CopyWithStubImpl$Input$AutobackupQuotasInput<TRes>
|
||||||
|
implements CopyWith$Input$AutobackupQuotasInput<TRes> {
|
||||||
|
_CopyWithStubImpl$Input$AutobackupQuotasInput(this._res);
|
||||||
|
|
||||||
|
TRes _res;
|
||||||
|
|
||||||
|
call({
|
||||||
|
int? last,
|
||||||
|
int? daily,
|
||||||
|
int? weekly,
|
||||||
|
int? monthly,
|
||||||
|
int? yearly,
|
||||||
|
}) =>
|
||||||
|
_res;
|
||||||
|
}
|
||||||
|
|
||||||
class Input$InitializeRepositoryInput {
|
class Input$InitializeRepositoryInput {
|
||||||
factory Input$InitializeRepositoryInput({
|
factory Input$InitializeRepositoryInput({
|
||||||
required Enum$BackupProvider provider,
|
required Enum$BackupProvider provider,
|
||||||
|
@ -1310,6 +1489,34 @@ Enum$BackupProvider fromJson$Enum$BackupProvider(String value) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum Enum$BackupReason { EXPLICIT, AUTO, PRE_RESTORE, $unknown }
|
||||||
|
|
||||||
|
String toJson$Enum$BackupReason(Enum$BackupReason e) {
|
||||||
|
switch (e) {
|
||||||
|
case Enum$BackupReason.EXPLICIT:
|
||||||
|
return r'EXPLICIT';
|
||||||
|
case Enum$BackupReason.AUTO:
|
||||||
|
return r'AUTO';
|
||||||
|
case Enum$BackupReason.PRE_RESTORE:
|
||||||
|
return r'PRE_RESTORE';
|
||||||
|
case Enum$BackupReason.$unknown:
|
||||||
|
return r'$unknown';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Enum$BackupReason fromJson$Enum$BackupReason(String value) {
|
||||||
|
switch (value) {
|
||||||
|
case r'EXPLICIT':
|
||||||
|
return Enum$BackupReason.EXPLICIT;
|
||||||
|
case r'AUTO':
|
||||||
|
return Enum$BackupReason.AUTO;
|
||||||
|
case r'PRE_RESTORE':
|
||||||
|
return Enum$BackupReason.PRE_RESTORE;
|
||||||
|
default:
|
||||||
|
return Enum$BackupReason.$unknown;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
enum Enum$DnsProvider { CLOUDFLARE, DIGITALOCEAN, DESEC, $unknown }
|
enum Enum$DnsProvider { CLOUDFLARE, DIGITALOCEAN, DESEC, $unknown }
|
||||||
|
|
||||||
String toJson$Enum$DnsProvider(Enum$DnsProvider e) {
|
String toJson$Enum$DnsProvider(Enum$DnsProvider e) {
|
||||||
|
|
|
@ -143,6 +143,51 @@ mixin BackupsApi on GraphQLApiMap {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<GenericResult> setAutobackupQuotas(
|
||||||
|
final AutobackupQuotas quotas,
|
||||||
|
) async {
|
||||||
|
QueryResult<Mutation$setAutobackupQuotas> response;
|
||||||
|
GenericResult? result;
|
||||||
|
|
||||||
|
try {
|
||||||
|
final GraphQLClient client = await getClient();
|
||||||
|
final variables = Variables$Mutation$setAutobackupQuotas(
|
||||||
|
quotas: Input$AutobackupQuotasInput(
|
||||||
|
last: quotas.last,
|
||||||
|
daily: quotas.daily,
|
||||||
|
weekly: quotas.weekly,
|
||||||
|
monthly: quotas.monthly,
|
||||||
|
yearly: quotas.yearly,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final options =
|
||||||
|
Options$Mutation$setAutobackupQuotas(variables: variables);
|
||||||
|
response = await client.mutate$setAutobackupQuotas(options);
|
||||||
|
if (response.hasException) {
|
||||||
|
final message = response.exception.toString();
|
||||||
|
print(message);
|
||||||
|
result = GenericResult(
|
||||||
|
success: false,
|
||||||
|
data: null,
|
||||||
|
message: message,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
result = GenericResult(
|
||||||
|
success: true,
|
||||||
|
data: null,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
print(e);
|
||||||
|
result = GenericResult(
|
||||||
|
success: false,
|
||||||
|
data: null,
|
||||||
|
message: e.toString(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
Future<GenericResult> removeRepository() async {
|
Future<GenericResult> removeRepository() async {
|
||||||
try {
|
try {
|
||||||
final GraphQLClient client = await getClient();
|
final GraphQLClient client = await getClient();
|
||||||
|
|
|
@ -2,6 +2,8 @@ import 'dart:io';
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:selfprivacy/config/get_it_config.dart';
|
import 'package:selfprivacy/config/get_it_config.dart';
|
||||||
|
import 'package:selfprivacy/logic/models/backup.dart';
|
||||||
|
import 'package:selfprivacy/logic/models/hive/backblaze_bucket.dart';
|
||||||
import 'package:selfprivacy/logic/models/hive/backups_credential.dart';
|
import 'package:selfprivacy/logic/models/hive/backups_credential.dart';
|
||||||
import 'package:selfprivacy/logic/api_maps/generic_result.dart';
|
import 'package:selfprivacy/logic/api_maps/generic_result.dart';
|
||||||
import 'package:selfprivacy/logic/api_maps/rest_maps/rest_api_map.dart';
|
import 'package:selfprivacy/logic/api_maps/rest_maps/rest_api_map.dart';
|
||||||
|
@ -179,6 +181,42 @@ class BackblazeApi extends RestApiMap {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<BackblazeBucket?> fetchBucket(
|
||||||
|
final BackupsCredential credentials,
|
||||||
|
final BackupConfiguration configuration,
|
||||||
|
) async {
|
||||||
|
BackblazeBucket? bucket;
|
||||||
|
final BackblazeApiAuth auth = await getAuthorizationToken();
|
||||||
|
final Dio client = await getClient();
|
||||||
|
client.options.baseUrl = auth.apiUrl;
|
||||||
|
final Response response = await client.get(
|
||||||
|
'$apiPrefix/b2_list_buckets',
|
||||||
|
queryParameters: {
|
||||||
|
'accountId': getIt<ApiConfigModel>().backblazeCredential!.keyId,
|
||||||
|
},
|
||||||
|
options: Options(
|
||||||
|
headers: {'Authorization': auth.authorizationToken},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
close(client);
|
||||||
|
if (response.statusCode == HttpStatus.ok) {
|
||||||
|
for (final rawBucket in response.data['buckets']) {
|
||||||
|
if (rawBucket['bucketId'] == configuration.locationId) {
|
||||||
|
bucket = BackblazeBucket(
|
||||||
|
bucketId: rawBucket['bucketId'],
|
||||||
|
bucketName: rawBucket['bucketName'],
|
||||||
|
encryptionKey: configuration.encryptionKey,
|
||||||
|
applicationKeyId: '',
|
||||||
|
applicationKey: '',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bucket;
|
||||||
|
} else {
|
||||||
|
throw Exception('code: ${response.statusCode}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool hasLogger;
|
bool hasLogger;
|
||||||
|
|
||||||
|
|
|
@ -320,7 +320,7 @@ class DigitalOceanApi extends RestApiMap {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<GenericResult<DigitalOceanVolume?>> createVolume() async {
|
Future<GenericResult<DigitalOceanVolume?>> createVolume(final int gb) async {
|
||||||
DigitalOceanVolume? volume;
|
DigitalOceanVolume? volume;
|
||||||
Response? createVolumeResponse;
|
Response? createVolumeResponse;
|
||||||
final Dio client = await getClient();
|
final Dio client = await getClient();
|
||||||
|
@ -330,7 +330,7 @@ class DigitalOceanApi extends RestApiMap {
|
||||||
createVolumeResponse = await client.post(
|
createVolumeResponse = await client.post(
|
||||||
'/volumes',
|
'/volumes',
|
||||||
data: {
|
data: {
|
||||||
'size_gigabytes': 10,
|
'size_gigabytes': gb,
|
||||||
'name': 'volume${StringGenerators.storageName()}',
|
'name': 'volume${StringGenerators.storageName()}',
|
||||||
'labels': {'labelkey': 'value'},
|
'labels': {'labelkey': 'value'},
|
||||||
'region': region,
|
'region': region,
|
||||||
|
@ -455,8 +455,8 @@ class DigitalOceanApi extends RestApiMap {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<GenericResult<bool>> resizeVolume(
|
Future<GenericResult<bool>> resizeVolume(
|
||||||
final String name,
|
final String uuid,
|
||||||
final DiskSize size,
|
final int gb,
|
||||||
) async {
|
) async {
|
||||||
bool success = false;
|
bool success = false;
|
||||||
|
|
||||||
|
@ -464,11 +464,10 @@ class DigitalOceanApi extends RestApiMap {
|
||||||
final Dio client = await getClient();
|
final Dio client = await getClient();
|
||||||
try {
|
try {
|
||||||
resizeVolumeResponse = await client.post(
|
resizeVolumeResponse = await client.post(
|
||||||
'/volumes/actions',
|
'/volumes/$uuid/actions',
|
||||||
data: {
|
data: {
|
||||||
'type': 'resize',
|
'type': 'resize',
|
||||||
'volume_name': name,
|
'size_gigabytes': gb,
|
||||||
'size_gigabytes': size.gibibyte,
|
|
||||||
'region': region,
|
'region': region,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
@ -320,8 +320,8 @@ class HetznerApi extends RestApiMap {
|
||||||
return GenericResult(success: true, data: null);
|
return GenericResult(success: true, data: null);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<GenericResult<double?>> getPricePerGb() async {
|
Future<GenericResult<HetznerPricing?>> getPricing() async {
|
||||||
double? price;
|
HetznerPricing? pricing;
|
||||||
|
|
||||||
final Response pricingResponse;
|
final Response pricingResponse;
|
||||||
final Dio client = await getClient();
|
final Dio client = await getClient();
|
||||||
|
@ -330,19 +330,34 @@ class HetznerApi extends RestApiMap {
|
||||||
|
|
||||||
final volume = pricingResponse.data['pricing']['volume'];
|
final volume = pricingResponse.data['pricing']['volume'];
|
||||||
final volumePrice = volume['price_per_gb_month']['gross'];
|
final volumePrice = volume['price_per_gb_month']['gross'];
|
||||||
price = double.parse(volumePrice);
|
final primaryIps = pricingResponse.data['pricing']['primary_ips'];
|
||||||
|
String? ipPrice;
|
||||||
|
for (final primaryIp in primaryIps) {
|
||||||
|
if (primaryIp['type'] == 'ipv4') {
|
||||||
|
for (final primaryIpPrice in primaryIp['prices']) {
|
||||||
|
if (primaryIpPrice['location'] == region!) {
|
||||||
|
ipPrice = primaryIpPrice['price_monthly']['gross'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pricing = HetznerPricing(
|
||||||
|
region!,
|
||||||
|
double.parse(volumePrice),
|
||||||
|
double.parse(ipPrice!),
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print(e);
|
print(e);
|
||||||
return GenericResult(
|
return GenericResult(
|
||||||
success: false,
|
success: false,
|
||||||
data: price,
|
data: pricing,
|
||||||
message: e.toString(),
|
message: e.toString(),
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
client.close();
|
client.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
return GenericResult(success: true, data: price);
|
return GenericResult(success: true, data: pricing);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<GenericResult<List<HetznerVolume>>> getVolumes({
|
Future<GenericResult<List<HetznerVolume>>> getVolumes({
|
||||||
|
@ -381,7 +396,7 @@ class HetznerApi extends RestApiMap {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<GenericResult<HetznerVolume?>> createVolume() async {
|
Future<GenericResult<HetznerVolume?>> createVolume(final int gb) async {
|
||||||
Response? createVolumeResponse;
|
Response? createVolumeResponse;
|
||||||
HetznerVolume? volume;
|
HetznerVolume? volume;
|
||||||
final Dio client = await getClient();
|
final Dio client = await getClient();
|
||||||
|
@ -389,7 +404,7 @@ class HetznerApi extends RestApiMap {
|
||||||
createVolumeResponse = await client.post(
|
createVolumeResponse = await client.post(
|
||||||
'/volumes',
|
'/volumes',
|
||||||
data: {
|
data: {
|
||||||
'size': 10,
|
'size': gb,
|
||||||
'name': StringGenerators.storageName(),
|
'name': StringGenerators.storageName(),
|
||||||
'labels': {'labelkey': 'value'},
|
'labels': {'labelkey': 'value'},
|
||||||
'location': region,
|
'location': region,
|
||||||
|
|
|
@ -36,6 +36,7 @@ class BackupsCubit extends ServerInstallationDependendCubit<BackupsState> {
|
||||||
backblazeBucket: bucket,
|
backblazeBucket: bucket,
|
||||||
isInitialized: backupConfig?.isInitialized,
|
isInitialized: backupConfig?.isInitialized,
|
||||||
autobackupPeriod: backupConfig?.autobackupPeriod ?? Duration.zero,
|
autobackupPeriod: backupConfig?.autobackupPeriod ?? Duration.zero,
|
||||||
|
autobackupQuotas: backupConfig?.autobackupQuotas,
|
||||||
backups: backups,
|
backups: backups,
|
||||||
preventActions: false,
|
preventActions: false,
|
||||||
refreshing: false,
|
refreshing: false,
|
||||||
|
@ -109,17 +110,34 @@ class BackupsCubit extends ServerInstallationDependendCubit<BackupsState> {
|
||||||
|
|
||||||
Future<void> reuploadKey() async {
|
Future<void> reuploadKey() async {
|
||||||
emit(state.copyWith(preventActions: true));
|
emit(state.copyWith(preventActions: true));
|
||||||
final BackblazeBucket? bucket = getIt<ApiConfigModel>().backblazeBucket;
|
BackblazeBucket? bucket = getIt<ApiConfigModel>().backblazeBucket;
|
||||||
if (bucket == null) {
|
if (bucket == null) {
|
||||||
emit(state.copyWith(isInitialized: false));
|
emit(state.copyWith(isInitialized: false));
|
||||||
} else {
|
} else {
|
||||||
|
String login = bucket.applicationKeyId;
|
||||||
|
String password = bucket.applicationKey;
|
||||||
|
if (login.isEmpty || password.isEmpty) {
|
||||||
|
final BackblazeApplicationKey key =
|
||||||
|
await backblaze.createKey(bucket.bucketId);
|
||||||
|
login = key.applicationKeyId;
|
||||||
|
password = key.applicationKey;
|
||||||
|
bucket = BackblazeBucket(
|
||||||
|
bucketId: bucket.bucketId,
|
||||||
|
bucketName: bucket.bucketName,
|
||||||
|
encryptionKey: bucket.encryptionKey,
|
||||||
|
applicationKey: password,
|
||||||
|
applicationKeyId: login,
|
||||||
|
);
|
||||||
|
await getIt<ApiConfigModel>().storeBackblazeBucket(bucket);
|
||||||
|
emit(state.copyWith(backblazeBucket: bucket));
|
||||||
|
}
|
||||||
final GenericResult result = await api.initializeRepository(
|
final GenericResult result = await api.initializeRepository(
|
||||||
InitializeRepositoryInput(
|
InitializeRepositoryInput(
|
||||||
provider: BackupsProviderType.backblaze,
|
provider: BackupsProviderType.backblaze,
|
||||||
locationId: bucket.bucketId,
|
locationId: bucket.bucketId,
|
||||||
locationName: bucket.bucketName,
|
locationName: bucket.bucketName,
|
||||||
login: bucket.applicationKeyId,
|
login: login,
|
||||||
password: bucket.applicationKey,
|
password: password,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
if (result.success == false) {
|
if (result.success == false) {
|
||||||
|
@ -129,7 +147,7 @@ class BackupsCubit extends ServerInstallationDependendCubit<BackupsState> {
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
emit(state.copyWith(preventActions: false));
|
emit(state.copyWith(preventActions: false));
|
||||||
getIt<NavigationService>().showSnackBar('backup.reuploaded_key');
|
getIt<NavigationService>().showSnackBar('backup.reuploaded_key'.tr());
|
||||||
await updateBackups();
|
await updateBackups();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -151,6 +169,7 @@ class BackupsCubit extends ServerInstallationDependendCubit<BackupsState> {
|
||||||
refreshing: false,
|
refreshing: false,
|
||||||
isInitialized: backupConfig?.isInitialized ?? false,
|
isInitialized: backupConfig?.isInitialized ?? false,
|
||||||
autobackupPeriod: backupConfig?.autobackupPeriod,
|
autobackupPeriod: backupConfig?.autobackupPeriod,
|
||||||
|
autobackupQuotas: backupConfig?.autobackupQuotas,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
if (useTimer) {
|
if (useTimer) {
|
||||||
|
@ -210,6 +229,25 @@ class BackupsCubit extends ServerInstallationDependendCubit<BackupsState> {
|
||||||
await updateBackups();
|
await updateBackups();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> setAutobackupQuotas(final AutobackupQuotas quotas) async {
|
||||||
|
emit(state.copyWith(preventActions: true));
|
||||||
|
final result = await api.setAutobackupQuotas(quotas);
|
||||||
|
if (result.success == false) {
|
||||||
|
getIt<NavigationService>()
|
||||||
|
.showSnackBar(result.message ?? 'Unknown error');
|
||||||
|
emit(state.copyWith(preventActions: false));
|
||||||
|
} else {
|
||||||
|
getIt<NavigationService>().showSnackBar('backup.quotas_set'.tr());
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
preventActions: false,
|
||||||
|
autobackupQuotas: quotas,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await updateBackups();
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> forgetSnapshot(final String snapshotId) async {
|
Future<void> forgetSnapshot(final String snapshotId) async {
|
||||||
final result = await api.forgetSnapshot(snapshotId);
|
final result = await api.forgetSnapshot(snapshotId);
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
|
|
|
@ -9,6 +9,7 @@ class BackupsState extends ServerInstallationDependendState {
|
||||||
this.refreshing = true,
|
this.refreshing = true,
|
||||||
this.autobackupPeriod,
|
this.autobackupPeriod,
|
||||||
this.backblazeBucket,
|
this.backblazeBucket,
|
||||||
|
this.autobackupQuotas,
|
||||||
});
|
});
|
||||||
|
|
||||||
final bool isInitialized;
|
final bool isInitialized;
|
||||||
|
@ -18,6 +19,7 @@ class BackupsState extends ServerInstallationDependendState {
|
||||||
final bool refreshing;
|
final bool refreshing;
|
||||||
final Duration? autobackupPeriod;
|
final Duration? autobackupPeriod;
|
||||||
final BackblazeBucket? backblazeBucket;
|
final BackblazeBucket? backblazeBucket;
|
||||||
|
final AutobackupQuotas? autobackupQuotas;
|
||||||
|
|
||||||
List<Backup> serviceBackups(final String serviceId) => backups
|
List<Backup> serviceBackups(final String serviceId) => backups
|
||||||
.where((final backup) => backup.serviceId == serviceId)
|
.where((final backup) => backup.serviceId == serviceId)
|
||||||
|
@ -40,6 +42,7 @@ class BackupsState extends ServerInstallationDependendState {
|
||||||
final bool? refreshing,
|
final bool? refreshing,
|
||||||
final Duration? autobackupPeriod,
|
final Duration? autobackupPeriod,
|
||||||
final BackblazeBucket? backblazeBucket,
|
final BackblazeBucket? backblazeBucket,
|
||||||
|
final AutobackupQuotas? autobackupQuotas,
|
||||||
}) =>
|
}) =>
|
||||||
BackupsState(
|
BackupsState(
|
||||||
isInitialized: isInitialized ?? this.isInitialized,
|
isInitialized: isInitialized ?? this.isInitialized,
|
||||||
|
@ -53,5 +56,6 @@ class BackupsState extends ServerInstallationDependendState {
|
||||||
? null
|
? null
|
||||||
: autobackupPeriod ?? this.autobackupPeriod,
|
: autobackupPeriod ?? this.autobackupPeriod,
|
||||||
backblazeBucket: backblazeBucket ?? this.backblazeBucket,
|
backblazeBucket: backblazeBucket ?? this.backblazeBucket,
|
||||||
|
autobackupQuotas: autobackupQuotas ?? this.autobackupQuotas,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -75,7 +75,7 @@ class DnsRecordsCubit
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> clear() async {
|
Future<void> clear() async {
|
||||||
emit(const DnsRecordsState(dnsState: DnsRecordsStatus.error));
|
emit(const DnsRecordsState(dnsState: DnsRecordsStatus.uninitialized));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> refresh() async {
|
Future<void> refresh() async {
|
||||||
|
|
|
@ -18,13 +18,11 @@ class DomainSetupCubit extends Cubit<DomainSetupState> {
|
||||||
} else if (result.data.length == 1) {
|
} else if (result.data.length == 1) {
|
||||||
emit(Loaded(result.data.first));
|
emit(Loaded(result.data.first));
|
||||||
} else {
|
} else {
|
||||||
emit(MoreThenOne());
|
emit(MoreThenOne(result.data));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> saveDomain() async {
|
Future<void> saveDomain(final String domainName) async {
|
||||||
assert(state is Loaded, 'wrong state');
|
|
||||||
final String domainName = (state as Loaded).domain;
|
|
||||||
emit(Loading(LoadingTypes.saving));
|
emit(Loading(LoadingTypes.saving));
|
||||||
|
|
||||||
final dnsProvider = ProvidersController.currentDnsProvider!;
|
final dnsProvider = ProvidersController.currentDnsProvider!;
|
||||||
|
@ -45,7 +43,10 @@ class Initial extends DomainSetupState {}
|
||||||
|
|
||||||
class Empty extends DomainSetupState {}
|
class Empty extends DomainSetupState {}
|
||||||
|
|
||||||
class MoreThenOne extends DomainSetupState {}
|
class MoreThenOne extends DomainSetupState {
|
||||||
|
MoreThenOne(this.domains);
|
||||||
|
final List<String> domains;
|
||||||
|
}
|
||||||
|
|
||||||
class Loading extends DomainSetupState {
|
class Loading extends DomainSetupState {
|
||||||
Loading(this.type);
|
Loading(this.type);
|
||||||
|
|
|
@ -12,7 +12,7 @@ class SshFormCubit extends FormCubit {
|
||||||
required this.user,
|
required this.user,
|
||||||
}) {
|
}) {
|
||||||
final RegExp keyRegExp = RegExp(
|
final RegExp keyRegExp = RegExp(
|
||||||
r'^(ssh-rsa AAAAB3NzaC1yc2|ssh-ed25519 AAAAC3NzaC1lZDI1NTE5)[0-9A-Za-z+/]+[=]{0,3}( .*)?$',
|
r'^(ecdsa-sha2-nistp256 AAAAE2VjZH|ssh-rsa AAAAB3NzaC1yc2|ssh-ed25519 AAAAC3NzaC1lZDI1NTE5)[0-9A-Za-z+/]+[=]{0,3}( .*)?$',
|
||||||
);
|
);
|
||||||
|
|
||||||
key = FieldCubit(
|
key = FieldCubit(
|
||||||
|
|
|
@ -26,8 +26,17 @@ class ApiProviderVolumeCubit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Price?> getPricePerGb() async =>
|
Future<Price?> getPricePerGb() async {
|
||||||
(await ProvidersController.currentServerProvider!.getPricePerGb()).data;
|
Price? price;
|
||||||
|
final pricingResult =
|
||||||
|
await ProvidersController.currentServerProvider!.getAdditionalPricing();
|
||||||
|
if (pricingResult.data == null || !pricingResult.success) {
|
||||||
|
getIt<NavigationService>().showSnackBar('server.pricing_error'.tr());
|
||||||
|
return price;
|
||||||
|
}
|
||||||
|
price = pricingResult.data!.perVolumeGb;
|
||||||
|
return price;
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> refresh() async {
|
Future<void> refresh() async {
|
||||||
emit(const ApiProviderVolumeState([], LoadingStatus.refreshing, false));
|
emit(const ApiProviderVolumeState([], LoadingStatus.refreshing, false));
|
||||||
|
@ -113,9 +122,11 @@ class ApiProviderVolumeCubit
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> createVolume() async {
|
Future<void> createVolume(final DiskSize size) async {
|
||||||
final ServerVolume? volume =
|
final ServerVolume? volume = (await ProvidersController
|
||||||
(await ProvidersController.currentServerProvider!.createVolume()).data;
|
.currentServerProvider!
|
||||||
|
.createVolume(size.gibibyte.toInt()))
|
||||||
|
.data;
|
||||||
|
|
||||||
final diskVolume = DiskVolume(providerVolume: volume);
|
final diskVolume = DiskVolume(providerVolume: volume);
|
||||||
await attachVolume(diskVolume);
|
await attachVolume(diskVolume);
|
||||||
|
|
|
@ -5,7 +5,10 @@ import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:selfprivacy/config/get_it_config.dart';
|
import 'package:selfprivacy/config/get_it_config.dart';
|
||||||
import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server_api.dart';
|
import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server_api.dart';
|
||||||
|
import 'package:selfprivacy/logic/api_maps/rest_maps/backblaze.dart';
|
||||||
import 'package:selfprivacy/logic/api_maps/tls_options.dart';
|
import 'package:selfprivacy/logic/api_maps/tls_options.dart';
|
||||||
|
import 'package:selfprivacy/logic/models/disk_size.dart';
|
||||||
|
import 'package:selfprivacy/logic/models/hive/backblaze_bucket.dart';
|
||||||
import 'package:selfprivacy/logic/models/hive/backups_credential.dart';
|
import 'package:selfprivacy/logic/models/hive/backups_credential.dart';
|
||||||
import 'package:selfprivacy/logic/models/callback_dialogue_branching.dart';
|
import 'package:selfprivacy/logic/models/callback_dialogue_branching.dart';
|
||||||
import 'package:selfprivacy/logic/models/hive/server_details.dart';
|
import 'package:selfprivacy/logic/models/hive/server_details.dart';
|
||||||
|
@ -13,6 +16,7 @@ import 'package:selfprivacy/logic/models/hive/user.dart';
|
||||||
import 'package:selfprivacy/logic/models/launch_installation_data.dart';
|
import 'package:selfprivacy/logic/models/launch_installation_data.dart';
|
||||||
import 'package:selfprivacy/logic/models/hive/server_domain.dart';
|
import 'package:selfprivacy/logic/models/hive/server_domain.dart';
|
||||||
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_repository.dart';
|
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_repository.dart';
|
||||||
|
import 'package:selfprivacy/logic/models/price.dart';
|
||||||
import 'package:selfprivacy/logic/models/server_basic_info.dart';
|
import 'package:selfprivacy/logic/models/server_basic_info.dart';
|
||||||
import 'package:selfprivacy/logic/models/server_provider_location.dart';
|
import 'package:selfprivacy/logic/models/server_provider_location.dart';
|
||||||
import 'package:selfprivacy/logic/models/server_type.dart';
|
import 'package:selfprivacy/logic/models/server_type.dart';
|
||||||
|
@ -32,6 +36,8 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
|
||||||
|
|
||||||
Timer? timer;
|
Timer? timer;
|
||||||
|
|
||||||
|
final DiskSize initialStorage = DiskSize.fromGibibyte(10);
|
||||||
|
|
||||||
Future<void> load() async {
|
Future<void> load() async {
|
||||||
final ServerInstallationState state = await repository.load();
|
final ServerInstallationState state = await repository.load();
|
||||||
|
|
||||||
|
@ -147,6 +153,19 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
|
||||||
return apiResult.data;
|
return apiResult.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<AdditionalPricing?> fetchAvailableAdditionalPricing() async {
|
||||||
|
AdditionalPricing? prices;
|
||||||
|
final pricingResult =
|
||||||
|
await ProvidersController.currentServerProvider!.getAdditionalPricing();
|
||||||
|
if (pricingResult.data == null || !pricingResult.success) {
|
||||||
|
getIt<NavigationService>().showSnackBar('server.pricing_error'.tr());
|
||||||
|
return prices;
|
||||||
|
}
|
||||||
|
|
||||||
|
prices = pricingResult.data;
|
||||||
|
return prices;
|
||||||
|
}
|
||||||
|
|
||||||
void setServerProviderKey(final String serverProviderKey) async {
|
void setServerProviderKey(final String serverProviderKey) async {
|
||||||
await repository.saveServerProviderKey(serverProviderKey);
|
await repository.saveServerProviderKey(serverProviderKey);
|
||||||
|
|
||||||
|
@ -167,12 +186,14 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> setLocationIdentifier(final String locationId) async {
|
||||||
|
await ProvidersController.currentServerProvider!
|
||||||
|
.trySetServerLocation(locationId);
|
||||||
|
}
|
||||||
|
|
||||||
void setServerType(final ServerType serverType) async {
|
void setServerType(final ServerType serverType) async {
|
||||||
await repository.saveServerType(serverType);
|
await repository.saveServerType(serverType);
|
||||||
|
|
||||||
await ProvidersController.currentServerProvider!
|
|
||||||
.trySetServerLocation(serverType.location.identifier);
|
|
||||||
|
|
||||||
emit(
|
emit(
|
||||||
(state as ServerInstallationNotFinished).copyWith(
|
(state as ServerInstallationNotFinished).copyWith(
|
||||||
serverTypeIdentificator: serverType.identifier,
|
serverTypeIdentificator: serverType.identifier,
|
||||||
|
@ -199,8 +220,23 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
|
||||||
applicationKey: applicationKey,
|
applicationKey: applicationKey,
|
||||||
provider: BackupsProviderType.backblaze,
|
provider: BackupsProviderType.backblaze,
|
||||||
);
|
);
|
||||||
|
final BackblazeBucket? bucket;
|
||||||
await repository.saveBackblazeKey(backblazeCredential);
|
await repository.saveBackblazeKey(backblazeCredential);
|
||||||
if (state is ServerInstallationRecovery) {
|
if (state is ServerInstallationRecovery) {
|
||||||
|
final configuration = await ServerApi(
|
||||||
|
customToken:
|
||||||
|
(state as ServerInstallationRecovery).serverDetails!.apiToken,
|
||||||
|
isWithToken: true,
|
||||||
|
).getBackupsConfiguration();
|
||||||
|
if (configuration != null) {
|
||||||
|
try {
|
||||||
|
bucket = await BackblazeApi()
|
||||||
|
.fetchBucket(backblazeCredential, configuration);
|
||||||
|
await getIt<ApiConfigModel>().storeBackblazeBucket(bucket!);
|
||||||
|
} catch (e) {
|
||||||
|
print(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
finishRecoveryProcess(backblazeCredential);
|
finishRecoveryProcess(backblazeCredential);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -257,6 +293,7 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
|
||||||
serverTypeId: state.serverTypeIdentificator!,
|
serverTypeId: state.serverTypeIdentificator!,
|
||||||
errorCallback: clearAppConfig,
|
errorCallback: clearAppConfig,
|
||||||
successCallback: onCreateServerSuccess,
|
successCallback: onCreateServerSuccess,
|
||||||
|
storageSize: initialStorage,
|
||||||
);
|
);
|
||||||
|
|
||||||
final result =
|
final result =
|
||||||
|
|
|
@ -155,7 +155,7 @@ class ServerInstallationRepository {
|
||||||
|
|
||||||
RecoveryStep _getCurrentRecoveryStep(
|
RecoveryStep _getCurrentRecoveryStep(
|
||||||
final String? serverProviderToken,
|
final String? serverProviderToken,
|
||||||
final String? cloudflareToken,
|
final String? dnsProviderToken,
|
||||||
final ServerDomain serverDomain,
|
final ServerDomain serverDomain,
|
||||||
final ServerHostingDetails? serverDetails,
|
final ServerHostingDetails? serverDetails,
|
||||||
) {
|
) {
|
||||||
|
@ -209,7 +209,7 @@ class ServerInstallationRepository {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return domain == domainResult.data[0];
|
return domainResult.data.contains(domain);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Map<String, bool>> isDnsAddressesMatch(
|
Future<Map<String, bool>> isDnsAddressesMatch(
|
||||||
|
@ -218,24 +218,28 @@ class ServerInstallationRepository {
|
||||||
final Map<String, bool> skippedMatches,
|
final Map<String, bool> skippedMatches,
|
||||||
) async {
|
) async {
|
||||||
final Map<String, bool> matches = <String, bool>{};
|
final Map<String, bool> matches = <String, bool>{};
|
||||||
await InternetAddress.lookup(domainName!).then(
|
try {
|
||||||
(final records) {
|
await InternetAddress.lookup(domainName!).then(
|
||||||
for (final record in records) {
|
(final records) {
|
||||||
if (skippedMatches[record.host] ?? false) {
|
for (final record in records) {
|
||||||
matches[record.host] = true;
|
if (skippedMatches[record.host] ?? false) {
|
||||||
continue;
|
matches[record.host] = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (record.address == ip4!) {
|
||||||
|
matches[record.host] = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (record.address == ip4!) {
|
},
|
||||||
matches[record.host] = true;
|
);
|
||||||
}
|
} catch (e) {
|
||||||
}
|
print(e);
|
||||||
},
|
}
|
||||||
);
|
|
||||||
|
|
||||||
return matches;
|
return matches;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> createDkimRecord(final ServerDomain cloudFlareDomain) async {
|
Future<void> createDkimRecord(final ServerDomain domain) async {
|
||||||
final ServerApi api = ServerApi();
|
final ServerApi api = ServerApi();
|
||||||
|
|
||||||
late DnsRecord record;
|
late DnsRecord record;
|
||||||
|
@ -248,7 +252,7 @@ class ServerInstallationRepository {
|
||||||
|
|
||||||
await ProvidersController.currentDnsProvider!.setDnsRecord(
|
await ProvidersController.currentDnsProvider!.setDnsRecord(
|
||||||
record,
|
record,
|
||||||
cloudFlareDomain,
|
domain,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,32 @@ class UsersState extends ServerInstallationDependendState {
|
||||||
@override
|
@override
|
||||||
List<Object> get props => [users, isLoading];
|
List<Object> get props => [users, isLoading];
|
||||||
|
|
||||||
|
/// Makes a copy of existing users list, but places 'primary'
|
||||||
|
/// to the beginning and sorts the rest alphabetically
|
||||||
|
///
|
||||||
|
/// If found a 'root' user, it doesn't get copied into the result
|
||||||
|
List<User> get orderedUsers {
|
||||||
|
User? primaryUser;
|
||||||
|
final List<User> normalUsers = [];
|
||||||
|
for (final User user in users) {
|
||||||
|
if (user.type == UserType.primary) {
|
||||||
|
primaryUser = user;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (user.type == UserType.root) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
normalUsers.add(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
normalUsers.sort(
|
||||||
|
(final User a, final User b) =>
|
||||||
|
a.login.toLowerCase().compareTo(b.login.toLowerCase()),
|
||||||
|
);
|
||||||
|
|
||||||
|
return primaryUser == null ? normalUsers : [primaryUser] + normalUsers;
|
||||||
|
}
|
||||||
|
|
||||||
UsersState copyWith({
|
UsersState copyWith({
|
||||||
final List<User>? users,
|
final List<User>? users,
|
||||||
final bool? isLoading,
|
final bool? isLoading,
|
||||||
|
|
|
@ -21,11 +21,12 @@ class NavigationService {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void showSnackBar(final String text) {
|
void showSnackBar(final String text, {final SnackBarBehavior? behavior}) {
|
||||||
final ScaffoldMessengerState state = scaffoldMessengerKey.currentState!;
|
final ScaffoldMessengerState state = scaffoldMessengerKey.currentState!;
|
||||||
final SnackBar snack = SnackBar(
|
final SnackBar snack = SnackBar(
|
||||||
content: Text(text),
|
content: Text(text),
|
||||||
duration: const Duration(seconds: 2),
|
duration: const Duration(seconds: 2),
|
||||||
|
behavior: behavior,
|
||||||
);
|
);
|
||||||
state.showSnackBar(snack);
|
state.showSnackBar(snack);
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ class Backup {
|
||||||
time: snapshot.createdAt,
|
time: snapshot.createdAt,
|
||||||
serviceId: snapshot.service.id,
|
serviceId: snapshot.service.id,
|
||||||
fallbackServiceName: snapshot.service.displayName,
|
fallbackServiceName: snapshot.service.displayName,
|
||||||
|
reason: snapshot.reason,
|
||||||
);
|
);
|
||||||
|
|
||||||
Backup({
|
Backup({
|
||||||
|
@ -18,6 +19,7 @@ class Backup {
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.serviceId,
|
required this.serviceId,
|
||||||
required this.fallbackServiceName,
|
required this.fallbackServiceName,
|
||||||
|
required this.reason,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Time of the backup
|
// Time of the backup
|
||||||
|
@ -26,6 +28,16 @@ class Backup {
|
||||||
final String id;
|
final String id;
|
||||||
final String serviceId;
|
final String serviceId;
|
||||||
final String fallbackServiceName;
|
final String fallbackServiceName;
|
||||||
|
final Enum$BackupReason reason;
|
||||||
|
}
|
||||||
|
|
||||||
|
extension BackupReasonExtension on Enum$BackupReason {
|
||||||
|
String get displayName => switch (this) {
|
||||||
|
Enum$BackupReason.AUTO => 'backup.snapshot_reasons.auto',
|
||||||
|
Enum$BackupReason.EXPLICIT => 'backup.snapshot_reasons.explicit',
|
||||||
|
Enum$BackupReason.PRE_RESTORE => 'backup.snapshot_reasons.pre_restore',
|
||||||
|
Enum$BackupReason.$unknown => 'backup.snapshot_reasons.unknown',
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
class BackupConfiguration {
|
class BackupConfiguration {
|
||||||
|
@ -41,6 +53,9 @@ class BackupConfiguration {
|
||||||
locationId: configuration.locationId,
|
locationId: configuration.locationId,
|
||||||
locationName: configuration.locationName,
|
locationName: configuration.locationName,
|
||||||
provider: BackupsProviderType.fromGraphQL(configuration.provider),
|
provider: BackupsProviderType.fromGraphQL(configuration.provider),
|
||||||
|
autobackupQuotas: AutobackupQuotas.fromGraphQL(
|
||||||
|
configuration.autobackupQuotas,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
BackupConfiguration({
|
BackupConfiguration({
|
||||||
|
@ -50,6 +65,7 @@ class BackupConfiguration {
|
||||||
required this.locationId,
|
required this.locationId,
|
||||||
required this.locationName,
|
required this.locationName,
|
||||||
required this.provider,
|
required this.provider,
|
||||||
|
required this.autobackupQuotas,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Duration? autobackupPeriod;
|
final Duration? autobackupPeriod;
|
||||||
|
@ -58,6 +74,49 @@ class BackupConfiguration {
|
||||||
final String? locationId;
|
final String? locationId;
|
||||||
final String? locationName;
|
final String? locationName;
|
||||||
final BackupsProviderType provider;
|
final BackupsProviderType provider;
|
||||||
|
final AutobackupQuotas autobackupQuotas;
|
||||||
|
}
|
||||||
|
|
||||||
|
class AutobackupQuotas {
|
||||||
|
AutobackupQuotas.fromGraphQL(
|
||||||
|
final Query$BackupConfiguration$backup$configuration$autobackupQuotas
|
||||||
|
autobackupQuotas,
|
||||||
|
) : this(
|
||||||
|
last: autobackupQuotas.last,
|
||||||
|
daily: autobackupQuotas.daily,
|
||||||
|
weekly: autobackupQuotas.weekly,
|
||||||
|
monthly: autobackupQuotas.monthly,
|
||||||
|
yearly: autobackupQuotas.yearly,
|
||||||
|
);
|
||||||
|
|
||||||
|
AutobackupQuotas({
|
||||||
|
required this.last,
|
||||||
|
required this.daily,
|
||||||
|
required this.weekly,
|
||||||
|
required this.monthly,
|
||||||
|
required this.yearly,
|
||||||
|
});
|
||||||
|
|
||||||
|
final int last;
|
||||||
|
final int daily;
|
||||||
|
final int weekly;
|
||||||
|
final int monthly;
|
||||||
|
final int yearly;
|
||||||
|
|
||||||
|
AutobackupQuotas copyWith({
|
||||||
|
final int? last,
|
||||||
|
final int? daily,
|
||||||
|
final int? weekly,
|
||||||
|
final int? monthly,
|
||||||
|
final int? yearly,
|
||||||
|
}) =>
|
||||||
|
AutobackupQuotas(
|
||||||
|
last: last ?? this.last,
|
||||||
|
daily: daily ?? this.daily,
|
||||||
|
weekly: weekly ?? this.weekly,
|
||||||
|
monthly: monthly ?? this.monthly,
|
||||||
|
yearly: yearly ?? this.yearly,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
enum BackupRestoreStrategy {
|
enum BackupRestoreStrategy {
|
||||||
|
|
|
@ -190,3 +190,22 @@ class HetznerVolume {
|
||||||
static HetznerVolume fromJson(final Map<String, dynamic> json) =>
|
static HetznerVolume fromJson(final Map<String, dynamic> json) =>
|
||||||
_$HetznerVolumeFromJson(json);
|
_$HetznerVolumeFromJson(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Prices for Hetzner resources in Euro (monthly).
|
||||||
|
/// https://docs.hetzner.cloud/#pricing
|
||||||
|
class HetznerPricing {
|
||||||
|
HetznerPricing(
|
||||||
|
this.region,
|
||||||
|
this.perVolumeGb,
|
||||||
|
this.perPublicIpv4,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Region name to which current price listing applies
|
||||||
|
final String region;
|
||||||
|
|
||||||
|
/// The cost of Volume per GB/month
|
||||||
|
final double perVolumeGb;
|
||||||
|
|
||||||
|
/// Costs of Primary IP type
|
||||||
|
final double perPublicIpv4;
|
||||||
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import 'package:selfprivacy/logic/models/disk_size.dart';
|
||||||
import 'package:selfprivacy/logic/models/hive/server_details.dart';
|
import 'package:selfprivacy/logic/models/hive/server_details.dart';
|
||||||
import 'package:selfprivacy/logic/models/hive/server_domain.dart';
|
import 'package:selfprivacy/logic/models/hive/server_domain.dart';
|
||||||
import 'package:selfprivacy/logic/models/hive/user.dart';
|
import 'package:selfprivacy/logic/models/hive/user.dart';
|
||||||
|
@ -11,6 +12,7 @@ class LaunchInstallationData {
|
||||||
required this.serverTypeId,
|
required this.serverTypeId,
|
||||||
required this.errorCallback,
|
required this.errorCallback,
|
||||||
required this.successCallback,
|
required this.successCallback,
|
||||||
|
required this.storageSize,
|
||||||
});
|
});
|
||||||
|
|
||||||
final User rootUser;
|
final User rootUser;
|
||||||
|
@ -20,4 +22,5 @@ class LaunchInstallationData {
|
||||||
final String serverTypeId;
|
final String serverTypeId;
|
||||||
final Function() errorCallback;
|
final Function() errorCallback;
|
||||||
final Function(ServerHostingDetails details) successCallback;
|
final Function(ServerHostingDetails details) successCallback;
|
||||||
|
final DiskSize storageSize;
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,3 +53,12 @@ class Currency {
|
||||||
final String? fontcode;
|
final String? fontcode;
|
||||||
final String? symbol;
|
final String? symbol;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class AdditionalPricing {
|
||||||
|
AdditionalPricing({
|
||||||
|
required this.perVolumeGb,
|
||||||
|
required this.perPublicIpv4,
|
||||||
|
});
|
||||||
|
final Price perVolumeGb;
|
||||||
|
final Price perPublicIpv4;
|
||||||
|
}
|
||||||
|
|
|
@ -56,7 +56,7 @@ class Service {
|
||||||
/// TODO Turn loginInfo into dynamic data, not static!
|
/// TODO Turn loginInfo into dynamic data, not static!
|
||||||
String get loginInfo {
|
String get loginInfo {
|
||||||
switch (id) {
|
switch (id) {
|
||||||
case 'mailserver':
|
case 'email':
|
||||||
return 'mail.login_info'.tr();
|
return 'mail.login_info'.tr();
|
||||||
case 'bitwarden':
|
case 'bitwarden':
|
||||||
return 'password_manager.login_info'.tr();
|
return 'password_manager.login_info'.tr();
|
||||||
|
|
|
@ -69,13 +69,6 @@ class CloudflareDnsProvider extends DnsProvider {
|
||||||
)
|
)
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
/// TODO: Remove when domain selection for more than one domain on account is implemented, move cachedZoneId writing to domain saving method
|
|
||||||
_adapter = ApiAdapter(
|
|
||||||
isWithToken: true,
|
|
||||||
cachedDomain: result.data[0].name,
|
|
||||||
cachedZoneId: result.data[0].id,
|
|
||||||
);
|
|
||||||
|
|
||||||
return GenericResult(
|
return GenericResult(
|
||||||
success: true,
|
success: true,
|
||||||
data: domains,
|
data: domains,
|
||||||
|
|
|
@ -254,7 +254,9 @@ class DigitalOceanServerProvider extends ServerProvider {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final int dropletId = serverResult.data!;
|
final int dropletId = serverResult.data!;
|
||||||
final newVolume = (await createVolume()).data;
|
final newVolume =
|
||||||
|
(await createVolume(installationData.storageSize.gibibyte.toInt()))
|
||||||
|
.data;
|
||||||
final bool attachedVolume = (await _adapter.api().attachVolume(
|
final bool attachedVolume = (await _adapter.api().attachVolume(
|
||||||
newVolume!.name,
|
newVolume!.name,
|
||||||
dropletId,
|
dropletId,
|
||||||
|
@ -527,14 +529,20 @@ class DigitalOceanServerProvider extends ServerProvider {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Hardcoded on their documentation and there is no pricing API at all
|
|
||||||
/// Probably we should scrap the doc page manually
|
|
||||||
@override
|
@override
|
||||||
Future<GenericResult<Price?>> getPricePerGb() async => GenericResult(
|
Future<GenericResult<AdditionalPricing?>> getAdditionalPricing() async =>
|
||||||
|
GenericResult(
|
||||||
success: true,
|
success: true,
|
||||||
data: Price(
|
data: AdditionalPricing(
|
||||||
value: 0.10,
|
perVolumeGb: Price(
|
||||||
currency: currency,
|
/// Hardcoded in their documentation and there is no pricing API
|
||||||
|
value: 0.10,
|
||||||
|
currency: currency,
|
||||||
|
),
|
||||||
|
perPublicIpv4: Price(
|
||||||
|
value: 0,
|
||||||
|
currency: currency,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -588,10 +596,10 @@ class DigitalOceanServerProvider extends ServerProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<GenericResult<ServerVolume?>> createVolume() async {
|
Future<GenericResult<ServerVolume?>> createVolume(final int gb) async {
|
||||||
ServerVolume? volume;
|
ServerVolume? volume;
|
||||||
|
|
||||||
final result = await _adapter.api().createVolume();
|
final result = await _adapter.api().createVolume(gb);
|
||||||
|
|
||||||
if (!result.success || result.data == null) {
|
if (!result.success || result.data == null) {
|
||||||
return GenericResult(
|
return GenericResult(
|
||||||
|
@ -690,8 +698,8 @@ class DigitalOceanServerProvider extends ServerProvider {
|
||||||
final DiskSize size,
|
final DiskSize size,
|
||||||
) async =>
|
) async =>
|
||||||
_adapter.api().resizeVolume(
|
_adapter.api().resizeVolume(
|
||||||
volume.name,
|
volume.uuid!,
|
||||||
size,
|
size.gibibyte.toInt(),
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -708,13 +716,37 @@ class DigitalOceanServerProvider extends ServerProvider {
|
||||||
message: result.message,
|
message: result.message,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
final resultVolumes = await _adapter.api().getVolumes();
|
||||||
|
if (resultVolumes.data.isEmpty || !resultVolumes.success) {
|
||||||
|
return GenericResult(
|
||||||
|
success: false,
|
||||||
|
data: metadata,
|
||||||
|
code: resultVolumes.code,
|
||||||
|
message: resultVolumes.message,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final resultPricePerGb = await getAdditionalPricing();
|
||||||
|
if (resultPricePerGb.data == null || !resultPricePerGb.success) {
|
||||||
|
return GenericResult(
|
||||||
|
success: false,
|
||||||
|
data: metadata,
|
||||||
|
code: resultPricePerGb.code,
|
||||||
|
message: resultPricePerGb.message,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
final List servers = result.data;
|
final List servers = result.data;
|
||||||
|
final List<DigitalOceanVolume> volumes = resultVolumes.data;
|
||||||
try {
|
try {
|
||||||
|
final Price pricePerGb = resultPricePerGb.data!.perVolumeGb;
|
||||||
final droplet = servers.firstWhere(
|
final droplet = servers.firstWhere(
|
||||||
(final server) => server['id'] == serverId,
|
(final server) => server['id'] == serverId,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final volume = volumes.firstWhere(
|
||||||
|
(final volume) => droplet['volume_ids'].contains(volume.id),
|
||||||
|
);
|
||||||
|
|
||||||
metadata = [
|
metadata = [
|
||||||
ServerMetadataEntity(
|
ServerMetadataEntity(
|
||||||
type: MetadataType.id,
|
type: MetadataType.id,
|
||||||
|
@ -739,7 +771,8 @@ class DigitalOceanServerProvider extends ServerProvider {
|
||||||
ServerMetadataEntity(
|
ServerMetadataEntity(
|
||||||
type: MetadataType.cost,
|
type: MetadataType.cost,
|
||||||
trId: 'server.monthly_cost',
|
trId: 'server.monthly_cost',
|
||||||
value: '${droplet['size']['price_monthly']} ${currency.shortcode}',
|
value:
|
||||||
|
'${droplet['size']['price_monthly']} + ${(volume.sizeGigabytes * pricePerGb.value).toStringAsFixed(2)} ${currency.shortcode}',
|
||||||
),
|
),
|
||||||
ServerMetadataEntity(
|
ServerMetadataEntity(
|
||||||
type: MetadataType.location,
|
type: MetadataType.location,
|
||||||
|
|
|
@ -165,7 +165,9 @@ class HetznerServerProvider extends ServerProvider {
|
||||||
Future<GenericResult<CallbackDialogueBranching?>> launchInstallation(
|
Future<GenericResult<CallbackDialogueBranching?>> launchInstallation(
|
||||||
final LaunchInstallationData installationData,
|
final LaunchInstallationData installationData,
|
||||||
) async {
|
) async {
|
||||||
final volumeResult = await _adapter.api().createVolume();
|
final volumeResult = await _adapter.api().createVolume(
|
||||||
|
installationData.storageSize.gibibyte.toInt(),
|
||||||
|
);
|
||||||
|
|
||||||
if (!volumeResult.success || volumeResult.data == null) {
|
if (!volumeResult.success || volumeResult.data == null) {
|
||||||
return GenericResult(
|
return GenericResult(
|
||||||
|
@ -546,8 +548,8 @@ class HetznerServerProvider extends ServerProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<GenericResult<Price?>> getPricePerGb() async {
|
Future<GenericResult<AdditionalPricing?>> getAdditionalPricing() async {
|
||||||
final result = await _adapter.api().getPricePerGb();
|
final result = await _adapter.api().getPricing();
|
||||||
|
|
||||||
if (!result.success || result.data == null) {
|
if (!result.success || result.data == null) {
|
||||||
return GenericResult(
|
return GenericResult(
|
||||||
|
@ -560,9 +562,15 @@ class HetznerServerProvider extends ServerProvider {
|
||||||
|
|
||||||
return GenericResult(
|
return GenericResult(
|
||||||
success: true,
|
success: true,
|
||||||
data: Price(
|
data: AdditionalPricing(
|
||||||
value: result.data!,
|
perVolumeGb: Price(
|
||||||
currency: currency,
|
value: result.data!.perVolumeGb,
|
||||||
|
currency: currency,
|
||||||
|
),
|
||||||
|
perPublicIpv4: Price(
|
||||||
|
value: result.data!.perPublicIpv4,
|
||||||
|
currency: currency,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -617,10 +625,10 @@ class HetznerServerProvider extends ServerProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<GenericResult<ServerVolume?>> createVolume() async {
|
Future<GenericResult<ServerVolume?>> createVolume(final int gb) async {
|
||||||
ServerVolume? volume;
|
ServerVolume? volume;
|
||||||
|
|
||||||
final result = await _adapter.api().createVolume();
|
final result = await _adapter.api().createVolume(gb);
|
||||||
|
|
||||||
if (!result.success || result.data == null) {
|
if (!result.success || result.data == null) {
|
||||||
return GenericResult(
|
return GenericResult(
|
||||||
|
@ -705,21 +713,46 @@ class HetznerServerProvider extends ServerProvider {
|
||||||
final int serverId,
|
final int serverId,
|
||||||
) async {
|
) async {
|
||||||
List<ServerMetadataEntity> metadata = [];
|
List<ServerMetadataEntity> metadata = [];
|
||||||
final result = await _adapter.api().getServers();
|
final resultServers = await _adapter.api().getServers();
|
||||||
if (result.data.isEmpty || !result.success) {
|
if (resultServers.data.isEmpty || !resultServers.success) {
|
||||||
return GenericResult(
|
return GenericResult(
|
||||||
success: false,
|
success: false,
|
||||||
data: metadata,
|
data: metadata,
|
||||||
code: result.code,
|
code: resultServers.code,
|
||||||
message: result.message,
|
message: resultServers.message,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final resultVolumes = await _adapter.api().getVolumes();
|
||||||
|
if (resultVolumes.data.isEmpty || !resultVolumes.success) {
|
||||||
|
return GenericResult(
|
||||||
|
success: false,
|
||||||
|
data: metadata,
|
||||||
|
code: resultVolumes.code,
|
||||||
|
message: resultVolumes.message,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final resultPricePerGb = await getAdditionalPricing();
|
||||||
|
if (resultPricePerGb.data == null || !resultPricePerGb.success) {
|
||||||
|
return GenericResult(
|
||||||
|
success: false,
|
||||||
|
data: metadata,
|
||||||
|
code: resultPricePerGb.code,
|
||||||
|
message: resultPricePerGb.message,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final List<HetznerServerInfo> servers = result.data;
|
final List<HetznerServerInfo> servers = resultServers.data;
|
||||||
|
final List<HetznerVolume> volumes = resultVolumes.data;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
final Price pricePerGb = resultPricePerGb.data!.perVolumeGb;
|
||||||
|
final Price pricePerIp = resultPricePerGb.data!.perPublicIpv4;
|
||||||
final HetznerServerInfo server = servers.firstWhere(
|
final HetznerServerInfo server = servers.firstWhere(
|
||||||
(final server) => server.id == serverId,
|
(final server) => server.id == serverId,
|
||||||
);
|
);
|
||||||
|
final HetznerVolume volume = volumes.firstWhere(
|
||||||
|
(final volume) => server.volumes.contains(volume.id),
|
||||||
|
);
|
||||||
|
|
||||||
metadata = [
|
metadata = [
|
||||||
ServerMetadataEntity(
|
ServerMetadataEntity(
|
||||||
|
@ -746,7 +779,8 @@ class HetznerServerProvider extends ServerProvider {
|
||||||
type: MetadataType.cost,
|
type: MetadataType.cost,
|
||||||
trId: 'server.monthly_cost',
|
trId: 'server.monthly_cost',
|
||||||
value:
|
value:
|
||||||
'${server.serverType.prices[1].monthly.toStringAsFixed(2)} ${currency.shortcode}',
|
// TODO: Make more descriptive
|
||||||
|
'${server.serverType.prices[1].monthly.toStringAsFixed(2)} + ${(volume.size * pricePerGb.value).toStringAsFixed(2)} + ${pricePerIp.value.toStringAsFixed(2)} ${currency.shortcode}',
|
||||||
),
|
),
|
||||||
ServerMetadataEntity(
|
ServerMetadataEntity(
|
||||||
type: MetadataType.location,
|
type: MetadataType.location,
|
||||||
|
|
|
@ -90,9 +90,9 @@ abstract class ServerProvider {
|
||||||
/// answered the request.
|
/// answered the request.
|
||||||
Future<GenericResult<DateTime?>> restart(final int serverId);
|
Future<GenericResult<DateTime?>> restart(final int serverId);
|
||||||
|
|
||||||
/// Returns [Price] information per one gigabyte of storage extension for
|
/// Returns [Price] information map of all additional resources, excluding
|
||||||
/// the requested accessible machine.
|
/// main server type pricing
|
||||||
Future<GenericResult<Price?>> getPricePerGb();
|
Future<GenericResult<AdditionalPricing?>> getAdditionalPricing();
|
||||||
|
|
||||||
/// Returns [ServerVolume] of all available volumes
|
/// Returns [ServerVolume] of all available volumes
|
||||||
/// assigned to the authorized user and attached to active machine.
|
/// assigned to the authorized user and attached to active machine.
|
||||||
|
@ -101,7 +101,7 @@ abstract class ServerProvider {
|
||||||
/// Tries to create an empty unattached [ServerVolume].
|
/// Tries to create an empty unattached [ServerVolume].
|
||||||
///
|
///
|
||||||
/// If success, returns this volume information.
|
/// If success, returns this volume information.
|
||||||
Future<GenericResult<ServerVolume?>> createVolume();
|
Future<GenericResult<ServerVolume?>> createVolume(final int gb);
|
||||||
|
|
||||||
/// Tries to delete the requested accessible [ServerVolume].
|
/// Tries to delete the requested accessible [ServerVolume].
|
||||||
Future<GenericResult<void>> deleteVolume(final ServerVolume volume);
|
Future<GenericResult<void>> deleteVolume(final ServerVolume volume);
|
||||||
|
|
|
@ -16,8 +16,8 @@ class SupportDrawer extends StatelessWidget {
|
||||||
return Drawer(
|
return Drawer(
|
||||||
width: 440,
|
width: 440,
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
child: Padding(
|
child: SafeArea(
|
||||||
padding: const EdgeInsets.all(8.0),
|
minimum: const EdgeInsets.all(8.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:selfprivacy/logic/models/message.dart';
|
import 'package:selfprivacy/logic/models/message.dart';
|
||||||
|
import 'package:selfprivacy/utils/platform_adapter.dart';
|
||||||
|
|
||||||
class LogListItem extends StatelessWidget {
|
class LogListItem extends StatelessWidget {
|
||||||
const LogListItem({
|
const LogListItem({
|
||||||
|
@ -71,7 +71,7 @@ class _RestApiRequestMessageItem extends StatelessWidget {
|
||||||
if (message.text != null)
|
if (message.text != null)
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Clipboard.setData(ClipboardData(text: message.text ?? ''));
|
PlatformAdapter.setClipboard(message.text ?? '');
|
||||||
},
|
},
|
||||||
child: Text('console_page.copy'.tr()),
|
child: Text('console_page.copy'.tr()),
|
||||||
),
|
),
|
||||||
|
@ -121,7 +121,7 @@ class _RestApiResponseMessageItem extends StatelessWidget {
|
||||||
if (message.text != null)
|
if (message.text != null)
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Clipboard.setData(ClipboardData(text: message.text ?? ''));
|
PlatformAdapter.setClipboard(message.text ?? '');
|
||||||
},
|
},
|
||||||
child: Text('console_page.copy'.tr()),
|
child: Text('console_page.copy'.tr()),
|
||||||
),
|
),
|
||||||
|
@ -195,7 +195,7 @@ class _GraphQlResponseMessageItem extends StatelessWidget {
|
||||||
if (message.text != null)
|
if (message.text != null)
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Clipboard.setData(ClipboardData(text: message.text ?? ''));
|
PlatformAdapter.setClipboard(message.text ?? '');
|
||||||
},
|
},
|
||||||
child: Text('console_page.copy'.tr()),
|
child: Text('console_page.copy'.tr()),
|
||||||
),
|
),
|
||||||
|
@ -264,7 +264,7 @@ class _GraphQlRequestMessageItem extends StatelessWidget {
|
||||||
if (message.text != null)
|
if (message.text != null)
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Clipboard.setData(ClipboardData(text: message.text ?? ''));
|
PlatformAdapter.setClipboard(message.text ?? '');
|
||||||
},
|
},
|
||||||
child: Text('console_page.copy'.tr()),
|
child: Text('console_page.copy'.tr()),
|
||||||
),
|
),
|
||||||
|
|
|
@ -0,0 +1,67 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:selfprivacy/ui/components/not_ready_card/not_ready_card.dart';
|
||||||
|
|
||||||
|
class EmptyPagePlaceholder extends StatelessWidget {
|
||||||
|
const EmptyPagePlaceholder({
|
||||||
|
required this.title,
|
||||||
|
required this.iconData,
|
||||||
|
required this.description,
|
||||||
|
this.showReadyCard = false,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String title;
|
||||||
|
final String description;
|
||||||
|
final IconData iconData;
|
||||||
|
final bool showReadyCard;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(final BuildContext context) => !showReadyCard
|
||||||
|
? _expandedContent(context)
|
||||||
|
: 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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _expandedContent(final BuildContext context) => Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
iconData,
|
||||||
|
size: 50,
|
||||||
|
color: Theme.of(context).colorScheme.onBackground,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
||||||
|
color: Theme.of(context).colorScheme.onBackground,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
description,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
||||||
|
color: Theme.of(context).colorScheme.onBackground,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
|
@ -16,6 +16,7 @@ import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart';
|
||||||
import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart';
|
import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart';
|
||||||
import 'package:selfprivacy/ui/helpers/modals.dart';
|
import 'package:selfprivacy/ui/helpers/modals.dart';
|
||||||
import 'package:selfprivacy/ui/pages/backups/change_period_modal.dart';
|
import 'package:selfprivacy/ui/pages/backups/change_period_modal.dart';
|
||||||
|
import 'package:selfprivacy/ui/pages/backups/change_rotation_quotas_modal.dart';
|
||||||
import 'package:selfprivacy/ui/pages/backups/copy_encryption_key_modal.dart';
|
import 'package:selfprivacy/ui/pages/backups/copy_encryption_key_modal.dart';
|
||||||
import 'package:selfprivacy/ui/pages/backups/create_backups_modal.dart';
|
import 'package:selfprivacy/ui/pages/backups/create_backups_modal.dart';
|
||||||
import 'package:selfprivacy/ui/pages/backups/snapshot_modal.dart';
|
import 'package:selfprivacy/ui/pages/backups/snapshot_modal.dart';
|
||||||
|
@ -83,6 +84,9 @@ class BackupDetailsPage extends StatelessWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final Color? overrideColor =
|
||||||
|
preventActions ? Theme.of(context).colorScheme.secondary : null;
|
||||||
|
|
||||||
return BrandHeroScreen(
|
return BrandHeroScreen(
|
||||||
heroIcon: BrandIcons.save,
|
heroIcon: BrandIcons.save,
|
||||||
heroTitle: 'backup.card_title'.tr(),
|
heroTitle: 'backup.card_title'.tr(),
|
||||||
|
@ -110,11 +114,15 @@ class BackupDetailsPage extends StatelessWidget {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
leading: const Icon(
|
leading: Icon(
|
||||||
Icons.add_circle_outline_rounded,
|
Icons.add_circle_outline_rounded,
|
||||||
|
color: overrideColor,
|
||||||
),
|
),
|
||||||
title: Text(
|
title: Text(
|
||||||
'backup.create_new'.tr(),
|
'backup.create_new'.tr(),
|
||||||
|
style: TextStyle(
|
||||||
|
color: overrideColor,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
|
@ -138,13 +146,20 @@ class BackupDetailsPage extends StatelessWidget {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
leading: const Icon(
|
leading: Icon(
|
||||||
Icons.manage_history_outlined,
|
Icons.manage_history_outlined,
|
||||||
|
color: overrideColor,
|
||||||
),
|
),
|
||||||
title: Text(
|
title: Text(
|
||||||
'backup.autobackup_period_title'.tr(),
|
'backup.autobackup_period_title'.tr(),
|
||||||
|
style: TextStyle(
|
||||||
|
color: overrideColor,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
|
style: TextStyle(
|
||||||
|
color: overrideColor,
|
||||||
|
),
|
||||||
autobackupPeriod != null
|
autobackupPeriod != null
|
||||||
? 'backup.autobackup_period_subtitle'.tr(
|
? 'backup.autobackup_period_subtitle'.tr(
|
||||||
namedArgs: {
|
namedArgs: {
|
||||||
|
@ -154,6 +169,38 @@ class BackupDetailsPage extends StatelessWidget {
|
||||||
: 'backup.autobackup_period_never'.tr(),
|
: 'backup.autobackup_period_never'.tr(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
ListTile(
|
||||||
|
onTap: preventActions
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
showModalBottomSheet(
|
||||||
|
useRootNavigator: true,
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
builder: (final BuildContext context) =>
|
||||||
|
DraggableScrollableSheet(
|
||||||
|
expand: false,
|
||||||
|
maxChildSize: 0.9,
|
||||||
|
minChildSize: 0.4,
|
||||||
|
initialChildSize: 0.6,
|
||||||
|
builder: (final context, final scrollController) =>
|
||||||
|
ChangeRotationQuotasModal(
|
||||||
|
scrollController: scrollController,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
leading: Icon(
|
||||||
|
Icons.auto_delete_outlined,
|
||||||
|
color: overrideColor,
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
'backup.rotation_quotas_title'.tr(),
|
||||||
|
style: TextStyle(
|
||||||
|
color: overrideColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
onTap: preventActions
|
onTap: preventActions
|
||||||
? null
|
? null
|
||||||
|
@ -175,14 +222,21 @@ class BackupDetailsPage extends StatelessWidget {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
leading: const Icon(
|
leading: Icon(
|
||||||
Icons.key_outlined,
|
Icons.key_outlined,
|
||||||
|
color: overrideColor,
|
||||||
),
|
),
|
||||||
title: Text(
|
title: Text(
|
||||||
'backup.backups_encryption_key'.tr(),
|
'backup.backups_encryption_key'.tr(),
|
||||||
|
style: TextStyle(
|
||||||
|
color: overrideColor,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
'backup.backups_encryption_key_subtitle'.tr(),
|
'backup.backups_encryption_key_subtitle'.tr(),
|
||||||
|
style: TextStyle(
|
||||||
|
color: overrideColor,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
|
@ -227,10 +281,16 @@ class BackupDetailsPage extends StatelessWidget {
|
||||||
),
|
),
|
||||||
if (backups.isEmpty)
|
if (backups.isEmpty)
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(
|
leading: Icon(
|
||||||
Icons.error_outline,
|
Icons.error_outline,
|
||||||
|
color: overrideColor,
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
'backup.no_backups'.tr(),
|
||||||
|
style: TextStyle(
|
||||||
|
color: overrideColor,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
title: Text('backup.no_backups'.tr()),
|
|
||||||
),
|
),
|
||||||
if (backups.isNotEmpty)
|
if (backups.isNotEmpty)
|
||||||
Column(
|
Column(
|
||||||
|
@ -282,9 +342,15 @@ class BackupDetailsPage extends StatelessWidget {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
title: Text(
|
title: Text(
|
||||||
|
style: TextStyle(
|
||||||
|
color: overrideColor,
|
||||||
|
),
|
||||||
'${MaterialLocalizations.of(context).formatShortDate(backup.time)} ${TimeOfDay.fromDateTime(backup.time).format(context)}',
|
'${MaterialLocalizations.of(context).formatShortDate(backup.time)} ${TimeOfDay.fromDateTime(backup.time).format(context)}',
|
||||||
),
|
),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
|
style: TextStyle(
|
||||||
|
color: overrideColor,
|
||||||
|
),
|
||||||
service?.displayName ?? backup.fallbackServiceName,
|
service?.displayName ?? backup.fallbackServiceName,
|
||||||
),
|
),
|
||||||
leading: service != null
|
leading: service != null
|
||||||
|
@ -293,12 +359,16 @@ class BackupDetailsPage extends StatelessWidget {
|
||||||
height: 24,
|
height: 24,
|
||||||
width: 24,
|
width: 24,
|
||||||
colorFilter: ColorFilter.mode(
|
colorFilter: ColorFilter.mode(
|
||||||
Theme.of(context).colorScheme.onBackground,
|
overrideColor ??
|
||||||
|
Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onBackground,
|
||||||
BlendMode.srcIn,
|
BlendMode.srcIn,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: const Icon(
|
: Icon(
|
||||||
Icons.question_mark_outlined,
|
Icons.question_mark_outlined,
|
||||||
|
color: overrideColor,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -339,12 +409,19 @@ class BackupDetailsPage extends StatelessWidget {
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(
|
title: Text(
|
||||||
'backup.refetch_backups'.tr(),
|
'backup.refetch_backups'.tr(),
|
||||||
|
style: TextStyle(
|
||||||
|
color: overrideColor,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
'backup.refetch_backups_subtitle'.tr(),
|
'backup.refetch_backups_subtitle'.tr(),
|
||||||
|
style: TextStyle(
|
||||||
|
color: overrideColor,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
leading: const Icon(
|
leading: Icon(
|
||||||
Icons.cached_outlined,
|
Icons.cached_outlined,
|
||||||
|
color: overrideColor,
|
||||||
),
|
),
|
||||||
onTap: preventActions
|
onTap: preventActions
|
||||||
? null
|
? null
|
||||||
|
@ -356,12 +433,19 @@ class BackupDetailsPage extends StatelessWidget {
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(
|
title: Text(
|
||||||
'backup.reupload_key'.tr(),
|
'backup.reupload_key'.tr(),
|
||||||
|
style: TextStyle(
|
||||||
|
color: overrideColor,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
'backup.reupload_key_subtitle'.tr(),
|
'backup.reupload_key_subtitle'.tr(),
|
||||||
|
style: TextStyle(
|
||||||
|
color: overrideColor,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
leading: const Icon(
|
leading: Icon(
|
||||||
Icons.warning_amber_outlined,
|
Icons.warning_amber_outlined,
|
||||||
|
color: overrideColor,
|
||||||
),
|
),
|
||||||
onTap: preventActions
|
onTap: preventActions
|
||||||
? null
|
? null
|
||||||
|
|
|
@ -0,0 +1,250 @@
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
|
||||||
|
import 'package:selfprivacy/logic/cubit/backups/backups_cubit.dart';
|
||||||
|
import 'package:selfprivacy/logic/cubit/server_jobs/server_jobs_cubit.dart';
|
||||||
|
import 'package:selfprivacy/logic/models/backup.dart';
|
||||||
|
|
||||||
|
class ChangeRotationQuotasModal extends StatefulWidget {
|
||||||
|
const ChangeRotationQuotasModal({
|
||||||
|
required this.scrollController,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final ScrollController scrollController;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ChangeRotationQuotasModal> createState() =>
|
||||||
|
_ChangeRotationQuotasModalState();
|
||||||
|
}
|
||||||
|
|
||||||
|
enum QuotaUnits {
|
||||||
|
last,
|
||||||
|
daily,
|
||||||
|
weekly,
|
||||||
|
monthly,
|
||||||
|
yearly,
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ChangeRotationQuotasModalState extends State<ChangeRotationQuotasModal> {
|
||||||
|
AutobackupQuotas selectedQuotas = AutobackupQuotas(
|
||||||
|
last: 3,
|
||||||
|
daily: 7,
|
||||||
|
weekly: 4,
|
||||||
|
monthly: 6,
|
||||||
|
yearly: -1,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Set initial period to the one currently set
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
selectedQuotas =
|
||||||
|
context.read<BackupsCubit>().state.autobackupQuotas ?? selectedQuotas;
|
||||||
|
}
|
||||||
|
|
||||||
|
String generateSubtitle(final int value, final QuotaUnits unit) {
|
||||||
|
switch (unit) {
|
||||||
|
case QuotaUnits.last:
|
||||||
|
return value == -1
|
||||||
|
? 'backup.quota_subtitles.last_infinite'.tr()
|
||||||
|
: 'backup.quota_subtitles.last'.plural(value);
|
||||||
|
case QuotaUnits.daily:
|
||||||
|
if (selectedQuotas.last == -1) {
|
||||||
|
return 'backup.quota_subtitles.no_effect'.tr();
|
||||||
|
}
|
||||||
|
return value == -1
|
||||||
|
? 'backup.quota_subtitles.daily_infinite'.tr()
|
||||||
|
: 'backup.quota_subtitles.daily'.plural(value);
|
||||||
|
case QuotaUnits.weekly:
|
||||||
|
if (selectedQuotas.last == -1 || selectedQuotas.daily == -1) {
|
||||||
|
return 'backup.quota_subtitles.no_effect'.tr();
|
||||||
|
}
|
||||||
|
return value == -1
|
||||||
|
? 'backup.quota_subtitles.weekly_infinite'.tr()
|
||||||
|
: 'backup.quota_subtitles.weekly'.plural(value);
|
||||||
|
case QuotaUnits.monthly:
|
||||||
|
if (selectedQuotas.last == -1 || selectedQuotas.daily == -1) {
|
||||||
|
return 'backup.quota_subtitles.no_effect'.tr();
|
||||||
|
}
|
||||||
|
return value == -1
|
||||||
|
? 'backup.quota_subtitles.monthly_infinite'.tr()
|
||||||
|
: 'backup.quota_subtitles.monthly'.plural(value);
|
||||||
|
case QuotaUnits.yearly:
|
||||||
|
if (selectedQuotas.last == -1 || selectedQuotas.daily == -1) {
|
||||||
|
return 'backup.quota_subtitles.no_effect'.tr();
|
||||||
|
}
|
||||||
|
return value == -1
|
||||||
|
? 'backup.quota_subtitles.yearly_infinite'.tr()
|
||||||
|
: 'backup.quota_subtitles.yearly'.plural(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(final BuildContext context) {
|
||||||
|
final AutobackupQuotas? initialAutobackupQuotas =
|
||||||
|
context.watch<BackupsCubit>().state.autobackupQuotas;
|
||||||
|
return ListView(
|
||||||
|
controller: widget.scrollController,
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'backup.rotation_quotas_title'.tr(),
|
||||||
|
style: Theme.of(context).textTheme.headlineSmall,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'backup.quotas_only_applied_to_autobackups'.tr(),
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
// Accordions for each quota type. When tapped allows to enter a new int value
|
||||||
|
// for the quota.
|
||||||
|
QuotaSelectionTile(
|
||||||
|
title: 'backup.quota_titles.last'.tr(),
|
||||||
|
subtitle: generateSubtitle(selectedQuotas.last, QuotaUnits.last),
|
||||||
|
value: selectedQuotas.last,
|
||||||
|
min: 1,
|
||||||
|
max: 30,
|
||||||
|
callback: (final double value) {
|
||||||
|
setState(() {
|
||||||
|
if (value == 31) {
|
||||||
|
selectedQuotas = selectedQuotas.copyWith(last: -1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
selectedQuotas = selectedQuotas.copyWith(last: value.toInt());
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
QuotaSelectionTile(
|
||||||
|
title: 'backup.quota_titles.daily'.tr(),
|
||||||
|
subtitle: generateSubtitle(selectedQuotas.daily, QuotaUnits.daily),
|
||||||
|
value: selectedQuotas.daily,
|
||||||
|
min: 0,
|
||||||
|
max: 30,
|
||||||
|
callback: (final double value) {
|
||||||
|
setState(() {
|
||||||
|
if (value == 31) {
|
||||||
|
selectedQuotas = selectedQuotas.copyWith(daily: -1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
selectedQuotas = selectedQuotas.copyWith(daily: value.toInt());
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
QuotaSelectionTile(
|
||||||
|
title: 'backup.quota_titles.weekly'.tr(),
|
||||||
|
subtitle: generateSubtitle(selectedQuotas.weekly, QuotaUnits.weekly),
|
||||||
|
value: selectedQuotas.weekly,
|
||||||
|
min: 0,
|
||||||
|
max: 15,
|
||||||
|
callback: (final double value) {
|
||||||
|
setState(() {
|
||||||
|
if (value == 16) {
|
||||||
|
selectedQuotas = selectedQuotas.copyWith(weekly: -1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
selectedQuotas = selectedQuotas.copyWith(weekly: value.toInt());
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
QuotaSelectionTile(
|
||||||
|
title: 'backup.quota_titles.monthly'.tr(),
|
||||||
|
subtitle:
|
||||||
|
generateSubtitle(selectedQuotas.monthly, QuotaUnits.monthly),
|
||||||
|
value: selectedQuotas.monthly,
|
||||||
|
min: 0,
|
||||||
|
max: 24,
|
||||||
|
callback: (final double value) {
|
||||||
|
setState(() {
|
||||||
|
if (value == 25) {
|
||||||
|
selectedQuotas = selectedQuotas.copyWith(monthly: -1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
selectedQuotas = selectedQuotas.copyWith(monthly: value.toInt());
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
QuotaSelectionTile(
|
||||||
|
title: 'backup.quota_titles.yearly'.tr(),
|
||||||
|
subtitle: generateSubtitle(selectedQuotas.yearly, QuotaUnits.yearly),
|
||||||
|
value: selectedQuotas.yearly,
|
||||||
|
min: 0,
|
||||||
|
max: 5,
|
||||||
|
callback: (final double value) {
|
||||||
|
setState(() {
|
||||||
|
if (value == 6) {
|
||||||
|
selectedQuotas = selectedQuotas.copyWith(yearly: -1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
selectedQuotas = selectedQuotas.copyWith(yearly: value.toInt());
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
FilledButton(
|
||||||
|
onPressed: selectedQuotas == initialAutobackupQuotas
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
context
|
||||||
|
.read<BackupsCubit>()
|
||||||
|
.setAutobackupQuotas(selectedQuotas);
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
'backup.set_rotation_quotas'.tr(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class QuotaSelectionTile extends StatelessWidget {
|
||||||
|
const QuotaSelectionTile({
|
||||||
|
required this.title,
|
||||||
|
required this.subtitle,
|
||||||
|
required this.value,
|
||||||
|
required this.min,
|
||||||
|
required this.max,
|
||||||
|
required this.callback,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String title;
|
||||||
|
final String subtitle;
|
||||||
|
final int value;
|
||||||
|
final int min;
|
||||||
|
final int max;
|
||||||
|
final void Function(double) callback;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(final BuildContext context) => ExpansionTile(
|
||||||
|
title: Text(title),
|
||||||
|
subtitle: Text(subtitle),
|
||||||
|
trailing: Text(
|
||||||
|
value == -1 ? '∞' : value.toString(),
|
||||||
|
style: Theme.of(context).textTheme.headlineMedium,
|
||||||
|
),
|
||||||
|
children: [
|
||||||
|
// Discrete slider to select the new value
|
||||||
|
if (value >= -1 && value <= max)
|
||||||
|
Slider(
|
||||||
|
value: value == -1 ? max + 1 : value.toDouble(),
|
||||||
|
min: min.toDouble(),
|
||||||
|
max: (max + 1).toDouble(),
|
||||||
|
divisions: max - min + 1,
|
||||||
|
label: value == -1 ? '∞' : value.toString(),
|
||||||
|
onChanged: callback,
|
||||||
|
),
|
||||||
|
if (value < -1 || value > max)
|
||||||
|
Text(
|
||||||
|
'Manually set to $value',
|
||||||
|
style: Theme.of(context).textTheme.headlineSmall,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
|
@ -2,11 +2,11 @@ import 'dart:async';
|
||||||
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
|
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
|
||||||
import 'package:selfprivacy/logic/cubit/backups/backups_cubit.dart';
|
import 'package:selfprivacy/logic/cubit/backups/backups_cubit.dart';
|
||||||
import 'package:selfprivacy/logic/cubit/server_jobs/server_jobs_cubit.dart';
|
import 'package:selfprivacy/logic/cubit/server_jobs/server_jobs_cubit.dart';
|
||||||
import 'package:selfprivacy/ui/components/info_box/info_box.dart';
|
import 'package:selfprivacy/ui/components/info_box/info_box.dart';
|
||||||
|
import 'package:selfprivacy/utils/platform_adapter.dart';
|
||||||
|
|
||||||
class CopyEncryptionKeyModal extends StatefulWidget {
|
class CopyEncryptionKeyModal extends StatefulWidget {
|
||||||
const CopyEncryptionKeyModal({
|
const CopyEncryptionKeyModal({
|
||||||
|
@ -144,11 +144,7 @@ class _CopyEncryptionKeyModalState extends State<CopyEncryptionKeyModal> {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
Clipboard.setData(
|
PlatformAdapter.setClipboard(encryptionKey);
|
||||||
ClipboardData(
|
|
||||||
text: encryptionKey,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.copy_all_outlined),
|
icon: const Icon(Icons.copy_all_outlined),
|
||||||
label: Text(
|
label: Text(
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:selfprivacy/config/get_it_config.dart';
|
||||||
|
import 'package:selfprivacy/utils/platform_adapter.dart';
|
||||||
|
|
||||||
|
class SnapshotIdListTile extends StatelessWidget {
|
||||||
|
const SnapshotIdListTile({
|
||||||
|
required this.snapshotId,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String snapshotId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(final BuildContext context) => ListTile(
|
||||||
|
onLongPress: () {
|
||||||
|
PlatformAdapter.setClipboard(snapshotId);
|
||||||
|
getIt<NavigationService>().showSnackBar(
|
||||||
|
'basis.copied_to_clipboard'.tr(),
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
leading: Icon(
|
||||||
|
Icons.numbers_outlined,
|
||||||
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
title: Text('backup.snapshot_id_title'.tr()),
|
||||||
|
subtitle: Text(snapshotId),
|
||||||
|
);
|
||||||
|
}
|
|
@ -11,6 +11,7 @@ import 'package:selfprivacy/logic/models/service.dart';
|
||||||
import 'package:selfprivacy/ui/components/buttons/brand_button.dart';
|
import 'package:selfprivacy/ui/components/buttons/brand_button.dart';
|
||||||
import 'package:selfprivacy/ui/components/cards/outlined_card.dart';
|
import 'package:selfprivacy/ui/components/cards/outlined_card.dart';
|
||||||
import 'package:selfprivacy/ui/components/info_box/info_box.dart';
|
import 'package:selfprivacy/ui/components/info_box/info_box.dart';
|
||||||
|
import 'package:selfprivacy/ui/pages/backups/snapshot_id_list_tile.dart';
|
||||||
|
|
||||||
class SnapshotModal extends StatefulWidget {
|
class SnapshotModal extends StatefulWidget {
|
||||||
const SnapshotModal({
|
const SnapshotModal({
|
||||||
|
@ -51,125 +52,129 @@ class _SnapshotModalState extends State<SnapshotModal> {
|
||||||
.state
|
.state
|
||||||
.getServiceById(widget.snapshot.serviceId);
|
.getServiceById(widget.snapshot.serviceId);
|
||||||
|
|
||||||
return ListView(
|
return Scaffold(
|
||||||
controller: widget.scrollController,
|
backgroundColor: Colors.transparent,
|
||||||
padding: const EdgeInsets.all(16),
|
body: ListView(
|
||||||
children: [
|
controller: widget.scrollController,
|
||||||
const SizedBox(height: 16),
|
padding: const EdgeInsets.all(16),
|
||||||
Text(
|
children: [
|
||||||
'backup.snapshot_modal_heading'.tr(),
|
const SizedBox(height: 16),
|
||||||
style: Theme.of(context).textTheme.headlineSmall,
|
Text(
|
||||||
textAlign: TextAlign.center,
|
'backup.snapshot_modal_heading'.tr(),
|
||||||
),
|
style: Theme.of(context).textTheme.headlineSmall,
|
||||||
const SizedBox(height: 16),
|
textAlign: TextAlign.center,
|
||||||
ListTile(
|
),
|
||||||
leading: service != null
|
const SizedBox(height: 16),
|
||||||
? SvgPicture.string(
|
ListTile(
|
||||||
service.svgIcon,
|
leading: service != null
|
||||||
height: 24,
|
? SvgPicture.string(
|
||||||
width: 24,
|
service.svgIcon,
|
||||||
colorFilter: ColorFilter.mode(
|
height: 24,
|
||||||
Theme.of(context).colorScheme.onSurface,
|
width: 24,
|
||||||
BlendMode.srcIn,
|
colorFilter: ColorFilter.mode(
|
||||||
|
Theme.of(context).colorScheme.onSurface,
|
||||||
|
BlendMode.srcIn,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const Icon(
|
||||||
|
Icons.question_mark_outlined,
|
||||||
),
|
),
|
||||||
)
|
title: Text(
|
||||||
: const Icon(
|
'backup.snapshot_service_title'.tr(),
|
||||||
Icons.question_mark_outlined,
|
|
||||||
),
|
|
||||||
title: Text(
|
|
||||||
'backup.snapshot_service_title'.tr(),
|
|
||||||
),
|
|
||||||
subtitle: Text(
|
|
||||||
service?.displayName ?? widget.snapshot.fallbackServiceName,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
ListTile(
|
|
||||||
leading: Icon(
|
|
||||||
Icons.access_time_outlined,
|
|
||||||
color: Theme.of(context).colorScheme.onSurface,
|
|
||||||
),
|
|
||||||
title: Text(
|
|
||||||
'backup.snapshot_creation_time_title'.tr(),
|
|
||||||
),
|
|
||||||
subtitle: Text(
|
|
||||||
'${MaterialLocalizations.of(context).formatShortDate(widget.snapshot.time)} ${TimeOfDay.fromDateTime(widget.snapshot.time).format(context)}',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
ListTile(
|
|
||||||
leading: Icon(
|
|
||||||
Icons.numbers_outlined,
|
|
||||||
color: Theme.of(context).colorScheme.onSurface,
|
|
||||||
),
|
|
||||||
title: Text(
|
|
||||||
'backup.snapshot_id_title'.tr(),
|
|
||||||
),
|
|
||||||
subtitle: Text(
|
|
||||||
widget.snapshot.id,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (service != null)
|
|
||||||
Column(
|
|
||||||
children: [
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text(
|
|
||||||
'backup.snapshot_modal_select_strategy'.tr(),
|
|
||||||
style: Theme.of(context).textTheme.titleMedium,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
_BackupStrategySelectionCard(
|
|
||||||
isSelected: selectedStrategy ==
|
|
||||||
BackupRestoreStrategy.downloadVerifyOverwrite,
|
|
||||||
onTap: () {
|
|
||||||
setState(() {
|
|
||||||
selectedStrategy =
|
|
||||||
BackupRestoreStrategy.downloadVerifyOverwrite;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
title:
|
|
||||||
'backup.snapshot_modal_download_verify_option_title'.tr(),
|
|
||||||
subtitle:
|
|
||||||
'backup.snapshot_modal_download_verify_option_description'
|
|
||||||
.tr(),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
_BackupStrategySelectionCard(
|
|
||||||
isSelected: selectedStrategy == BackupRestoreStrategy.inplace,
|
|
||||||
onTap: () {
|
|
||||||
setState(() {
|
|
||||||
selectedStrategy = BackupRestoreStrategy.inplace;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
title: 'backup.snapshot_modal_inplace_option_title'.tr(),
|
|
||||||
subtitle:
|
|
||||||
'backup.snapshot_modal_inplace_option_description'.tr(),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
// Restore backup button
|
|
||||||
BrandButton.filled(
|
|
||||||
onPressed: isServiceBusy
|
|
||||||
? null
|
|
||||||
: () {
|
|
||||||
context.read<BackupsCubit>().restoreBackup(
|
|
||||||
widget.snapshot.id,
|
|
||||||
selectedStrategy,
|
|
||||||
);
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
getIt<NavigationService>()
|
|
||||||
.showSnackBar('backup.restore_started'.tr());
|
|
||||||
},
|
|
||||||
text: 'backup.restore'.tr(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
else
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(16.0),
|
|
||||||
child: InfoBox(
|
|
||||||
isWarning: true,
|
|
||||||
text: 'backup.snapshot_modal_service_not_found'.tr(),
|
|
||||||
),
|
),
|
||||||
)
|
subtitle: Text(
|
||||||
],
|
service?.displayName ?? widget.snapshot.fallbackServiceName,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: Icon(
|
||||||
|
Icons.access_time_outlined,
|
||||||
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
'backup.snapshot_creation_time_title'.tr(),
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
'${MaterialLocalizations.of(context).formatShortDate(widget.snapshot.time)} ${TimeOfDay.fromDateTime(widget.snapshot.time).format(context)}',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SnapshotIdListTile(snapshotId: widget.snapshot.id),
|
||||||
|
ListTile(
|
||||||
|
leading: Icon(
|
||||||
|
Icons.info_outline,
|
||||||
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
'backup.snapshot_reason_title'.tr(),
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
widget.snapshot.reason.displayName.tr(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (service != null)
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'backup.snapshot_modal_select_strategy'.tr(),
|
||||||
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_BackupStrategySelectionCard(
|
||||||
|
isSelected: selectedStrategy ==
|
||||||
|
BackupRestoreStrategy.downloadVerifyOverwrite,
|
||||||
|
onTap: () {
|
||||||
|
setState(() {
|
||||||
|
selectedStrategy =
|
||||||
|
BackupRestoreStrategy.downloadVerifyOverwrite;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
title:
|
||||||
|
'backup.snapshot_modal_download_verify_option_title'.tr(),
|
||||||
|
subtitle:
|
||||||
|
'backup.snapshot_modal_download_verify_option_description'
|
||||||
|
.tr(),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_BackupStrategySelectionCard(
|
||||||
|
isSelected: selectedStrategy == BackupRestoreStrategy.inplace,
|
||||||
|
onTap: () {
|
||||||
|
setState(() {
|
||||||
|
selectedStrategy = BackupRestoreStrategy.inplace;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
title: 'backup.snapshot_modal_inplace_option_title'.tr(),
|
||||||
|
subtitle:
|
||||||
|
'backup.snapshot_modal_inplace_option_description'.tr(),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
// Restore backup button
|
||||||
|
BrandButton.filled(
|
||||||
|
onPressed: isServiceBusy
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
context.read<BackupsCubit>().restoreBackup(
|
||||||
|
widget.snapshot.id,
|
||||||
|
selectedStrategy,
|
||||||
|
);
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
getIt<NavigationService>()
|
||||||
|
.showSnackBar('backup.restore_started'.tr());
|
||||||
|
},
|
||||||
|
text: 'backup.restore'.tr(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: InfoBox(
|
||||||
|
isWarning: true,
|
||||||
|
text: 'backup.snapshot_modal_service_not_found'.tr(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,6 +49,8 @@ class _ProvidersPageState extends State<ProvidersPage> {
|
||||||
return StateType.stable;
|
return StateType.stable;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool isClickable() => getServerStatus() != StateType.uninitialized;
|
||||||
|
|
||||||
StateType getDnsStatus() {
|
StateType getDnsStatus() {
|
||||||
if (dnsStatus == DnsRecordsStatus.uninitialized ||
|
if (dnsStatus == DnsRecordsStatus.uninitialized ||
|
||||||
dnsStatus == DnsRecordsStatus.refreshing) {
|
dnsStatus == DnsRecordsStatus.refreshing) {
|
||||||
|
@ -83,7 +85,9 @@ class _ProvidersPageState extends State<ProvidersPage> {
|
||||||
subtitle: diskStatus.isDiskOkay
|
subtitle: diskStatus.isDiskOkay
|
||||||
? 'storage.status_ok'.tr()
|
? 'storage.status_ok'.tr()
|
||||||
: 'storage.status_error'.tr(),
|
: 'storage.status_error'.tr(),
|
||||||
onTap: () => context.pushRoute(const ServerDetailsRoute()),
|
onTap: isClickable()
|
||||||
|
? () => context.pushRoute(const ServerDetailsRoute())
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
_Card(
|
_Card(
|
||||||
|
@ -93,7 +97,9 @@ class _ProvidersPageState extends State<ProvidersPage> {
|
||||||
subtitle: appConfig.isDomainSelected
|
subtitle: appConfig.isDomainSelected
|
||||||
? appConfig.serverDomain!.domainName
|
? appConfig.serverDomain!.domainName
|
||||||
: '',
|
: '',
|
||||||
onTap: () => context.pushRoute(const DnsDetailsRoute()),
|
onTap: isClickable()
|
||||||
|
? () => context.pushRoute(const DnsDetailsRoute())
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
_Card(
|
_Card(
|
||||||
|
@ -103,7 +109,9 @@ class _ProvidersPageState extends State<ProvidersPage> {
|
||||||
icon: BrandIcons.save,
|
icon: BrandIcons.save,
|
||||||
title: 'backup.card_title'.tr(),
|
title: 'backup.card_title'.tr(),
|
||||||
subtitle: isBackupInitialized ? 'backup.card_subtitle'.tr() : '',
|
subtitle: isBackupInitialized ? 'backup.card_subtitle'.tr() : '',
|
||||||
onTap: () => context.pushRoute(const BackupDetailsRoute()),
|
onTap: isClickable()
|
||||||
|
? () => context.pushRoute(const BackupDetailsRoute())
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
@ -8,6 +8,7 @@ import 'package:selfprivacy/logic/models/disk_size.dart';
|
||||||
import 'package:selfprivacy/logic/models/price.dart';
|
import 'package:selfprivacy/logic/models/price.dart';
|
||||||
import 'package:selfprivacy/ui/components/buttons/brand_button.dart';
|
import 'package:selfprivacy/ui/components/buttons/brand_button.dart';
|
||||||
import 'package:selfprivacy/ui/components/info_box/info_box.dart';
|
import 'package:selfprivacy/ui/components/info_box/info_box.dart';
|
||||||
|
import 'package:selfprivacy/ui/helpers/modals.dart';
|
||||||
import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart';
|
import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart';
|
||||||
import 'package:selfprivacy/logic/models/disk_status.dart';
|
import 'package:selfprivacy/logic/models/disk_status.dart';
|
||||||
|
|
||||||
|
@ -78,10 +79,12 @@ class _ExtendingVolumePageState extends State<ExtendingVolumePage> {
|
||||||
}
|
}
|
||||||
final price = snapshot.data as Price;
|
final price = snapshot.data as Price;
|
||||||
_pricePerGb = price.value;
|
_pricePerGb = price.value;
|
||||||
_sizeController.text = _currentSliderGbValue.truncate().toString();
|
final currentSizeValue = _currentSliderGbValue.truncate().toString();
|
||||||
|
_sizeController.text = 'storage.gb'.tr(args: [currentSizeValue]);
|
||||||
_priceController.text =
|
_priceController.text =
|
||||||
(_pricePerGb * double.parse(_sizeController.text))
|
'${(_pricePerGb * double.parse(currentSizeValue)).toStringAsFixed(2)}'
|
||||||
.toStringAsFixed(2);
|
' '
|
||||||
|
'${price.currency.shortcode}';
|
||||||
minSize =
|
minSize =
|
||||||
widget.diskVolumeToResize.sizeTotal + DiskSize.fromGibibyte(3);
|
widget.diskVolumeToResize.sizeTotal + DiskSize.fromGibibyte(3);
|
||||||
if (_currentSliderGbValue < 0) {
|
if (_currentSliderGbValue < 0) {
|
||||||
|
@ -129,7 +132,7 @@ class _ExtendingVolumePageState extends State<ExtendingVolumePage> {
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
errorText: _isError ? ' ' : null,
|
errorText: _isError ? ' ' : null,
|
||||||
labelText: price.currency.shortcode,
|
labelText: 'storage.price'.tr(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -152,12 +155,24 @@ class _ExtendingVolumePageState extends State<ExtendingVolumePage> {
|
||||||
onPressed: _isError || isAlreadyResizing
|
onPressed: _isError || isAlreadyResizing
|
||||||
? null
|
? null
|
||||||
: () {
|
: () {
|
||||||
context.read<ApiProviderVolumeCubit>().resizeVolume(
|
showPopUpAlert(
|
||||||
widget.diskVolumeToResize,
|
alertTitle: 'storage.extending_volume_title'.tr(),
|
||||||
DiskSize.fromGibibyte(_currentSliderGbValue),
|
description:
|
||||||
context.read<ApiServerVolumeCubit>().reload,
|
'storage.extending_volume_modal_description'.tr(
|
||||||
);
|
args: [_sizeController.text, _priceController.text],
|
||||||
context.router.popUntilRoot();
|
),
|
||||||
|
actionButtonTitle: 'basis.continue'.tr(),
|
||||||
|
actionButtonOnPressed: () {
|
||||||
|
context.read<ApiProviderVolumeCubit>().resizeVolume(
|
||||||
|
widget.diskVolumeToResize,
|
||||||
|
DiskSize.fromGibibyte(
|
||||||
|
_currentSliderGbValue.truncate().toDouble(),
|
||||||
|
),
|
||||||
|
context.read<ApiServerVolumeCubit>().reload,
|
||||||
|
);
|
||||||
|
context.router.popUntilRoot();
|
||||||
|
},
|
||||||
|
);
|
||||||
},
|
},
|
||||||
child: Text('storage.extend_volume_button.title'.tr()),
|
child: Text('storage.extend_volume_button.title'.tr()),
|
||||||
),
|
),
|
||||||
|
|
|
@ -45,8 +45,11 @@ class StorageCard extends StatelessWidget {
|
||||||
clipBehavior: Clip.antiAlias,
|
clipBehavior: Clip.antiAlias,
|
||||||
child: InkResponse(
|
child: InkResponse(
|
||||||
highlightShape: BoxShape.rectangle,
|
highlightShape: BoxShape.rectangle,
|
||||||
onTap: () =>
|
|
||||||
context.pushRoute(ServerStorageRoute(diskStatus: diskStatus)),
|
/// TODO: when 'isEmpty' replace with a skeleton
|
||||||
|
onTap: () => diskStatus.diskVolumes.isEmpty
|
||||||
|
? null
|
||||||
|
: context.pushRoute(ServerStorageRoute(diskStatus: diskStatus)),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
|
|
@ -7,9 +7,10 @@ import 'package:selfprivacy/logic/cubit/services/services_cubit.dart';
|
||||||
import 'package:selfprivacy/logic/models/service.dart';
|
import 'package:selfprivacy/logic/models/service.dart';
|
||||||
import 'package:selfprivacy/logic/models/state_types.dart';
|
import 'package:selfprivacy/logic/models/state_types.dart';
|
||||||
import 'package:selfprivacy/ui/components/brand_header/brand_header.dart';
|
import 'package:selfprivacy/ui/components/brand_header/brand_header.dart';
|
||||||
|
import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart';
|
||||||
import 'package:selfprivacy/ui/components/icon_status_mask/icon_status_mask.dart';
|
import 'package:selfprivacy/ui/components/icon_status_mask/icon_status_mask.dart';
|
||||||
import 'package:selfprivacy/ui/components/not_ready_card/not_ready_card.dart';
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:selfprivacy/ui/helpers/empty_page_placeholder.dart';
|
||||||
import 'package:selfprivacy/ui/router/router.dart';
|
import 'package:selfprivacy/ui/router/router.dart';
|
||||||
import 'package:selfprivacy/utils/breakpoints.dart';
|
import 'package:selfprivacy/utils/breakpoints.dart';
|
||||||
import 'package:selfprivacy/utils/launch_url.dart';
|
import 'package:selfprivacy/utils/launch_url.dart';
|
||||||
|
@ -42,30 +43,36 @@ class _ServicesPageState extends State<ServicesPage> {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
body: RefreshIndicator(
|
body: !isReady
|
||||||
onRefresh: () async {
|
? EmptyPagePlaceholder(
|
||||||
await context.read<ServicesCubit>().reload();
|
showReadyCard: true,
|
||||||
},
|
title: 'service_page.nothing_here'.tr(),
|
||||||
child: ListView(
|
description: 'basis.please_connect'.tr(),
|
||||||
padding: paddingH15V0,
|
iconData: BrandIcons.box,
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'basis.services_title'.tr(),
|
|
||||||
style: Theme.of(context).textTheme.bodyLarge,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
if (!isReady) ...[const NotReadyCard(), const SizedBox(height: 24)],
|
|
||||||
...services.map(
|
|
||||||
(final service) => Padding(
|
|
||||||
padding: const EdgeInsets.only(
|
|
||||||
bottom: 30,
|
|
||||||
),
|
|
||||||
child: _Card(service: service),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
],
|
: RefreshIndicator(
|
||||||
),
|
onRefresh: () async {
|
||||||
),
|
await context.read<ServicesCubit>().reload();
|
||||||
|
},
|
||||||
|
child: ListView(
|
||||||
|
padding: paddingH15V0,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'basis.services_title'.tr(),
|
||||||
|
style: Theme.of(context).textTheme.bodyLarge,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
...services.map(
|
||||||
|
(final service) => Padding(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
bottom: 30,
|
||||||
|
),
|
||||||
|
child: _Card(service: service),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -129,16 +136,16 @@ class _Card extends StatelessWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
service.displayName,
|
||||||
|
style: Theme.of(context).textTheme.headlineMedium,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Column(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(height: 12),
|
|
||||||
Text(
|
|
||||||
service.displayName,
|
|
||||||
style: Theme.of(context).textTheme.headlineMedium,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
if (service.url != '' && service.url != null)
|
if (service.url != '' && service.url != null)
|
||||||
Column(
|
Column(
|
||||||
|
@ -146,7 +153,7 @@ class _Card extends StatelessWidget {
|
||||||
_ServiceLink(
|
_ServiceLink(
|
||||||
url: service.url ?? '',
|
url: service.url ?? '',
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 8),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
if (service.id == 'mailserver')
|
if (service.id == 'mailserver')
|
||||||
|
@ -156,19 +163,21 @@ class _Card extends StatelessWidget {
|
||||||
url: domainName,
|
url: domainName,
|
||||||
isActive: false,
|
isActive: false,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 8),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Text(
|
|
||||||
service.loginInfo,
|
|
||||||
style: Theme.of(context).textTheme.bodyMedium,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
Text(
|
Text(
|
||||||
service.description,
|
service.description,
|
||||||
style: Theme.of(context).textTheme.bodyMedium,
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
service.loginInfo,
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
|
color: Theme.of(context).colorScheme.secondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
|
|
@ -2,11 +2,10 @@ import 'package:cubit_form/cubit_form.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_svg/flutter_svg.dart';
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
import 'package:selfprivacy/config/brand_theme.dart';
|
|
||||||
import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart';
|
import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart';
|
||||||
import 'package:selfprivacy/logic/cubit/forms/setup/initializing/dns_provider_form_cubit.dart';
|
import 'package:selfprivacy/logic/cubit/forms/setup/initializing/dns_provider_form_cubit.dart';
|
||||||
|
import 'package:selfprivacy/logic/cubit/support_system/support_system_cubit.dart';
|
||||||
import 'package:selfprivacy/logic/models/hive/server_domain.dart';
|
import 'package:selfprivacy/logic/models/hive/server_domain.dart';
|
||||||
import 'package:selfprivacy/ui/components/brand_md/brand_md.dart';
|
|
||||||
import 'package:selfprivacy/ui/components/buttons/brand_button.dart';
|
import 'package:selfprivacy/ui/components/buttons/brand_button.dart';
|
||||||
import 'package:selfprivacy/ui/components/buttons/outlined_button.dart';
|
import 'package:selfprivacy/ui/components/buttons/outlined_button.dart';
|
||||||
import 'package:selfprivacy/ui/components/cards/outlined_card.dart';
|
import 'package:selfprivacy/ui/components/cards/outlined_card.dart';
|
||||||
|
@ -125,22 +124,10 @@ class ProviderInputDataPage extends StatelessWidget {
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
BrandOutlinedButton(
|
BrandOutlinedButton(
|
||||||
child: Text('initializing.how'.tr()),
|
child: Text('initializing.how'.tr()),
|
||||||
onPressed: () => showModalBottomSheet<void>(
|
onPressed: () => context.read<SupportSystemCubit>().showArticle(
|
||||||
context: context,
|
article: providerInfo.pathToHow,
|
||||||
isScrollControlled: true,
|
context: context,
|
||||||
backgroundColor: Colors.transparent,
|
|
||||||
builder: (final BuildContext context) => Padding(
|
|
||||||
padding: paddingH15V0,
|
|
||||||
child: ListView(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
|
||||||
children: [
|
|
||||||
BrandMarkdown(
|
|
||||||
fileName: providerInfo.pathToHow,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
|
@ -0,0 +1,163 @@
|
||||||
|
import 'package:cubit_form/cubit_form.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
|
||||||
|
import 'package:selfprivacy/logic/cubit/forms/setup/initializing/domain_setup_cubit.dart';
|
||||||
|
import 'package:selfprivacy/ui/components/buttons/brand_button.dart';
|
||||||
|
import 'package:selfprivacy/ui/components/cards/outlined_card.dart';
|
||||||
|
import 'package:selfprivacy/ui/layouts/responsive_layout_with_infobox.dart';
|
||||||
|
|
||||||
|
class DomainPicker extends StatefulWidget {
|
||||||
|
const DomainPicker({
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<DomainPicker> createState() => _DomainPickerState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DomainPickerState extends State<DomainPicker> {
|
||||||
|
String? selectedDomain;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(final BuildContext context) {
|
||||||
|
final DomainSetupState state = context.watch<DomainSetupCubit>().state;
|
||||||
|
|
||||||
|
return ResponsiveLayoutWithInfobox(
|
||||||
|
topChild: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
(state is MoreThenOne)
|
||||||
|
? 'initializing.multiple_domains_found'.tr()
|
||||||
|
: 'initializing.use_this_domain'.tr(),
|
||||||
|
style: Theme.of(context).textTheme.headlineSmall,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
(state is MoreThenOne)
|
||||||
|
? 'initializing.multiple_domains_found_text'.tr()
|
||||||
|
: 'initializing.use_this_domain_text'.tr(),
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
primaryColumn: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
if (state is Empty)
|
||||||
|
Text(
|
||||||
|
'initializing.no_connected_domains'.tr(),
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
|
),
|
||||||
|
if (state is Loading)
|
||||||
|
Text(
|
||||||
|
state.type == LoadingTypes.loadingDomain
|
||||||
|
? 'initializing.loading_domain_list'.tr()
|
||||||
|
: 'basis.saving'.tr(),
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
|
),
|
||||||
|
if (state is MoreThenOne)
|
||||||
|
...state.domains.map(
|
||||||
|
(final domain) => Column(
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: OutlinedCard(
|
||||||
|
borderColor: domain == selectedDomain
|
||||||
|
? Theme.of(context).colorScheme.primary
|
||||||
|
: null,
|
||||||
|
borderWidth: domain == selectedDomain ? 3 : 1,
|
||||||
|
child: InkResponse(
|
||||||
|
highlightShape: BoxShape.rectangle,
|
||||||
|
onTap: () => setState(() {
|
||||||
|
selectedDomain = domain;
|
||||||
|
}),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Radio<String>(
|
||||||
|
value: domain,
|
||||||
|
groupValue: selectedDomain,
|
||||||
|
onChanged: (final String? value) {
|
||||||
|
setState(() {
|
||||||
|
selectedDomain = value;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
domain,
|
||||||
|
style: Theme.of(context).textTheme.bodyLarge,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
// Button to select and save domain
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (state is MoreThenOne)
|
||||||
|
BrandButton.filled(
|
||||||
|
onPressed: (selectedDomain != null &&
|
||||||
|
state.domains.contains(selectedDomain))
|
||||||
|
? () => context
|
||||||
|
.read<DomainSetupCubit>()
|
||||||
|
.saveDomain(selectedDomain!)
|
||||||
|
: null,
|
||||||
|
child: Text('initializing.use_this_domain'.tr()),
|
||||||
|
),
|
||||||
|
if (state is Loaded) ...[
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
state.domain,
|
||||||
|
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
||||||
|
color: Theme.of(context).colorScheme.onBackground,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
if (state is Empty) ...[
|
||||||
|
const SizedBox(height: 30),
|
||||||
|
BrandButton.filled(
|
||||||
|
onPressed: () => context.read<DomainSetupCubit>().load(),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
Icons.refresh,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Text(
|
||||||
|
'domain.update_list'.tr(),
|
||||||
|
style: Theme.of(context).textTheme.bodyLarge,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
if (state is Loaded) ...[
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
BrandButton.filled(
|
||||||
|
onPressed: () =>
|
||||||
|
context.read<DomainSetupCubit>().saveDomain(state.domain),
|
||||||
|
text: 'initializing.save_domain'.tr(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,6 +18,7 @@ import 'package:selfprivacy/ui/components/progress_bar/progress_bar.dart';
|
||||||
import 'package:selfprivacy/ui/components/drawers/support_drawer.dart';
|
import 'package:selfprivacy/ui/components/drawers/support_drawer.dart';
|
||||||
import 'package:selfprivacy/ui/layouts/responsive_layout_with_infobox.dart';
|
import 'package:selfprivacy/ui/layouts/responsive_layout_with_infobox.dart';
|
||||||
import 'package:selfprivacy/ui/pages/setup/initializing/dns_provider_picker.dart';
|
import 'package:selfprivacy/ui/pages/setup/initializing/dns_provider_picker.dart';
|
||||||
|
import 'package:selfprivacy/ui/pages/setup/initializing/domain_picker.dart';
|
||||||
import 'package:selfprivacy/ui/pages/setup/initializing/server_provider_picker.dart';
|
import 'package:selfprivacy/ui/pages/setup/initializing/server_provider_picker.dart';
|
||||||
import 'package:selfprivacy/ui/pages/setup/initializing/server_type_picker.dart';
|
import 'package:selfprivacy/ui/pages/setup/initializing/server_type_picker.dart';
|
||||||
import 'package:selfprivacy/ui/pages/setup/recovering/recovery_routing.dart';
|
import 'package:selfprivacy/ui/pages/setup/recovering/recovery_routing.dart';
|
||||||
|
@ -99,7 +100,7 @@ class InitializingPage extends StatelessWidget {
|
||||||
steps: const [
|
steps: const [
|
||||||
'Hosting',
|
'Hosting',
|
||||||
'Server Type',
|
'Server Type',
|
||||||
'CloudFlare',
|
'DNS Provider',
|
||||||
'Backblaze',
|
'Backblaze',
|
||||||
'Domain',
|
'Domain',
|
||||||
'User',
|
'User',
|
||||||
|
@ -319,98 +320,7 @@ class InitializingPage extends StatelessWidget {
|
||||||
Widget _stepDomain(final ServerInstallationCubit initializingCubit) =>
|
Widget _stepDomain(final ServerInstallationCubit initializingCubit) =>
|
||||||
BlocProvider(
|
BlocProvider(
|
||||||
create: (final context) => DomainSetupCubit(initializingCubit)..load(),
|
create: (final context) => DomainSetupCubit(initializingCubit)..load(),
|
||||||
child: Builder(
|
child: const DomainPicker(),
|
||||||
builder: (final context) {
|
|
||||||
final DomainSetupState state =
|
|
||||||
context.watch<DomainSetupCubit>().state;
|
|
||||||
return ResponsiveLayoutWithInfobox(
|
|
||||||
topChild: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'initializing.use_this_domain'.tr(),
|
|
||||||
style: Theme.of(context).textTheme.headlineSmall,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Text(
|
|
||||||
'initializing.use_this_domain_text'.tr(),
|
|
||||||
style: Theme.of(context).textTheme.bodyMedium,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
primaryColumn: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
if (state is Empty)
|
|
||||||
Text(
|
|
||||||
'initializing.no_connected_domains'.tr(),
|
|
||||||
style: Theme.of(context).textTheme.bodyMedium,
|
|
||||||
),
|
|
||||||
if (state is Loading)
|
|
||||||
Text(
|
|
||||||
state.type == LoadingTypes.loadingDomain
|
|
||||||
? 'initializing.loading_domain_list'.tr()
|
|
||||||
: 'basis.saving'.tr(),
|
|
||||||
style: Theme.of(context).textTheme.bodyMedium,
|
|
||||||
),
|
|
||||||
if (state is MoreThenOne)
|
|
||||||
Text(
|
|
||||||
'initializing.found_more_domains'.tr(),
|
|
||||||
style: Theme.of(context).textTheme.bodyMedium,
|
|
||||||
),
|
|
||||||
if (state is Loaded) ...[
|
|
||||||
Row(
|
|
||||||
mainAxisSize: MainAxisSize.max,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
state.domain,
|
|
||||||
style: Theme.of(context)
|
|
||||||
.textTheme
|
|
||||||
.headlineMedium
|
|
||||||
?.copyWith(
|
|
||||||
color:
|
|
||||||
Theme.of(context).colorScheme.onBackground,
|
|
||||||
),
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
if (state is Empty) ...[
|
|
||||||
const SizedBox(height: 30),
|
|
||||||
BrandButton.filled(
|
|
||||||
onPressed: () => context.read<DomainSetupCubit>().load(),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
const Icon(
|
|
||||||
Icons.refresh,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 10),
|
|
||||||
Text(
|
|
||||||
'domain.update_list'.tr(),
|
|
||||||
style: Theme.of(context).textTheme.bodyLarge,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
if (state is Loaded) ...[
|
|
||||||
const SizedBox(height: 32),
|
|
||||||
BrandButton.filled(
|
|
||||||
onPressed: () =>
|
|
||||||
context.read<DomainSetupCubit>().saveDomain(),
|
|
||||||
text: 'initializing.save_domain'.tr(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _stepUser(final ServerInstallationCubit initializingCubit) =>
|
Widget _stepUser(final ServerInstallationCubit initializingCubit) =>
|
||||||
|
|
|
@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:selfprivacy/illustrations/stray_deer.dart';
|
import 'package:selfprivacy/illustrations/stray_deer.dart';
|
||||||
import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart';
|
import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart';
|
||||||
import 'package:selfprivacy/logic/cubit/app_settings/app_settings_cubit.dart';
|
import 'package:selfprivacy/logic/cubit/app_settings/app_settings_cubit.dart';
|
||||||
|
import 'package:selfprivacy/logic/models/price.dart';
|
||||||
import 'package:selfprivacy/logic/models/server_provider_location.dart';
|
import 'package:selfprivacy/logic/models/server_provider_location.dart';
|
||||||
import 'package:selfprivacy/logic/models/server_type.dart';
|
import 'package:selfprivacy/logic/models/server_type.dart';
|
||||||
import 'package:selfprivacy/ui/components/buttons/brand_button.dart';
|
import 'package:selfprivacy/ui/components/buttons/brand_button.dart';
|
||||||
|
@ -25,7 +26,12 @@ class _ServerTypePickerState extends State<ServerTypePicker> {
|
||||||
ServerProviderLocation? serverProviderLocation;
|
ServerProviderLocation? serverProviderLocation;
|
||||||
ServerType? serverType;
|
ServerType? serverType;
|
||||||
|
|
||||||
void setServerProviderLocation(final ServerProviderLocation? location) {
|
void setServerProviderLocation(final ServerProviderLocation? location) async {
|
||||||
|
if (location != null) {
|
||||||
|
await widget.serverInstallationCubit.setLocationIdentifier(
|
||||||
|
location.identifier,
|
||||||
|
);
|
||||||
|
}
|
||||||
setState(() {
|
setState(() {
|
||||||
serverProviderLocation = location;
|
serverProviderLocation = location;
|
||||||
});
|
});
|
||||||
|
@ -153,194 +159,320 @@ class SelectTypePage extends StatelessWidget {
|
||||||
final Function backToLocationPickingCallback;
|
final Function backToLocationPickingCallback;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(final BuildContext context) => FutureBuilder(
|
Widget build(final BuildContext context) {
|
||||||
future: serverInstallationCubit.fetchAvailableTypesByLocation(location),
|
final Future<List<ServerType>> serverTypes =
|
||||||
builder: (
|
serverInstallationCubit.fetchAvailableTypesByLocation(location);
|
||||||
final BuildContext context,
|
final Future<AdditionalPricing?> prices =
|
||||||
final AsyncSnapshot<Object?> snapshot,
|
serverInstallationCubit.fetchAvailableAdditionalPricing();
|
||||||
) {
|
return FutureBuilder(
|
||||||
if (snapshot.hasData) {
|
future: Future.wait([
|
||||||
if ((snapshot.data as List<ServerType>).isEmpty) {
|
serverTypes,
|
||||||
return Column(
|
prices,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
]),
|
||||||
children: [
|
builder: (
|
||||||
Text(
|
final BuildContext context,
|
||||||
'initializing.locations_not_found'.tr(),
|
final AsyncSnapshot<List<dynamic>> snapshot,
|
||||||
style: Theme.of(context).textTheme.headlineSmall,
|
) {
|
||||||
),
|
if (snapshot.hasData) {
|
||||||
const SizedBox(height: 16),
|
if ((snapshot.data![0] as List<ServerType>).isEmpty ||
|
||||||
Text(
|
(snapshot.data![1] as AdditionalPricing?) == null) {
|
||||||
'initializing.locations_not_found_text'.tr(),
|
return Column(
|
||||||
style: Theme.of(context).textTheme.bodyMedium,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
),
|
children: [
|
||||||
LayoutBuilder(
|
Text(
|
||||||
builder: (final context, final constraints) => CustomPaint(
|
'initializing.locations_not_found'.tr(),
|
||||||
size: Size(
|
style: Theme.of(context).textTheme.headlineSmall,
|
||||||
constraints.maxWidth,
|
),
|
||||||
(constraints.maxWidth * 1).toDouble(),
|
const SizedBox(height: 16),
|
||||||
),
|
Text(
|
||||||
painter: StrayDeerPainter(
|
'initializing.locations_not_found_text'.tr(),
|
||||||
colorScheme: Theme.of(context).colorScheme,
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
colorPalette: context
|
),
|
||||||
.read<AppSettingsCubit>()
|
LayoutBuilder(
|
||||||
.state
|
builder: (final context, final constraints) => CustomPaint(
|
||||||
.corePaletteOrDefault,
|
size: Size(
|
||||||
),
|
constraints.maxWidth,
|
||||||
|
(constraints.maxWidth * 1).toDouble(),
|
||||||
|
),
|
||||||
|
painter: StrayDeerPainter(
|
||||||
|
colorScheme: Theme.of(context).colorScheme,
|
||||||
|
colorPalette: context
|
||||||
|
.read<AppSettingsCubit>()
|
||||||
|
.state
|
||||||
|
.corePaletteOrDefault,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
),
|
||||||
BrandButton.rised(
|
const SizedBox(height: 16),
|
||||||
onPressed: () {
|
BrandButton.rised(
|
||||||
backToLocationPickingCallback();
|
onPressed: () {
|
||||||
},
|
backToLocationPickingCallback();
|
||||||
text: 'initializing.back_to_locations'.tr(),
|
},
|
||||||
),
|
text: 'initializing.back_to_locations'.tr(),
|
||||||
],
|
),
|
||||||
);
|
],
|
||||||
}
|
);
|
||||||
return ResponsiveLayoutWithInfobox(
|
}
|
||||||
topChild: Column(
|
final prices = snapshot.data![1] as AdditionalPricing;
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
final storagePrice = serverInstallationCubit.initialStorage.gibibyte *
|
||||||
children: [
|
prices.perVolumeGb.value;
|
||||||
Text(
|
final publicIpPrice = prices.perPublicIpv4.value;
|
||||||
'initializing.choose_server_type'.tr(),
|
return ResponsiveLayoutWithInfobox(
|
||||||
style: Theme.of(context).textTheme.headlineSmall,
|
topChild: Column(
|
||||||
),
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
const SizedBox(height: 16),
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'initializing.choose_server_type_text'.tr(),
|
'initializing.choose_server_type'.tr(),
|
||||||
style: Theme.of(context).textTheme.bodyMedium,
|
style: Theme.of(context).textTheme.headlineSmall,
|
||||||
),
|
),
|
||||||
],
|
const SizedBox(height: 16),
|
||||||
),
|
Text(
|
||||||
primaryColumn: Column(
|
'initializing.choose_server_type_text'.tr(),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
children: [
|
),
|
||||||
...(snapshot.data! as List<ServerType>).map(
|
],
|
||||||
(final type) => Column(
|
),
|
||||||
children: [
|
primaryColumn: Column(
|
||||||
SizedBox(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
width: double.infinity,
|
children: [
|
||||||
child: InkWell(
|
...(snapshot.data![0] as List<ServerType>).map(
|
||||||
onTap: () {
|
(final type) => Column(
|
||||||
serverInstallationCubit.setServerType(type);
|
children: [
|
||||||
},
|
SizedBox(
|
||||||
child: Card(
|
width: double.infinity,
|
||||||
child: Padding(
|
child: InkWell(
|
||||||
padding: const EdgeInsets.all(16.0),
|
onTap: () {
|
||||||
child: Column(
|
serverInstallationCubit.setServerType(type);
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
},
|
||||||
children: [
|
child: Card(
|
||||||
Text(
|
child: Padding(
|
||||||
type.title,
|
padding: const EdgeInsets.all(16.0),
|
||||||
style: Theme.of(context)
|
child: Column(
|
||||||
.textTheme
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
.titleMedium,
|
children: [
|
||||||
),
|
Text(
|
||||||
const SizedBox(height: 8),
|
type.title,
|
||||||
Row(
|
style:
|
||||||
|
Theme.of(context).textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.memory_outlined,
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onSurface,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
'server.core_count'.plural(type.cores),
|
||||||
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.bodyMedium,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.memory_outlined,
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onSurface,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
'initializing.choose_server_type_ram'
|
||||||
|
.tr(args: [type.ram.toString()]),
|
||||||
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.bodyMedium,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.sd_card_outlined,
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onSurface,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
'initializing.choose_server_type_storage'
|
||||||
|
.tr(
|
||||||
|
args: [type.disk.gibibyte.toString()],
|
||||||
|
),
|
||||||
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.bodyMedium,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
const Divider(height: 8),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.payments_outlined,
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onSurface,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
'initializing.choose_server_type_payment_per_month'
|
||||||
|
.tr(
|
||||||
|
args: [
|
||||||
|
'${(type.price.value + storagePrice + publicIpPrice).toStringAsFixed(4)} ${type.price.currency.shortcode}'
|
||||||
|
],
|
||||||
|
),
|
||||||
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.bodyLarge,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
IntrinsicHeight(
|
||||||
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
VerticalDivider(
|
||||||
Icons.memory_outlined,
|
width: 24.0,
|
||||||
|
indent: 4.0,
|
||||||
|
endIndent: 4.0,
|
||||||
color: Theme.of(context)
|
color: Theme.of(context)
|
||||||
.colorScheme
|
.colorScheme
|
||||||
.onSurface,
|
.onSurface
|
||||||
|
.withAlpha(128),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(
|
Column(
|
||||||
'server.core_count'
|
crossAxisAlignment:
|
||||||
.plural(type.cores),
|
CrossAxisAlignment.start,
|
||||||
style: Theme.of(context)
|
children: [
|
||||||
.textTheme
|
Row(
|
||||||
.bodyMedium,
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.memory_outlined,
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onSurface
|
||||||
|
.withAlpha(128),
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
'initializing.choose_server_type_payment_server'
|
||||||
|
.tr(
|
||||||
|
args: [
|
||||||
|
type.price.value
|
||||||
|
.toString()
|
||||||
|
],
|
||||||
|
),
|
||||||
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.bodyMedium
|
||||||
|
?.copyWith(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onSurface
|
||||||
|
.withAlpha(128),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.sd_card_outlined,
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onSurface
|
||||||
|
.withAlpha(128),
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
'initializing.choose_server_type_payment_storage'
|
||||||
|
.tr(
|
||||||
|
args: [
|
||||||
|
storagePrice.toString()
|
||||||
|
],
|
||||||
|
),
|
||||||
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.bodyMedium
|
||||||
|
?.copyWith(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onSurface
|
||||||
|
.withAlpha(128),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (publicIpPrice != 0)
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.lan_outlined,
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onSurface
|
||||||
|
.withAlpha(128),
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
'initializing.choose_server_type_payment_ip'
|
||||||
|
.tr(
|
||||||
|
args: [
|
||||||
|
publicIpPrice.toString()
|
||||||
|
],
|
||||||
|
),
|
||||||
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.bodyMedium
|
||||||
|
?.copyWith(
|
||||||
|
color: Theme.of(
|
||||||
|
context,
|
||||||
|
)
|
||||||
|
.colorScheme
|
||||||
|
.onSurface
|
||||||
|
.withAlpha(128),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
),
|
||||||
Row(
|
],
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
Icons.memory_outlined,
|
|
||||||
color: Theme.of(context)
|
|
||||||
.colorScheme
|
|
||||||
.onSurface,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Text(
|
|
||||||
'initializing.choose_server_type_ram'
|
|
||||||
.tr(args: [type.ram.toString()]),
|
|
||||||
style: Theme.of(context)
|
|
||||||
.textTheme
|
|
||||||
.bodyMedium,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
Icons.sd_card_outlined,
|
|
||||||
color: Theme.of(context)
|
|
||||||
.colorScheme
|
|
||||||
.onSurface,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Text(
|
|
||||||
'initializing.choose_server_type_storage'
|
|
||||||
.tr(
|
|
||||||
args: [
|
|
||||||
type.disk.gibibyte.toString()
|
|
||||||
],
|
|
||||||
),
|
|
||||||
style: Theme.of(context)
|
|
||||||
.textTheme
|
|
||||||
.bodyMedium,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
const Divider(height: 8),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
Icons.payments_outlined,
|
|
||||||
color: Theme.of(context)
|
|
||||||
.colorScheme
|
|
||||||
.onSurface,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Text(
|
|
||||||
'initializing.choose_server_type_payment_per_month'
|
|
||||||
.tr(
|
|
||||||
args: [
|
|
||||||
'${type.price.value.toString()} ${type.price.currency.shortcode}'
|
|
||||||
],
|
|
||||||
),
|
|
||||||
style: Theme.of(context)
|
|
||||||
.textTheme
|
|
||||||
.bodyLarge,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
),
|
||||||
],
|
const SizedBox(height: 8),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
secondaryColumn:
|
),
|
||||||
InfoBox(text: 'initializing.choose_server_type_notice'.tr()),
|
secondaryColumn:
|
||||||
);
|
InfoBox(text: 'initializing.choose_server_type_notice'.tr()),
|
||||||
} else {
|
);
|
||||||
return const Center(child: CircularProgressIndicator());
|
} else {
|
||||||
}
|
return const Center(child: CircularProgressIndicator());
|
||||||
},
|
}
|
||||||
);
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,73 +0,0 @@
|
||||||
part of 'users.dart';
|
|
||||||
|
|
||||||
class _NoUsers extends StatelessWidget {
|
|
||||||
const _NoUsers({required this.text});
|
|
||||||
|
|
||||||
final String text;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(final BuildContext context) => Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
BrandIcons.users,
|
|
||||||
size: 50,
|
|
||||||
color: Theme.of(context).colorScheme.onBackground,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
Text(
|
|
||||||
'users.nobody_here'.tr(),
|
|
||||||
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
|
||||||
color: Theme.of(context).colorScheme.onBackground,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
Text(
|
|
||||||
text,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
|
||||||
color: Theme.of(context).colorScheme.onBackground,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
class _CouldNotLoadUsers extends StatelessWidget {
|
|
||||||
const _CouldNotLoadUsers({required this.text});
|
|
||||||
|
|
||||||
final String text;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(final BuildContext context) => Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
BrandIcons.users,
|
|
||||||
size: 50,
|
|
||||||
color: Theme.of(context).colorScheme.onBackground,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
Text(
|
|
||||||
'users.could_not_fetch_users'.tr(),
|
|
||||||
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
|
||||||
color: Theme.of(context).colorScheme.onBackground,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
Text(
|
|
||||||
text,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
|
||||||
color: Theme.of(context).colorScheme.onBackground,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -3,11 +3,11 @@ part of 'users.dart';
|
||||||
class _User extends StatelessWidget {
|
class _User extends StatelessWidget {
|
||||||
const _User({
|
const _User({
|
||||||
required this.user,
|
required this.user,
|
||||||
required this.isRootUser,
|
required this.isPrimaryUser,
|
||||||
});
|
});
|
||||||
|
|
||||||
final User user;
|
final User user;
|
||||||
final bool isRootUser;
|
final bool isPrimaryUser;
|
||||||
@override
|
@override
|
||||||
Widget build(final BuildContext context) => InkWell(
|
Widget build(final BuildContext context) => InkWell(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
@ -32,7 +32,7 @@ class _User extends StatelessWidget {
|
||||||
user.login,
|
user.login,
|
||||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||||
color: Theme.of(context).colorScheme.onBackground,
|
color: Theme.of(context).colorScheme.onBackground,
|
||||||
decoration: isRootUser
|
decoration: isPrimaryUser
|
||||||
? TextDecoration.underline
|
? TextDecoration.underline
|
||||||
: user.isFoundOnServer
|
: user.isFoundOnServer
|
||||||
? TextDecoration.none
|
? TextDecoration.none
|
||||||
|
|
|
@ -143,17 +143,27 @@ class _UserLogins extends StatelessWidget {
|
||||||
final String domainName;
|
final String domainName;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(final BuildContext context) => FilledCard(
|
Widget build(final BuildContext context) {
|
||||||
child: Column(
|
final email = '${user.login}@$domainName';
|
||||||
children: [
|
return FilledCard(
|
||||||
ListTileOnSurfaceVariant(
|
child: Column(
|
||||||
title: '${user.login}@$domainName',
|
children: [
|
||||||
subtitle: 'users.email_login'.tr(),
|
ListTileOnSurfaceVariant(
|
||||||
leadingIcon: Icons.alternate_email_outlined,
|
onTap: () {
|
||||||
),
|
PlatformAdapter.setClipboard(email);
|
||||||
],
|
getIt<NavigationService>().showSnackBar(
|
||||||
),
|
'basis.copied_to_clipboard'.tr(),
|
||||||
);
|
behavior: SnackBarBehavior.floating,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
title: email,
|
||||||
|
subtitle: 'users.email_login'.tr(),
|
||||||
|
leadingIcon: Icons.alternate_email_outlined,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SshKeysCard extends StatelessWidget {
|
class _SshKeysCard extends StatelessWidget {
|
||||||
|
|
|
@ -3,6 +3,7 @@ import 'package:cubit_form/cubit_form.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:selfprivacy/config/brand_theme.dart';
|
import 'package:selfprivacy/config/brand_theme.dart';
|
||||||
|
import 'package:selfprivacy/config/get_it_config.dart';
|
||||||
import 'package:selfprivacy/logic/cubit/forms/user/ssh_form_cubit.dart';
|
import 'package:selfprivacy/logic/cubit/forms/user/ssh_form_cubit.dart';
|
||||||
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
|
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
|
||||||
import 'package:selfprivacy/logic/cubit/forms/factories/field_cubit_factory.dart';
|
import 'package:selfprivacy/logic/cubit/forms/factories/field_cubit_factory.dart';
|
||||||
|
@ -15,16 +16,16 @@ import 'package:selfprivacy/ui/components/buttons/brand_button.dart';
|
||||||
import 'package:selfprivacy/ui/components/buttons/outlined_button.dart';
|
import 'package:selfprivacy/ui/components/buttons/outlined_button.dart';
|
||||||
import 'package:selfprivacy/ui/components/cards/filled_card.dart';
|
import 'package:selfprivacy/ui/components/cards/filled_card.dart';
|
||||||
import 'package:selfprivacy/ui/components/brand_header/brand_header.dart';
|
import 'package:selfprivacy/ui/components/brand_header/brand_header.dart';
|
||||||
|
import 'package:selfprivacy/ui/helpers/empty_page_placeholder.dart';
|
||||||
import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart';
|
import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart';
|
||||||
import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart';
|
import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart';
|
||||||
import 'package:selfprivacy/ui/components/info_box/info_box.dart';
|
import 'package:selfprivacy/ui/components/info_box/info_box.dart';
|
||||||
import 'package:selfprivacy/ui/components/list_tiles/list_tile_on_surface_variant.dart';
|
import 'package:selfprivacy/ui/components/list_tiles/list_tile_on_surface_variant.dart';
|
||||||
import 'package:selfprivacy/ui/components/not_ready_card/not_ready_card.dart';
|
|
||||||
import 'package:selfprivacy/ui/router/router.dart';
|
import 'package:selfprivacy/ui/router/router.dart';
|
||||||
import 'package:selfprivacy/utils/breakpoints.dart';
|
import 'package:selfprivacy/utils/breakpoints.dart';
|
||||||
|
import 'package:selfprivacy/utils/platform_adapter.dart';
|
||||||
import 'package:selfprivacy/utils/ui_helpers.dart';
|
import 'package:selfprivacy/utils/ui_helpers.dart';
|
||||||
|
|
||||||
part 'empty.dart';
|
|
||||||
part 'new_user.dart';
|
part 'new_user.dart';
|
||||||
part 'user.dart';
|
part 'user.dart';
|
||||||
part 'user_details.dart';
|
part 'user_details.dart';
|
||||||
|
@ -41,19 +42,16 @@ class UsersPage extends StatelessWidget {
|
||||||
Widget child;
|
Widget child;
|
||||||
|
|
||||||
if (!isReady) {
|
if (!isReady) {
|
||||||
child = isNotReady();
|
child = EmptyPagePlaceholder(
|
||||||
|
showReadyCard: true,
|
||||||
|
title: 'users.nobody_here'.tr(),
|
||||||
|
description: 'basis.please_connect'.tr(),
|
||||||
|
iconData: BrandIcons.users,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
child = BlocBuilder<UsersCubit, UsersState>(
|
child = BlocBuilder<UsersCubit, UsersState>(
|
||||||
builder: (final BuildContext context, final UsersState state) {
|
builder: (final BuildContext context, final UsersState state) {
|
||||||
final List<User> users = state.users
|
final users = state.orderedUsers;
|
||||||
.where((final user) => user.type != UserType.root)
|
|
||||||
.toList();
|
|
||||||
// final List<User> users = [];
|
|
||||||
users.sort(
|
|
||||||
(final User a, final User b) =>
|
|
||||||
a.login.toLowerCase().compareTo(b.login.toLowerCase()),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (users.isEmpty) {
|
if (users.isEmpty) {
|
||||||
if (state.isLoading) {
|
if (state.isLoading) {
|
||||||
return const Center(
|
return const Center(
|
||||||
|
@ -70,8 +68,10 @@ class UsersPage extends StatelessWidget {
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
_CouldNotLoadUsers(
|
EmptyPagePlaceholder(
|
||||||
text: 'users.could_not_fetch_description'.tr(),
|
title: 'users.could_not_fetch_users'.tr(),
|
||||||
|
description: 'users.could_not_fetch_description'.tr(),
|
||||||
|
iconData: BrandIcons.users,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 18),
|
const SizedBox(height: 18),
|
||||||
BrandOutlinedButton(
|
BrandOutlinedButton(
|
||||||
|
@ -115,7 +115,7 @@ class UsersPage extends StatelessWidget {
|
||||||
itemBuilder:
|
itemBuilder:
|
||||||
(final BuildContext context, final int index) => _User(
|
(final BuildContext context, final int index) => _User(
|
||||||
user: users[index],
|
user: users[index],
|
||||||
isRootUser: users[index].type == UserType.primary,
|
isPrimaryUser: users[index].type == UserType.primary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -138,24 +138,4 @@ class UsersPage extends StatelessWidget {
|
||||||
body: child,
|
body: child,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget isNotReady() => 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: _NoUsers(
|
|
||||||
text: 'users.not_ready'.tr(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,18 +15,6 @@ abstract class _$RootRouter extends RootStackRouter {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
final Map<String, PageFactory> pagesMap = {
|
final Map<String, PageFactory> pagesMap = {
|
||||||
OnboardingRoute.name: (routeData) {
|
|
||||||
return AutoRoutePage<dynamic>(
|
|
||||||
routeData: routeData,
|
|
||||||
child: const OnboardingPage(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
BackupDetailsRoute.name: (routeData) {
|
|
||||||
return AutoRoutePage<dynamic>(
|
|
||||||
routeData: routeData,
|
|
||||||
child: const BackupDetailsPage(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
BackupsListRoute.name: (routeData) {
|
BackupsListRoute.name: (routeData) {
|
||||||
final args = routeData.argsAs<BackupsListRouteArgs>();
|
final args = routeData.argsAs<BackupsListRouteArgs>();
|
||||||
return AutoRoutePage<dynamic>(
|
return AutoRoutePage<dynamic>(
|
||||||
|
@ -37,10 +25,58 @@ abstract class _$RootRouter extends RootStackRouter {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
RecoveryKeyRoute.name: (routeData) {
|
BackupDetailsRoute.name: (routeData) {
|
||||||
return AutoRoutePage<dynamic>(
|
return AutoRoutePage<dynamic>(
|
||||||
routeData: routeData,
|
routeData: routeData,
|
||||||
child: const RecoveryKeyPage(),
|
child: const BackupDetailsPage(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
DevicesRoute.name: (routeData) {
|
||||||
|
return AutoRoutePage<dynamic>(
|
||||||
|
routeData: routeData,
|
||||||
|
child: const DevicesScreen(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
DnsDetailsRoute.name: (routeData) {
|
||||||
|
return AutoRoutePage<dynamic>(
|
||||||
|
routeData: routeData,
|
||||||
|
child: const DnsDetailsPage(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
AboutApplicationRoute.name: (routeData) {
|
||||||
|
return AutoRoutePage<dynamic>(
|
||||||
|
routeData: routeData,
|
||||||
|
child: const AboutApplicationPage(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
AppSettingsRoute.name: (routeData) {
|
||||||
|
return AutoRoutePage<dynamic>(
|
||||||
|
routeData: routeData,
|
||||||
|
child: const AppSettingsPage(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
DeveloperSettingsRoute.name: (routeData) {
|
||||||
|
return AutoRoutePage<dynamic>(
|
||||||
|
routeData: routeData,
|
||||||
|
child: const DeveloperSettingsPage(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
ConsoleRoute.name: (routeData) {
|
||||||
|
return AutoRoutePage<dynamic>(
|
||||||
|
routeData: routeData,
|
||||||
|
child: const ConsolePage(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
MoreRoute.name: (routeData) {
|
||||||
|
return AutoRoutePage<dynamic>(
|
||||||
|
routeData: routeData,
|
||||||
|
child: const MorePage(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
OnboardingRoute.name: (routeData) {
|
||||||
|
return AutoRoutePage<dynamic>(
|
||||||
|
routeData: routeData,
|
||||||
|
child: const OnboardingPage(),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
ProvidersRoute.name: (routeData) {
|
ProvidersRoute.name: (routeData) {
|
||||||
|
@ -49,6 +85,18 @@ abstract class _$RootRouter extends RootStackRouter {
|
||||||
child: const ProvidersPage(),
|
child: const ProvidersPage(),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
RecoveryKeyRoute.name: (routeData) {
|
||||||
|
return AutoRoutePage<dynamic>(
|
||||||
|
routeData: routeData,
|
||||||
|
child: const RecoveryKeyPage(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
RootRoute.name: (routeData) {
|
||||||
|
return AutoRoutePage<dynamic>(
|
||||||
|
routeData: routeData,
|
||||||
|
child: WrappedRoute(child: const RootPage()),
|
||||||
|
);
|
||||||
|
},
|
||||||
ServerDetailsRoute.name: (routeData) {
|
ServerDetailsRoute.name: (routeData) {
|
||||||
return AutoRoutePage<dynamic>(
|
return AutoRoutePage<dynamic>(
|
||||||
routeData: routeData,
|
routeData: routeData,
|
||||||
|
@ -67,17 +115,6 @@ abstract class _$RootRouter extends RootStackRouter {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
ExtendingVolumeRoute.name: (routeData) {
|
|
||||||
final args = routeData.argsAs<ExtendingVolumeRouteArgs>();
|
|
||||||
return AutoRoutePage<dynamic>(
|
|
||||||
routeData: routeData,
|
|
||||||
child: ExtendingVolumePage(
|
|
||||||
diskVolumeToResize: args.diskVolumeToResize,
|
|
||||||
diskStatus: args.diskStatus,
|
|
||||||
key: args.key,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
ServerStorageRoute.name: (routeData) {
|
ServerStorageRoute.name: (routeData) {
|
||||||
final args = routeData.argsAs<ServerStorageRouteArgs>();
|
final args = routeData.argsAs<ServerStorageRouteArgs>();
|
||||||
return AutoRoutePage<dynamic>(
|
return AutoRoutePage<dynamic>(
|
||||||
|
@ -88,52 +125,15 @@ abstract class _$RootRouter extends RootStackRouter {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
DevicesRoute.name: (routeData) {
|
ExtendingVolumeRoute.name: (routeData) {
|
||||||
|
final args = routeData.argsAs<ExtendingVolumeRouteArgs>();
|
||||||
return AutoRoutePage<dynamic>(
|
return AutoRoutePage<dynamic>(
|
||||||
routeData: routeData,
|
routeData: routeData,
|
||||||
child: const DevicesScreen(),
|
child: ExtendingVolumePage(
|
||||||
);
|
diskVolumeToResize: args.diskVolumeToResize,
|
||||||
},
|
diskStatus: args.diskStatus,
|
||||||
RootRoute.name: (routeData) {
|
key: args.key,
|
||||||
return AutoRoutePage<dynamic>(
|
),
|
||||||
routeData: routeData,
|
|
||||||
child: WrappedRoute(child: const RootPage()),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
AboutApplicationRoute.name: (routeData) {
|
|
||||||
return AutoRoutePage<dynamic>(
|
|
||||||
routeData: routeData,
|
|
||||||
child: const AboutApplicationPage(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
ConsoleRoute.name: (routeData) {
|
|
||||||
return AutoRoutePage<dynamic>(
|
|
||||||
routeData: routeData,
|
|
||||||
child: const ConsolePage(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
MoreRoute.name: (routeData) {
|
|
||||||
return AutoRoutePage<dynamic>(
|
|
||||||
routeData: routeData,
|
|
||||||
child: const MorePage(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
AppSettingsRoute.name: (routeData) {
|
|
||||||
return AutoRoutePage<dynamic>(
|
|
||||||
routeData: routeData,
|
|
||||||
child: const AppSettingsPage(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
DeveloperSettingsRoute.name: (routeData) {
|
|
||||||
return AutoRoutePage<dynamic>(
|
|
||||||
routeData: routeData,
|
|
||||||
child: const DeveloperSettingsPage(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
DnsDetailsRoute.name: (routeData) {
|
|
||||||
return AutoRoutePage<dynamic>(
|
|
||||||
routeData: routeData,
|
|
||||||
child: const DnsDetailsPage(),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
ServiceRoute.name: (routeData) {
|
ServiceRoute.name: (routeData) {
|
||||||
|
@ -152,6 +152,18 @@ abstract class _$RootRouter extends RootStackRouter {
|
||||||
child: const ServicesPage(),
|
child: const ServicesPage(),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
InitializingRoute.name: (routeData) {
|
||||||
|
return AutoRoutePage<dynamic>(
|
||||||
|
routeData: routeData,
|
||||||
|
child: const InitializingPage(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
RecoveryRoute.name: (routeData) {
|
||||||
|
return AutoRoutePage<dynamic>(
|
||||||
|
routeData: routeData,
|
||||||
|
child: const RecoveryRouting(),
|
||||||
|
);
|
||||||
|
},
|
||||||
UsersRoute.name: (routeData) {
|
UsersRoute.name: (routeData) {
|
||||||
return AutoRoutePage<dynamic>(
|
return AutoRoutePage<dynamic>(
|
||||||
routeData: routeData,
|
routeData: routeData,
|
||||||
|
@ -174,49 +186,9 @@ abstract class _$RootRouter extends RootStackRouter {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
InitializingRoute.name: (routeData) {
|
|
||||||
return AutoRoutePage<dynamic>(
|
|
||||||
routeData: routeData,
|
|
||||||
child: const InitializingPage(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
RecoveryRoute.name: (routeData) {
|
|
||||||
return AutoRoutePage<dynamic>(
|
|
||||||
routeData: routeData,
|
|
||||||
child: const RecoveryRouting(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// generated route for
|
|
||||||
/// [OnboardingPage]
|
|
||||||
class OnboardingRoute extends PageRouteInfo<void> {
|
|
||||||
const OnboardingRoute({List<PageRouteInfo>? children})
|
|
||||||
: super(
|
|
||||||
OnboardingRoute.name,
|
|
||||||
initialChildren: children,
|
|
||||||
);
|
|
||||||
|
|
||||||
static const String name = 'OnboardingRoute';
|
|
||||||
|
|
||||||
static const PageInfo<void> page = PageInfo<void>(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// generated route for
|
|
||||||
/// [BackupDetailsPage]
|
|
||||||
class BackupDetailsRoute extends PageRouteInfo<void> {
|
|
||||||
const BackupDetailsRoute({List<PageRouteInfo>? children})
|
|
||||||
: super(
|
|
||||||
BackupDetailsRoute.name,
|
|
||||||
initialChildren: children,
|
|
||||||
);
|
|
||||||
|
|
||||||
static const String name = 'BackupDetailsRoute';
|
|
||||||
|
|
||||||
static const PageInfo<void> page = PageInfo<void>(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// generated route for
|
/// generated route for
|
||||||
/// [BackupsListPage]
|
/// [BackupsListPage]
|
||||||
class BackupsListRoute extends PageRouteInfo<BackupsListRouteArgs> {
|
class BackupsListRoute extends PageRouteInfo<BackupsListRouteArgs> {
|
||||||
|
@ -256,15 +228,127 @@ class BackupsListRouteArgs {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// generated route for
|
/// generated route for
|
||||||
/// [RecoveryKeyPage]
|
/// [BackupDetailsPage]
|
||||||
class RecoveryKeyRoute extends PageRouteInfo<void> {
|
class BackupDetailsRoute extends PageRouteInfo<void> {
|
||||||
const RecoveryKeyRoute({List<PageRouteInfo>? children})
|
const BackupDetailsRoute({List<PageRouteInfo>? children})
|
||||||
: super(
|
: super(
|
||||||
RecoveryKeyRoute.name,
|
BackupDetailsRoute.name,
|
||||||
initialChildren: children,
|
initialChildren: children,
|
||||||
);
|
);
|
||||||
|
|
||||||
static const String name = 'RecoveryKeyRoute';
|
static const String name = 'BackupDetailsRoute';
|
||||||
|
|
||||||
|
static const PageInfo<void> page = PageInfo<void>(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// generated route for
|
||||||
|
/// [DevicesScreen]
|
||||||
|
class DevicesRoute extends PageRouteInfo<void> {
|
||||||
|
const DevicesRoute({List<PageRouteInfo>? children})
|
||||||
|
: super(
|
||||||
|
DevicesRoute.name,
|
||||||
|
initialChildren: children,
|
||||||
|
);
|
||||||
|
|
||||||
|
static const String name = 'DevicesRoute';
|
||||||
|
|
||||||
|
static const PageInfo<void> page = PageInfo<void>(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// generated route for
|
||||||
|
/// [DnsDetailsPage]
|
||||||
|
class DnsDetailsRoute extends PageRouteInfo<void> {
|
||||||
|
const DnsDetailsRoute({List<PageRouteInfo>? children})
|
||||||
|
: super(
|
||||||
|
DnsDetailsRoute.name,
|
||||||
|
initialChildren: children,
|
||||||
|
);
|
||||||
|
|
||||||
|
static const String name = 'DnsDetailsRoute';
|
||||||
|
|
||||||
|
static const PageInfo<void> page = PageInfo<void>(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// generated route for
|
||||||
|
/// [AboutApplicationPage]
|
||||||
|
class AboutApplicationRoute extends PageRouteInfo<void> {
|
||||||
|
const AboutApplicationRoute({List<PageRouteInfo>? children})
|
||||||
|
: super(
|
||||||
|
AboutApplicationRoute.name,
|
||||||
|
initialChildren: children,
|
||||||
|
);
|
||||||
|
|
||||||
|
static const String name = 'AboutApplicationRoute';
|
||||||
|
|
||||||
|
static const PageInfo<void> page = PageInfo<void>(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// generated route for
|
||||||
|
/// [AppSettingsPage]
|
||||||
|
class AppSettingsRoute extends PageRouteInfo<void> {
|
||||||
|
const AppSettingsRoute({List<PageRouteInfo>? children})
|
||||||
|
: super(
|
||||||
|
AppSettingsRoute.name,
|
||||||
|
initialChildren: children,
|
||||||
|
);
|
||||||
|
|
||||||
|
static const String name = 'AppSettingsRoute';
|
||||||
|
|
||||||
|
static const PageInfo<void> page = PageInfo<void>(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// generated route for
|
||||||
|
/// [DeveloperSettingsPage]
|
||||||
|
class DeveloperSettingsRoute extends PageRouteInfo<void> {
|
||||||
|
const DeveloperSettingsRoute({List<PageRouteInfo>? children})
|
||||||
|
: super(
|
||||||
|
DeveloperSettingsRoute.name,
|
||||||
|
initialChildren: children,
|
||||||
|
);
|
||||||
|
|
||||||
|
static const String name = 'DeveloperSettingsRoute';
|
||||||
|
|
||||||
|
static const PageInfo<void> page = PageInfo<void>(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// generated route for
|
||||||
|
/// [ConsolePage]
|
||||||
|
class ConsoleRoute extends PageRouteInfo<void> {
|
||||||
|
const ConsoleRoute({List<PageRouteInfo>? children})
|
||||||
|
: super(
|
||||||
|
ConsoleRoute.name,
|
||||||
|
initialChildren: children,
|
||||||
|
);
|
||||||
|
|
||||||
|
static const String name = 'ConsoleRoute';
|
||||||
|
|
||||||
|
static const PageInfo<void> page = PageInfo<void>(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// generated route for
|
||||||
|
/// [MorePage]
|
||||||
|
class MoreRoute extends PageRouteInfo<void> {
|
||||||
|
const MoreRoute({List<PageRouteInfo>? children})
|
||||||
|
: super(
|
||||||
|
MoreRoute.name,
|
||||||
|
initialChildren: children,
|
||||||
|
);
|
||||||
|
|
||||||
|
static const String name = 'MoreRoute';
|
||||||
|
|
||||||
|
static const PageInfo<void> page = PageInfo<void>(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// generated route for
|
||||||
|
/// [OnboardingPage]
|
||||||
|
class OnboardingRoute extends PageRouteInfo<void> {
|
||||||
|
const OnboardingRoute({List<PageRouteInfo>? children})
|
||||||
|
: super(
|
||||||
|
OnboardingRoute.name,
|
||||||
|
initialChildren: children,
|
||||||
|
);
|
||||||
|
|
||||||
|
static const String name = 'OnboardingRoute';
|
||||||
|
|
||||||
static const PageInfo<void> page = PageInfo<void>(name);
|
static const PageInfo<void> page = PageInfo<void>(name);
|
||||||
}
|
}
|
||||||
|
@ -283,6 +367,34 @@ class ProvidersRoute extends PageRouteInfo<void> {
|
||||||
static const PageInfo<void> page = PageInfo<void>(name);
|
static const PageInfo<void> page = PageInfo<void>(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// generated route for
|
||||||
|
/// [RecoveryKeyPage]
|
||||||
|
class RecoveryKeyRoute extends PageRouteInfo<void> {
|
||||||
|
const RecoveryKeyRoute({List<PageRouteInfo>? children})
|
||||||
|
: super(
|
||||||
|
RecoveryKeyRoute.name,
|
||||||
|
initialChildren: children,
|
||||||
|
);
|
||||||
|
|
||||||
|
static const String name = 'RecoveryKeyRoute';
|
||||||
|
|
||||||
|
static const PageInfo<void> page = PageInfo<void>(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// generated route for
|
||||||
|
/// [RootPage]
|
||||||
|
class RootRoute extends PageRouteInfo<void> {
|
||||||
|
const RootRoute({List<PageRouteInfo>? children})
|
||||||
|
: super(
|
||||||
|
RootRoute.name,
|
||||||
|
initialChildren: children,
|
||||||
|
);
|
||||||
|
|
||||||
|
static const String name = 'RootRoute';
|
||||||
|
|
||||||
|
static const PageInfo<void> page = PageInfo<void>(name);
|
||||||
|
}
|
||||||
|
|
||||||
/// generated route for
|
/// generated route for
|
||||||
/// [ServerDetailsScreen]
|
/// [ServerDetailsScreen]
|
||||||
class ServerDetailsRoute extends PageRouteInfo<void> {
|
class ServerDetailsRoute extends PageRouteInfo<void> {
|
||||||
|
@ -345,6 +457,44 @@ class ServicesMigrationRouteArgs {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// generated route for
|
||||||
|
/// [ServerStoragePage]
|
||||||
|
class ServerStorageRoute extends PageRouteInfo<ServerStorageRouteArgs> {
|
||||||
|
ServerStorageRoute({
|
||||||
|
required DiskStatus diskStatus,
|
||||||
|
Key? key,
|
||||||
|
List<PageRouteInfo>? children,
|
||||||
|
}) : super(
|
||||||
|
ServerStorageRoute.name,
|
||||||
|
args: ServerStorageRouteArgs(
|
||||||
|
diskStatus: diskStatus,
|
||||||
|
key: key,
|
||||||
|
),
|
||||||
|
initialChildren: children,
|
||||||
|
);
|
||||||
|
|
||||||
|
static const String name = 'ServerStorageRoute';
|
||||||
|
|
||||||
|
static const PageInfo<ServerStorageRouteArgs> page =
|
||||||
|
PageInfo<ServerStorageRouteArgs>(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
class ServerStorageRouteArgs {
|
||||||
|
const ServerStorageRouteArgs({
|
||||||
|
required this.diskStatus,
|
||||||
|
this.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final DiskStatus diskStatus;
|
||||||
|
|
||||||
|
final Key? key;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'ServerStorageRouteArgs{diskStatus: $diskStatus, key: $key}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// generated route for
|
/// generated route for
|
||||||
/// [ExtendingVolumePage]
|
/// [ExtendingVolumePage]
|
||||||
class ExtendingVolumeRoute extends PageRouteInfo<ExtendingVolumeRouteArgs> {
|
class ExtendingVolumeRoute extends PageRouteInfo<ExtendingVolumeRouteArgs> {
|
||||||
|
@ -388,156 +538,6 @@ class ExtendingVolumeRouteArgs {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// generated route for
|
|
||||||
/// [ServerStoragePage]
|
|
||||||
class ServerStorageRoute extends PageRouteInfo<ServerStorageRouteArgs> {
|
|
||||||
ServerStorageRoute({
|
|
||||||
required DiskStatus diskStatus,
|
|
||||||
Key? key,
|
|
||||||
List<PageRouteInfo>? children,
|
|
||||||
}) : super(
|
|
||||||
ServerStorageRoute.name,
|
|
||||||
args: ServerStorageRouteArgs(
|
|
||||||
diskStatus: diskStatus,
|
|
||||||
key: key,
|
|
||||||
),
|
|
||||||
initialChildren: children,
|
|
||||||
);
|
|
||||||
|
|
||||||
static const String name = 'ServerStorageRoute';
|
|
||||||
|
|
||||||
static const PageInfo<ServerStorageRouteArgs> page =
|
|
||||||
PageInfo<ServerStorageRouteArgs>(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
class ServerStorageRouteArgs {
|
|
||||||
const ServerStorageRouteArgs({
|
|
||||||
required this.diskStatus,
|
|
||||||
this.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
final DiskStatus diskStatus;
|
|
||||||
|
|
||||||
final Key? key;
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() {
|
|
||||||
return 'ServerStorageRouteArgs{diskStatus: $diskStatus, key: $key}';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// generated route for
|
|
||||||
/// [DevicesScreen]
|
|
||||||
class DevicesRoute extends PageRouteInfo<void> {
|
|
||||||
const DevicesRoute({List<PageRouteInfo>? children})
|
|
||||||
: super(
|
|
||||||
DevicesRoute.name,
|
|
||||||
initialChildren: children,
|
|
||||||
);
|
|
||||||
|
|
||||||
static const String name = 'DevicesRoute';
|
|
||||||
|
|
||||||
static const PageInfo<void> page = PageInfo<void>(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// generated route for
|
|
||||||
/// [RootPage]
|
|
||||||
class RootRoute extends PageRouteInfo<void> {
|
|
||||||
const RootRoute({List<PageRouteInfo>? children})
|
|
||||||
: super(
|
|
||||||
RootRoute.name,
|
|
||||||
initialChildren: children,
|
|
||||||
);
|
|
||||||
|
|
||||||
static const String name = 'RootRoute';
|
|
||||||
|
|
||||||
static const PageInfo<void> page = PageInfo<void>(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// generated route for
|
|
||||||
/// [AboutApplicationPage]
|
|
||||||
class AboutApplicationRoute extends PageRouteInfo<void> {
|
|
||||||
const AboutApplicationRoute({List<PageRouteInfo>? children})
|
|
||||||
: super(
|
|
||||||
AboutApplicationRoute.name,
|
|
||||||
initialChildren: children,
|
|
||||||
);
|
|
||||||
|
|
||||||
static const String name = 'AboutApplicationRoute';
|
|
||||||
|
|
||||||
static const PageInfo<void> page = PageInfo<void>(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// generated route for
|
|
||||||
/// [ConsolePage]
|
|
||||||
class ConsoleRoute extends PageRouteInfo<void> {
|
|
||||||
const ConsoleRoute({List<PageRouteInfo>? children})
|
|
||||||
: super(
|
|
||||||
ConsoleRoute.name,
|
|
||||||
initialChildren: children,
|
|
||||||
);
|
|
||||||
|
|
||||||
static const String name = 'ConsoleRoute';
|
|
||||||
|
|
||||||
static const PageInfo<void> page = PageInfo<void>(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// generated route for
|
|
||||||
/// [MorePage]
|
|
||||||
class MoreRoute extends PageRouteInfo<void> {
|
|
||||||
const MoreRoute({List<PageRouteInfo>? children})
|
|
||||||
: super(
|
|
||||||
MoreRoute.name,
|
|
||||||
initialChildren: children,
|
|
||||||
);
|
|
||||||
|
|
||||||
static const String name = 'MoreRoute';
|
|
||||||
|
|
||||||
static const PageInfo<void> page = PageInfo<void>(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// generated route for
|
|
||||||
/// [AppSettingsPage]
|
|
||||||
class AppSettingsRoute extends PageRouteInfo<void> {
|
|
||||||
const AppSettingsRoute({List<PageRouteInfo>? children})
|
|
||||||
: super(
|
|
||||||
AppSettingsRoute.name,
|
|
||||||
initialChildren: children,
|
|
||||||
);
|
|
||||||
|
|
||||||
static const String name = 'AppSettingsRoute';
|
|
||||||
|
|
||||||
static const PageInfo<void> page = PageInfo<void>(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// generated route for
|
|
||||||
/// [DeveloperSettingsPage]
|
|
||||||
class DeveloperSettingsRoute extends PageRouteInfo<void> {
|
|
||||||
const DeveloperSettingsRoute({List<PageRouteInfo>? children})
|
|
||||||
: super(
|
|
||||||
DeveloperSettingsRoute.name,
|
|
||||||
initialChildren: children,
|
|
||||||
);
|
|
||||||
|
|
||||||
static const String name = 'DeveloperSettingsRoute';
|
|
||||||
|
|
||||||
static const PageInfo<void> page = PageInfo<void>(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// generated route for
|
|
||||||
/// [DnsDetailsPage]
|
|
||||||
class DnsDetailsRoute extends PageRouteInfo<void> {
|
|
||||||
const DnsDetailsRoute({List<PageRouteInfo>? children})
|
|
||||||
: super(
|
|
||||||
DnsDetailsRoute.name,
|
|
||||||
initialChildren: children,
|
|
||||||
);
|
|
||||||
|
|
||||||
static const String name = 'DnsDetailsRoute';
|
|
||||||
|
|
||||||
static const PageInfo<void> page = PageInfo<void>(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// generated route for
|
/// generated route for
|
||||||
/// [ServicePage]
|
/// [ServicePage]
|
||||||
class ServiceRoute extends PageRouteInfo<ServiceRouteArgs> {
|
class ServiceRoute extends PageRouteInfo<ServiceRouteArgs> {
|
||||||
|
@ -590,6 +590,34 @@ class ServicesRoute extends PageRouteInfo<void> {
|
||||||
static const PageInfo<void> page = PageInfo<void>(name);
|
static const PageInfo<void> page = PageInfo<void>(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// generated route for
|
||||||
|
/// [InitializingPage]
|
||||||
|
class InitializingRoute extends PageRouteInfo<void> {
|
||||||
|
const InitializingRoute({List<PageRouteInfo>? children})
|
||||||
|
: super(
|
||||||
|
InitializingRoute.name,
|
||||||
|
initialChildren: children,
|
||||||
|
);
|
||||||
|
|
||||||
|
static const String name = 'InitializingRoute';
|
||||||
|
|
||||||
|
static const PageInfo<void> page = PageInfo<void>(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// generated route for
|
||||||
|
/// [RecoveryRouting]
|
||||||
|
class RecoveryRoute extends PageRouteInfo<void> {
|
||||||
|
const RecoveryRoute({List<PageRouteInfo>? children})
|
||||||
|
: super(
|
||||||
|
RecoveryRoute.name,
|
||||||
|
initialChildren: children,
|
||||||
|
);
|
||||||
|
|
||||||
|
static const String name = 'RecoveryRoute';
|
||||||
|
|
||||||
|
static const PageInfo<void> page = PageInfo<void>(name);
|
||||||
|
}
|
||||||
|
|
||||||
/// generated route for
|
/// generated route for
|
||||||
/// [UsersPage]
|
/// [UsersPage]
|
||||||
class UsersRoute extends PageRouteInfo<void> {
|
class UsersRoute extends PageRouteInfo<void> {
|
||||||
|
@ -655,31 +683,3 @@ class UserDetailsRouteArgs {
|
||||||
return 'UserDetailsRouteArgs{login: $login, key: $key}';
|
return 'UserDetailsRouteArgs{login: $login, key: $key}';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// generated route for
|
|
||||||
/// [InitializingPage]
|
|
||||||
class InitializingRoute extends PageRouteInfo<void> {
|
|
||||||
const InitializingRoute({List<PageRouteInfo>? children})
|
|
||||||
: super(
|
|
||||||
InitializingRoute.name,
|
|
||||||
initialChildren: children,
|
|
||||||
);
|
|
||||||
|
|
||||||
static const String name = 'InitializingRoute';
|
|
||||||
|
|
||||||
static const PageInfo<void> page = PageInfo<void>(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// generated route for
|
|
||||||
/// [RecoveryRouting]
|
|
||||||
class RecoveryRoute extends PageRouteInfo<void> {
|
|
||||||
const RecoveryRoute({List<PageRouteInfo>? children})
|
|
||||||
: super(
|
|
||||||
RecoveryRoute.name,
|
|
||||||
initialChildren: children,
|
|
||||||
);
|
|
||||||
|
|
||||||
static const String name = 'RecoveryRoute';
|
|
||||||
|
|
||||||
static const PageInfo<void> page = PageInfo<void>(name);
|
|
||||||
}
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ import 'dart:io';
|
||||||
|
|
||||||
import 'package:device_info_plus/device_info_plus.dart';
|
import 'package:device_info_plus/device_info_plus.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
/// SelfPrivacy wrapper for Platform information provider.
|
/// SelfPrivacy wrapper for Platform information provider.
|
||||||
class PlatformAdapter {
|
class PlatformAdapter {
|
||||||
|
@ -56,4 +57,8 @@ class PlatformAdapter {
|
||||||
|
|
||||||
return 'Unidentified';
|
return 'Unidentified';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void setClipboard(final String clipboardData) {
|
||||||
|
Clipboard.setData(ClipboardData(text: clipboardData));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
Nightly builds of SelfPrivacy app.
|
|
Before Width: | Height: | Size: 13 KiB |
|
@ -1 +0,0 @@
|
||||||
Self-hosted services without pain
|
|
|
@ -1 +0,0 @@
|
||||||
SelfPrivacy (Nightly)
|
|
|
@ -1,23 +0,0 @@
|
||||||
<p>SelfPrivacy — is a platform on your cloud hosting, that allows to deploy your own private services and control them using mobile application.</p>
|
|
||||||
|
|
||||||
<p>To use this application, you'll be required to create accounts of different service providers. Please reffer to this manual: https://selfprivacy.org/en/second</p>
|
|
||||||
|
|
||||||
<p>Application will do the following things for you:</p>
|
|
||||||
|
|
||||||
<ol>
|
|
||||||
<li>Create your personal server</li>
|
|
||||||
<li>Setup NixOS</li>
|
|
||||||
<li>Bring all services to the ready-to-use state. Services include:</li>
|
|
||||||
</ol>
|
|
||||||
|
|
||||||
<ul>
|
|
||||||
<li>E-mail, ready to use with DeltaChat</li>
|
|
||||||
<li>NextCloud — your personal cloud storage</li>
|
|
||||||
<li>Bitwarden — secure and private password manager</li>
|
|
||||||
<li>Pleroma — your private fediverse space for blogging</li>
|
|
||||||
<li>Jitsi — awesome Zoom alternative</li>
|
|
||||||
<li>Gitea — your own Git server</li>
|
|
||||||
<li>OpenConnect — Personal VPN server</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<p><b>Project is currently in open beta state</b>. Feel free to try it. It would be much appreciated if you would provide us with some feedback.</p>
|
|
Before Width: | Height: | Size: 8.1 KiB |
Before Width: | Height: | Size: 60 KiB |
Before Width: | Height: | Size: 78 KiB |
Before Width: | Height: | Size: 52 KiB |
Before Width: | Height: | Size: 43 KiB |
|
@ -1 +0,0 @@
|
||||||
Self-hosted services without pain
|
|
|
@ -1 +0,0 @@
|
||||||
SelfPrivacy
|
|
|
@ -1,7 +1,7 @@
|
||||||
name: selfprivacy
|
name: selfprivacy
|
||||||
description: selfprivacy.org
|
description: selfprivacy.org
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
version: 0.8.0+17
|
version: 0.9.1+19
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.0.2 <4.0.0'
|
sdk: '>=3.0.2 <4.0.0'
|
||||||
|
|