chore: Merge master into digital-ocean-dns

pull/213/head
NaiJi ✨ 2023-01-03 13:00:01 +04:00
commit 6767b679a0
41 changed files with 4158 additions and 717 deletions

View File

@ -97,16 +97,16 @@ steps:
GOOGLE_KEYSTORE_PASS:
from_secret: GOOGLE_KEYSTORE_PASS
# - name: Build Intermediate Android Release Artifact (Bundle)
# commands:
# - ./ci.py --build-bundle
# environment:
# STANDALONE_KEYSTORE_PASS:
# from_secret: STANDALONE_KEYSTORE_PASS
# FDROID_KEYSTORE_PASS:
# from_secret: FDROID_KEYSTORE_PASS
# GOOGLE_KEYSTORE_PASS:
# from_secret: GOOGLE_KEYSTORE_PASS
- name: Build Intermediate Android Release Artifact (Bundle)
commands:
- ./ci.py --build-bundle
environment:
STANDALONE_KEYSTORE_PASS:
from_secret: STANDALONE_KEYSTORE_PASS
FDROID_KEYSTORE_PASS:
from_secret: FDROID_KEYSTORE_PASS
GOOGLE_KEYSTORE_PASS:
from_secret: GOOGLE_KEYSTORE_PASS
- name: Sign Android Release Artifact (.APK) for Standalone Use
commands:
@ -132,16 +132,16 @@ steps:
GOOGLE_KEYSTORE_PASS:
from_secret: GOOGLE_KEYSTORE_PASS
# - name: Sign Android Release Artifact (Bundle) for Google Play
# commands:
# - ./ci.py --sign-bundle
# environment:
# STANDALONE_KEYSTORE_PASS:
# from_secret: STANDALONE_KEYSTORE_PASS
# FDROID_KEYSTORE_PASS:
# from_secret: FDROID_KEYSTORE_PASS
# GOOGLE_KEYSTORE_PASS:
# from_secret: GOOGLE_KEYSTORE_PASS
- name: Sign Android Release Artifact (Bundle) for Google Play
commands:
- ./ci.py --sign-bundle
environment:
STANDALONE_KEYSTORE_PASS:
from_secret: STANDALONE_KEYSTORE_PASS
FDROID_KEYSTORE_PASS:
from_secret: FDROID_KEYSTORE_PASS
GOOGLE_KEYSTORE_PASS:
from_secret: GOOGLE_KEYSTORE_PASS
- name: Package Linux AppImage Artifact
commands:

View File

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

View File

@ -0,0 +1,6 @@
<svg width="24" height="25" viewBox="0 0 24 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.0013 24.0128V19.3592C16.927 19.3592 20.7505 14.4743 18.8591 9.2901C18.1652 7.37152 16.6276 5.83395 14.709 5.14C9.52482 3.26224 4.63995 7.07217 4.63995 11.9979H0C0 4.14669 7.59264 -1.97641 15.8248 0.595295C19.417 1.72467 22.2881 4.58211 23.4038 8.17433C25.9755 16.4201 19.8661 24.0128 12.0013 24.0128Z" fill="white"/>
<path d="M12.0149 19.3729H7.38855V14.7466H12.0149V19.3729Z" fill="white"/>
<path d="M7.38861 22.9376H3.82361V19.3726H7.38861V22.9376Z" fill="white"/>
<path d="M3.82354 19.373H0.843628V16.3931H3.82354V19.373Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 657 B

View File

@ -0,0 +1,10 @@
<svg width="24" height="23" viewBox="0 0 24 23" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_51804_9018)">
<path d="M22.5704 0H19.3252C18.5948 0 18.2817 0.302609 18.2817 1.04348V9.25565H5.35304V1.04348C5.35304 0.313043 5.05044 0 4.30957 0H1.04348C0.302609 0 0 0.302609 0 1.04348V22.1739C0 22.9148 0.302609 23.2174 1.04348 23.2174H4.30957C5.04 23.2174 5.35304 22.9252 5.35304 22.1739V13.8261H18.2922V22.1739C18.2922 22.9043 18.5948 23.2174 19.3357 23.2174H22.5809C23.3113 23.2174 23.6243 22.9148 23.6243 22.1739V1.04348C23.6035 0.333913 23.3009 0 22.5704 0Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0_51804_9018">
<rect width="24" height="22.9565" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 722 B

View File

@ -0,0 +1,12 @@
### How to get Digital Ocean API Token
1. Visit the following [link](https://cloud.digitalocean.com/) and sign
into newly created account.
2. Enter into previously created project. If you haven't created one,
then please proceed.
3. Go to the "API" link on the left bar.
4. Click on the "Generate New Token".
5. Enter any name for the token.
6. Put expiration time to "No expiry".
7. Check the "Write (optional)" checkbox.
8. Now click on the "Generate Token" button.
9. After that, the token will be shown. Store it in any reliable place, preferably a password manager.

View File

@ -0,0 +1,10 @@
### How to get Digital Ocean API Token
1. Перейдите по [ссылке](https://cloud.digitalocean.com/) и войдите в ваш аккаунт.
2. Перейдите в новый проект, либо создайте проект, если ещё этого не сделали.
3. Перейдите в "API" раздел в меню слева.
4. Нажмите на "Generate New Token".
5. Введите какое-нибудь имя для токена.
6. Установите время истощения на "No expiry".
7. Проставьте галочку в пункте "Write (optional)".
8. Теперь нажмите на "Generate Token" кнопку внизу.
9. После этого появится ваш токен. Скопируйте его в надёжное место, лучше в ваш собственный менеджер паролей.

View File

@ -4,6 +4,7 @@
"basis": {
"providers": "Providers",
"providers_title": "Your Data Center",
"select": "Select",
"services": "Services",
"services_title": "Your personal, private and independent services.",
"users": "Users",
@ -53,7 +54,8 @@
"about_application_page": {
"title": "About",
"application_version_text": "Application version v.{}",
"api_version_text": "Server API version v.{}"
"api_version_text": "Server API version v.{}",
"privacy_policy": "Privacy policy"
},
"application_settings": {
"title": "Application settings",
@ -79,8 +81,14 @@
"onboarding": {
"page1_title": "Digital independence, available to all of us",
"page1_text": "Mail, VPN, Messenger, social network and much more on your private server, under your control.",
"page2_title": "SelfPrivacy — it's not a cloud, but your personal datacenter",
"page2_text": "SelfPrivacy works only with your provider accounts: Hetzner, Cloudflare, Backblaze. If you do not own those, we'll help you to create them."
"page2_title": "SelfPrivacy is not a cloud, it's Your personal datacenter",
"page2_text": "SelfPrivacy only works with providers that you choose. If you do not have required accounts in those, we'll help you to create them.",
"page2_server_provider_title": "Server provider",
"page2_server_provider_text": "A server provider maintains your server in its own data center. SelfPrivacy will automatically connect to the provider and setup all necessary things.",
"page2_dns_provider_title": "DNS provider",
"page2_dns_provider_text": "You need a domain to have a place in the Internet. And you also need a reliable DNS provider to have the domain pointed to your server. We will suggest you pick a supported DNS provider to automatically setup networking.",
"page2_backup_provider_title": "Backup provider",
"page2_backup_provider_text": "What if something happens to your server? Imagine a hacker attack, an accidental data deletion or denial of service? Your data will be kept safe in your provider of backups. They will be securely encrypted and anytime accessible to restore your server with."
},
"resource_chart": {
"month": "Month",
@ -268,21 +276,47 @@
"no_ssh_notice": "Only email and SSH accounts are created for this user. Single Sign On for all services is coming soon."
},
"initializing": {
"connect_to_server": "Connect the server provider",
"select_provider": "Select your provider",
"server_provider_description": "A place where your data and SelfPrivacy services will reside:",
"dns_provider_description": "A service which lets your IP point towards domain names:",
"connect_to_server": "Let's start with a server.",
"select_provider": "Pick any provider from the following list, they all support SelfPrivacy",
"select_provider_notice": "By 'Relatively small' we mean a machine with 2 cores of CPU and 2 gigabytes of RAM.",
"select_provider_countries_title": "Available countries",
"select_provider_countries_text_hetzner": "Germany, Finland, USA",
"select_provider_countries_text_do": "USA, Netherlands, Singapore, UK, Germany, Canada, India, Australia",
"select_provider_price_title": "Average price",
"select_provider_price_text_hetzner": "€8 per month for a relatively small server and 50GB of disk storage",
"select_provider_price_text_do": "$17 per month for a relatively small server and 50GB of disk storage",
"select_provider_payment_title": "Payment methods",
"select_provider_payment_text_hetzner": "Credit cards, SWIFT, SEPA, PayPal",
"select_provider_payment_text_do": "Credit cards, Google Pay, PayPal",
"select_provider_email_notice": "E-mail hosting won't be available for new clients. Nevertheless it will be unlocked as soon as you complete your first payment.",
"select_provider_site_button": "Visit site",
"connect_to_server_provider": "Now log in ",
"connect_to_server_provider_text": "With API token SelfPrivacy will be able to rent a machine and setup your server on it",
"how": "How to obtain API token",
"provider_bad_key_error": "Provider API key is invalid",
"could_not_connect": "Counldn't connect to the provider.",
"choose_location_type": "Choose your server location and type:",
"back_to_locations": "Go back to available locations!",
"no_locations_found": "No available locations found. Make sure your account is accessible.",
"choose_location_type": "Where do you want to order your server?",
"choose_location_type_text": "Different locations provide different server configurations, prices and connection speed.",
"locations_not_found": "Oops!",
"locations_not_found_text": "There are no available servers to rent",
"back_to_locations": "Select something else",
"no_locations_found": "No available locations found, make sure your account is accessible",
"choose_server_type": "What type of server do you need?",
"choose_server_type_text": "Different resource capabilities support different services. Don't worry, you can expand your server anytime",
"choose_server_type_notice": "The important things to look at are the CPU and RAM. The data of your services will be stored on a mounted volume which is easily explandable and gets paid for separately.",
"choose_server_type_ram": "{} GB of RAM",
"choose_server_type_storage": "{} GB of system storage",
"choose_server_type_payment_per_month": "{} per month",
"no_server_types_found": "No available server types found. Make sure your account is accessible and try to change your server location.",
"cloudflare_bad_key_error": "Cloudflare API key is invalid",
"backblaze_bad_key_error": "Backblaze storage information is invalid",
"connect_to_dns": "Connect the DNS provider",
"select_dns": "Now let's select a DNS provider",
"manage_domain_dns": "To manage your domain's DNS",
"use_this_domain": "Use this domain?",
"use_this_domain_text": "The token you provided gives access to the following domain",
"cloudflare_api_token": "CloudFlare API Token",
"connect_backblaze_storage": "Connect Backblaze storage",
"no_connected_domains": "No connected domains at the moment",
@ -393,6 +427,8 @@
"generation_error": "Couldn't generate a recovery key. {}"
},
"modals": {
"dns_removal_error": "Couldn't remove DNS records.",
"server_deletion_error": "Couldn't delete active server.",
"server_validators_error": "Couldn't fetch available servers.",
"already_exists": "Such server already exists.",
"unexpected_error": "Unexpected error during placement from the provider side.",
@ -443,4 +479,4 @@
"length_not_equal": "Length is [], should be {}",
"length_longer": "Length is [], should be shorter than or equal to {}"
}
}
}

View File

@ -3,6 +3,7 @@
"locale": "ru",
"basis": {
"providers": "Провайдеры",
"select": "Выбрать",
"providers_title": "Ваш Дата Центр",
"services": "Сервисы",
"services_title": "Ваши личные, приватные и независимые сервисы.",
@ -53,7 +54,8 @@
"about_application_page": {
"title": "О приложении",
"application_version_text": "Версия приложения v.{}",
"api_version_text": "Версия API сервера v.{}"
"api_version_text": "Версия API сервера v.{}",
"privacy_policy": "Политика конфиденциальности"
},
"application_settings": {
"title": "Настройки приложения",
@ -80,7 +82,13 @@
"page1_title": "Цифровая независимость доступна каждому",
"page1_text": "Почта, VPN, Мессенджер, социальная сеть и многое другое на Вашем личном сервере, под Вашим полным контролем.",
"page2_title": "SelfPrivacy — это не облако, а Ваш личный дата-центр",
"page2_text": "SelfPrivacy работает только с вашими сервис-провайдерами: Hetzner, Cloudflare, Backblaze. Если у Вас нет учётных записей, мы поможем их создать."
"page2_text": "SelfPrivacy работает только с сервис-провайдерами на ваш выбор. Если у Вас нет учётных записей, мы поможем их создать.",
"page2_server_provider_title": "Сервер-провайдер",
"page2_server_provider_text": "Сервер-провайдер будет обслуживать ваш сервер в своём дата-центре. SelfPrivacy автоматически подключится к нему и настроит вам сервер.",
"page2_dns_provider_title": "DNS-провайдер",
"page2_dns_provider_text": "Чтобы быть в интернете, нужен домен. Чтобы домен указывал на ваш сервер, нужен надёжный DNS сервер. Мы предложим вам выбрать один из поддерживаемых DNS серверов автоматически настроим все записи. Хотите настроить их вручную? Так тоже можно.",
"page2_backup_provider_title": "Бэкап-провайдер",
"page2_backup_provider_text": "Что если с сервером что-то случится? Хакерская атака, отказ в обслуживании или просто случайное удаление данных? Ваши данные будут в сохранности в другом месте, у провайдера хранилища ваших резервных копий. Все они надёжно шифруются, и вы сможете восстановить свой сервер."
},
"resource_chart": {
"month": "Месяц",
@ -268,20 +276,45 @@
"no_ssh_notice": "Для этого пользователя созданы только SSH и Email аккаунты. Единая авторизация для всех сервисов ещё не реализована."
},
"initializing": {
"connect_to_server": "Подключите сервер",
"server_provider_description": "Здесь будут жить ваши данные и SelfPrivacy-сервисы:",
"dns_provider_description": "Это позволит связать ваш домен с IP адресом:",
"connect_to_server": "Начнём с сервера.",
"select_provider": "Ниже подборка провайдеров, которых поддерживает SelfPrivacy",
"select_provider_notice": "Под 'Небольшим сервером' имеется ввиду сервер с двумя потоками процессора и двумя гигабайтами оперативной памяти.",
"select_provider_countries_title": "Доступные страны",
"select_provider_countries_text_hetzner": "Германия, Финляндия, США",
"select_provider_countries_text_do": "США, Нидерланды, Сингапур, Великобритания, Германия, Канада, Индия, Австралия",
"select_provider_price_title": "Средняя цена",
"select_provider_price_text_hetzner": "€8 в месяц за небольшой сервер и 50GB места на диске",
"select_provider_price_text_do": "$17 в месяц за небольшой сервер и 50GB места на диске",
"select_provider_payment_title": "Методы оплаты",
"select_provider_payment_text_hetzner": "Банковские карты, SWIFT, SEPA, PayPal",
"select_provider_payment_text_do": "Банковские карты, Google Pay, PayPal",
"select_provider_email_notice": "Хостинг электронной почты недоступен для новых клиентов. Разблокировать можно будет после первой оплаты.",
"select_provider_site_button": "Посетить сайт",
"connect_to_server_provider": "Авторизоваться в ",
"connect_to_server_provider_text": "С помощью API токена приложение SelfPrivacy сможет от вашего имени заказать и настроить сервер",
"how": "Как получить API Token",
"provider_bad_key_error": "API ключ провайдера неверен",
"could_not_connect": "Не удалось соединиться с провайдером.",
"choose_location_type": "Выберите локацию и тип вашего сервера:",
"back_to_locations": "Назад к доступным локациям!",
"choose_location_type": "Где заказать сервер?",
"choose_location_type_text": "От выбора локации будут зависеть доступные конфигурации, цены и скорость вашего соединения с сервером.",
"locations_not_found": "Упс!",
"locations_not_found_text": "В этом месте не оказалось доступных серверов для аренды",
"back_to_locations": "Выберем другой",
"no_locations_found": "Не найдено локаций. Убедитесь, что ваш аккаунт доступен.",
"no_server_types_found": "Не удалось получить список серверов. Убедитесь, что ваш аккаунт доступен и попытайтесь сменить локацию сервера.",
"choose_server_type": "Какой выбрать тип сервера?",
"choose_server_type_text": "От ресурсов сервера зависит, какие сервисы смогут запуститься. Расширить сервер можно будет в любое время",
"choose_server_type_notice": "Главное, на что стоит обратить внимание — количество потоков процессора и объём оперативной памяти. Данные сервисов будут размещены на отдельном диске, который оплачивается отдельно и легко расширяем!",
"choose_server_type_ram": "{} GB у RAM",
"choose_server_type_storage": "{} GB системного хранилища",
"choose_server_type_payment_per_month": "{} в месяц",
"no_server_types_found": "Не найдено доступных типов сервера! Пожалуйста, убедитесь, что у вас есть доступ к провайдеру сервера...",
"cloudflare_bad_key_error": "Cloudflare API ключ неверен",
"backblaze_bad_key_error": "Информация о Backblaze хранилище неверна",
"connect_to_dns": "Подключите DNS провайдер",
"manage_domain_dns": "Для управления DNS вашего домена",
"use_this_domain": "Используем этот домен?",
"use_this_domain_text": "Указанный вами токен даёт контроль над этим доменом",
"cloudflare_api_token": "CloudFlare API ключ",
"connect_backblaze_storage": "Подключите облачное хранилище Backblaze",
"no_connected_domains": "На данный момент подлюченных доменов нет",
@ -393,6 +426,8 @@
"generation_error": "Не удалось сгенерировать ключ. {}"
},
"modals": {
"dns_removal_error": "Невозможно удалить DNS записи.",
"server_deletion_error": "Невозможно удалить сервер.",
"server_validators_error": "Не удалось получить список серверов.",
"already_exists": "Такой сервер уже существует.",
"unexpected_error": "Непредвиденная ошибка со стороны провайдера.",
@ -443,4 +478,4 @@
"length_not_equal": "Длина строки [], должно быть равно {}",
"length_longer": "Длина строки [], должно быть меньше либо равно {}"
}
}
}

1
ci.py
View File

@ -149,6 +149,7 @@ def package_linux_archive():
def deploy_gitea_release():
gitea_upload_attachment(f"{HOST_MOUNTED_VOLUME}/standalone_{APP_NAME}-{APP_SEMVER}.apk")
gitea_upload_attachment(f"{HOST_MOUNTED_VOLUME}/standalone_{APP_NAME}-{APP_SEMVER}.apk.idsig")
gitea_upload_attachment(f"{HOST_MOUNTED_VOLUME}/{APP_NAME}-{APP_SEMVER}.aab")
gitea_upload_attachment(f"{HOST_MOUNTED_VOLUME}/SelfPrivacy-{APP_SEMVER}-x86_64.AppImage")
gitea_upload_attachment(f"{HOST_MOUNTED_VOLUME}/SelfPrivacy-{APP_SEMVER}-x86_64.AppImage.zsync")
gitea_upload_attachment(f"{HOST_MOUNTED_VOLUME}/{APP_NAME}-{APP_SEMVER}.flatpak")

View File

@ -0,0 +1,33 @@
Server setup:
- Added support for Digital Ocean as server provider
- You can now choose server region
- You can now choose server tier
- Server installation UI has been refreshed
- Fields now have more specific error messages
Common UI:
- New app bar used in most of the screens
Services:
- Services are now sorted by their status
Server settings:
- Timezone search screen now has a search bar
- Fixed job creation when switching the setting multiple times
- Server destruction now works
Jobs:
- Jobs panel now should take slightly less space
Auth:
- Recovery key page can now be reloaded by dragging down
Logging:
- Log console now has a limit of 500 lines
- GraphQL API requests are now logged in the console
- Networks errors are better handled
For developers:
- App now only uses GraphQL API to communicate with the server. All REST API calls have been removed.
- Server can now be deployed with staging ACME certificates
- Language assets have been reorganized

File diff suppressed because it is too large Load Diff

View File

@ -90,7 +90,8 @@ class BackblazeApi extends ApiMap {
),
);
if (response.statusCode == HttpStatus.ok) {
isTokenValid = response.data['allowed']['capabilities'].contains('listBuckets');
isTokenValid =
response.data['allowed']['capabilities'].contains('listBuckets');
} else if (response.statusCode == HttpStatus.unauthorized) {
isTokenValid = false;
} else {

View File

@ -18,6 +18,7 @@ import 'package:selfprivacy/logic/models/server_metadata.dart';
import 'package:selfprivacy/logic/models/server_provider_location.dart';
import 'package:selfprivacy/logic/models/server_type.dart';
import 'package:selfprivacy/utils/extensions/string_extensions.dart';
import 'package:selfprivacy/utils/network_utils.dart';
import 'package:selfprivacy/utils/password_generator.dart';
class DigitalOceanApi extends ServerProviderApi with VolumeProviderApi {
@ -325,23 +326,6 @@ class DigitalOceanApi extends ServerProviderApi with VolumeProviderApi {
return success;
}
static String getHostnameFromDomain(final String domain) {
// Replace all non-alphanumeric characters with an underscore
String hostname =
domain.split('.')[0].replaceAll(RegExp(r'[^a-zA-Z0-9]'), '-');
if (hostname.endsWith('-')) {
hostname = hostname.substring(0, hostname.length - 1);
}
if (hostname.startsWith('-')) {
hostname = hostname.substring(1);
}
if (hostname.isEmpty) {
hostname = 'selfprivacy-server';
}
return hostname;
}
@override
Future<APIGenericResult<ServerHostingDetails?>> createServer({
required final String dnsApiToken,
@ -431,17 +415,42 @@ class DigitalOceanApi extends ServerProviderApi with VolumeProviderApi {
}
@override
Future<void> deleteServer({
Future<APIGenericResult<bool>> deleteServer({
required final String domainName,
}) async {
final Dio client = await getClient();
final ServerBasicInfo serverToRemove = (await getServers()).firstWhere(
(final el) => el.name == domainName,
);
final ServerVolume volumeToRemove = (await getVolumes()).firstWhere(
(final el) => el.serverId == serverToRemove.id,
);
final String hostname = getHostnameFromDomain(domainName);
final servers = await getServers();
final ServerBasicInfo serverToRemove;
try {
serverToRemove = servers.firstWhere(
(final el) => el.name == hostname,
);
} catch (e) {
print(e);
return APIGenericResult(
data: false,
success: false,
message: e.toString(),
);
}
final volumes = await getVolumes();
final ServerVolume volumeToRemove;
try {
volumeToRemove = volumes.firstWhere(
(final el) => el.serverId == serverToRemove.id,
);
} catch (e) {
print(e);
return APIGenericResult(
data: false,
success: false,
message: e.toString(),
);
}
final List<Future> laterFutures = <Future>[];
await detachVolume(volumeToRemove);
@ -449,13 +458,23 @@ class DigitalOceanApi extends ServerProviderApi with VolumeProviderApi {
try {
laterFutures.add(deleteVolume(volumeToRemove));
laterFutures.add(client.delete('/droplets/$serverToRemove.id'));
laterFutures.add(client.delete('/droplets/${serverToRemove.id}'));
await Future.wait(laterFutures);
} catch (e) {
print(e);
return APIGenericResult(
success: false,
data: false,
message: e.toString(),
);
} finally {
close(client);
}
return APIGenericResult(
success: true,
data: true,
);
}
@override

View File

@ -19,6 +19,7 @@ import 'package:selfprivacy/logic/models/server_metadata.dart';
import 'package:selfprivacy/logic/models/server_provider_location.dart';
import 'package:selfprivacy/logic/models/server_type.dart';
import 'package:selfprivacy/utils/extensions/string_extensions.dart';
import 'package:selfprivacy/utils/network_utils.dart';
import 'package:selfprivacy/utils/password_generator.dart';
class HetznerApi extends ServerProviderApi with VolumeProviderApi {
@ -461,49 +462,47 @@ class HetznerApi extends ServerProviderApi with VolumeProviderApi {
);
}
static String getHostnameFromDomain(final String domain) {
// Replace all non-alphanumeric characters with an underscore
String hostname =
domain.split('.')[0].replaceAll(RegExp(r'[^a-zA-Z0-9]'), '-');
if (hostname.endsWith('-')) {
hostname = hostname.substring(0, hostname.length - 1);
}
if (hostname.startsWith('-')) {
hostname = hostname.substring(1);
}
if (hostname.isEmpty) {
hostname = 'selfprivacy-server';
}
return hostname;
}
@override
Future<void> deleteServer({
Future<APIGenericResult<bool>> deleteServer({
required final String domainName,
}) async {
final Dio client = await getClient();
try {
final String hostname = getHostnameFromDomain(domainName);
final String hostname = getHostnameFromDomain(domainName);
final Response serversReponse = await client.get('/servers');
final List servers = serversReponse.data['servers'];
final Map server =
servers.firstWhere((final el) => el['name'] == hostname);
final List volumes = server['volumes'];
final List<Future> laterFutures = <Future>[];
final Response serversReponse = await client.get('/servers');
final List servers = serversReponse.data['servers'];
final Map server = servers.firstWhere((final el) => el['name'] == hostname);
final List volumes = server['volumes'];
final List<Future> laterFutures = <Future>[];
for (final volumeId in volumes) {
await client.post('/volumes/$volumeId/actions/detach');
}
await Future.delayed(const Duration(seconds: 10));
for (final volumeId in volumes) {
await client.post('/volumes/$volumeId/actions/detach');
for (final volumeId in volumes) {
laterFutures.add(client.delete('/volumes/$volumeId'));
}
laterFutures.add(client.delete('/servers/${server['id']}'));
await Future.wait(laterFutures);
} catch (e) {
print(e);
return APIGenericResult(
success: false,
data: false,
message: e.toString(),
);
} finally {
close(client);
}
await Future.delayed(const Duration(seconds: 10));
for (final volumeId in volumes) {
laterFutures.add(client.delete('/volumes/$volumeId'));
}
laterFutures.add(client.delete('/servers/${server['id']}'));
await Future.wait(laterFutures);
close(client);
return APIGenericResult(
success: true,
data: true,
);
}
@override

View File

@ -31,7 +31,9 @@ abstract class ServerProviderApi extends ApiMap {
Future<ServerHostingDetails> restart();
Future<ServerHostingDetails> powerOn();
Future<void> deleteServer({required final String domainName});
Future<APIGenericResult<bool>> deleteServer({
required final String domainName,
});
Future<APIGenericResult<ServerHostingDetails?>> createServer({
required final String dnsApiToken,
required final User rootUser,

View File

@ -1,7 +1,12 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:hive/hive.dart';
import 'package:material_color_utilities/material_color_utilities.dart'
as color_utils;
import 'package:selfprivacy/config/brand_colors.dart';
import 'package:selfprivacy/config/hive_config.dart';
import 'package:selfprivacy/theming/factory/app_theme_factory.dart';
export 'package:provider/provider.dart';
@ -20,7 +25,7 @@ class AppSettingsCubit extends Cubit<AppSettingsState> {
Box box = Hive.box(BNames.appSettingsBox);
void load() {
void load() async {
final bool? isDarkModeOn = box.get(BNames.isDarkModeOn);
final bool? isOnboardingShowing = box.get(BNames.isOnboardingShowing);
emit(
@ -29,6 +34,14 @@ class AppSettingsCubit extends Cubit<AppSettingsState> {
isOnboardingShowing: isOnboardingShowing,
),
);
WidgetsFlutterBinding.ensureInitialized();
final color_utils.CorePalette? colorPalette =
await AppThemeFactory.getCorePalette();
emit(
state.copyWith(
corePalette: colorPalette,
),
);
}
void updateDarkMode({required final bool isDarkModeOn}) {

View File

@ -4,20 +4,27 @@ class AppSettingsState extends Equatable {
const AppSettingsState({
required this.isDarkModeOn,
required this.isOnboardingShowing,
this.corePalette,
});
final bool isDarkModeOn;
final bool isOnboardingShowing;
final color_utils.CorePalette? corePalette;
AppSettingsState copyWith({
final bool? isDarkModeOn,
final bool? isOnboardingShowing,
final color_utils.CorePalette? corePalette,
}) =>
AppSettingsState(
isDarkModeOn: isDarkModeOn ?? this.isDarkModeOn,
isOnboardingShowing: isOnboardingShowing ?? this.isOnboardingShowing,
corePalette: corePalette ?? this.corePalette,
);
color_utils.CorePalette get corePaletteOrDefault =>
corePalette ?? color_utils.CorePalette.of(BrandColors.primary.value);
@override
List<Object> get props => [isDarkModeOn, isOnboardingShowing];
List<dynamic> get props => [isDarkModeOn, isOnboardingShowing, corePalette];
}

View File

@ -755,7 +755,11 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
closeTimer();
if (state.serverDetails != null) {
await repository.deleteServer(state.serverDomain!);
final bool deletionResult =
await repository.deleteServer(state.serverDomain!);
if (!deletionResult) {
return;
}
}
await repository.deleteServerRelatedRecords();
emit(

View File

@ -763,13 +763,26 @@ class ServerInstallationRepository {
await box.put(BNames.hasFinalChecked, value);
}
Future<void> deleteServer(final ServerDomain serverDomain) async {
await ApiController.currentServerProviderApiFactory!
Future<bool> deleteServer(final ServerDomain serverDomain) async {
final APIGenericResult<bool> deletionResult = await ApiController
.currentServerProviderApiFactory!
.getServerProvider()
.deleteServer(
domainName: serverDomain.domainName,
);
if (!deletionResult.success) {
getIt<NavigationService>()
.showSnackBar('modals.server_validators_error'.tr());
return false;
}
if (!deletionResult.data) {
getIt<NavigationService>()
.showSnackBar('modals.server_deletion_error'.tr());
return false;
}
await box.put(BNames.hasFinalChecked, false);
await box.put(BNames.isServerStarted, false);
await box.put(BNames.isServerResetedFirstTime, false);
@ -777,9 +790,15 @@ class ServerInstallationRepository {
await box.put(BNames.isLoading, false);
await box.put(BNames.serverDetails, null);
await ApiController.currentDnsProviderApiFactory!
final APIGenericResult<void> removalResult = await ApiController
.currentDnsProviderApiFactory!
.getDnsProvider()
.removeSimilarRecords(domain: serverDomain);
if (!removalResult.success) {
getIt<NavigationService>().showSnackBar('modals.dns_removal_error'.tr());
}
return true;
}
Future<void> deleteServerRelatedRecords() async {

View File

@ -9,5 +9,9 @@ class ConsoleModel extends ChangeNotifier {
void addMessage(final Message message) {
messages.add(message);
notifyListeners();
// Make sure we don't have too many messages
if (messages.length > 500) {
messages.removeAt(0);
}
}
}

View File

@ -95,4 +95,15 @@ enum ServerProvider {
return unknown;
}
}
String get displayName {
switch (this) {
case ServerProvider.hetzner:
return 'Hetzner Cloud';
case ServerProvider.digitalOcean:
return 'Digital Ocean';
default:
return 'Unknown';
}
}
}

View File

@ -105,13 +105,13 @@ class ServiceStorageUsage {
}
enum ServiceStatus {
failed,
reloading,
activating,
active,
deactivating,
failed,
inactive,
off,
reloading;
off;
factory ServiceStatus.fromGraphQL(final Enum$ServiceStatusEnum graphQL) {
switch (graphQL) {

View File

@ -1,10 +1,7 @@
import 'dart:io';
import 'package:dynamic_color/dynamic_color.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:system_theme/system_theme.dart';
import 'package:gtk_theme_fl/gtk_theme_fl.dart';
import 'package:material_color_utilities/palettes/core_palette.dart';
abstract class AppThemeFactory {
AppThemeFactory._();
@ -22,40 +19,17 @@ abstract class AppThemeFactory {
required final Color fallbackColor,
final bool isDark = false,
}) async {
ColorScheme? gtkColorsScheme;
final Brightness brightness = isDark ? Brightness.dark : Brightness.light;
final ColorScheme? dynamicColorsScheme =
await _getDynamicColors(brightness);
if (Platform.isLinux) {
final GtkThemeData themeData = await GtkThemeData.initialize();
final bool isGtkDark =
Color(themeData.theme_bg_color).computeLuminance() < 0.5;
final bool isInverseNeeded = isGtkDark != isDark;
gtkColorsScheme = ColorScheme.fromSeed(
seedColor: Color(themeData.theme_selected_bg_color),
brightness: brightness,
background: isInverseNeeded ? null : Color(themeData.theme_bg_color),
surface: isInverseNeeded ? null : Color(themeData.theme_base_color),
);
}
final SystemAccentColor accentColor = SystemAccentColor(fallbackColor);
try {
await accentColor.load();
} on MissingPluginException catch (e) {
print('_createAppTheme: ${e.message}');
}
final ColorScheme fallbackColorScheme = ColorScheme.fromSeed(
seedColor: accentColor.accent,
seedColor: fallbackColor,
brightness: brightness,
);
final ColorScheme colorScheme =
dynamicColorsScheme ?? gtkColorsScheme ?? fallbackColorScheme;
final ColorScheme colorScheme = dynamicColorsScheme ?? fallbackColorScheme;
final Typography appTypography = Typography.material2021();
@ -80,4 +54,12 @@ abstract class AppThemeFactory {
return Future.value(null);
}
}
static Future<CorePalette?> getCorePalette() async {
try {
return await DynamicColorPlugin.getCorePalette();
} on PlatformException {
return Future.value(null);
}
}
}

View File

@ -15,7 +15,7 @@ class BrandBottomSheet extends StatelessWidget {
Widget build(final BuildContext context) {
final double mainHeight = MediaQuery.of(context).size.height -
MediaQuery.of(context).padding.top -
100;
300;
late Widget innerWidget;
if (isExpended) {
innerWidget = Scaffold(
@ -29,31 +29,28 @@ class BrandBottomSheet extends StatelessWidget {
child: IntrinsicHeight(child: child),
);
}
return ConstrainedBox(
constraints: BoxConstraints(maxHeight: mainHeight + 4 + 6),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Center(
child: Container(
height: 4,
width: 30,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(2),
color: BrandColors.gray4,
),
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Center(
child: Container(
height: 4,
width: 30,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(2),
color: BrandColors.gray4,
),
),
const SizedBox(height: 6),
ClipRRect(
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
child: ConstrainedBox(
constraints: BoxConstraints(maxHeight: mainHeight),
child: innerWidget,
),
),
const SizedBox(height: 6),
ClipRRect(
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
child: ConstrainedBox(
constraints: BoxConstraints(maxHeight: mainHeight),
child: innerWidget,
),
],
),
),
],
);
}
}

View File

@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
import 'package:selfprivacy/config/brand_colors.dart';
import 'package:selfprivacy/config/text_themes.dart';
import 'package:selfprivacy/logic/cubit/app_settings/app_settings_cubit.dart';
import 'package:selfprivacy/ui/components/brand_text/brand_text.dart';
class ProgressBar extends StatefulWidget {
const ProgressBar({
@ -63,13 +62,6 @@ class _ProgressBarState extends State<ProgressBar> {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
BrandText.h2('Progress'),
const SizedBox(height: 10),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: even,
),
const SizedBox(height: 7),
Container(
alignment: Alignment.centerLeft,
decoration: BoxDecoration(
@ -98,11 +90,6 @@ class _ProgressBarState extends State<ProgressBar> {
),
),
),
const SizedBox(height: 5),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: odd,
),
],
);
}

View File

@ -5,6 +5,7 @@ import 'package:selfprivacy/ui/components/brand_header/brand_header.dart';
import 'package:selfprivacy/ui/components/brand_text/brand_text.dart';
import 'package:package_info/package_info.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:url_launcher/url_launcher.dart';
class AboutApplicationPage extends StatelessWidget {
const AboutApplicationPage({super.key});
@ -37,6 +38,26 @@ class AboutApplicationPage extends StatelessWidget {
.tr(args: [snapshot.data.toString()]),
),
),
const SizedBox(height: 10),
// Button to call showAboutDialog
TextButton(
onPressed: () => showAboutDialog(
context: context,
applicationName: 'SelfPrivacy',
applicationLegalese: '© 2022 SelfPrivacy',
// Link to privacy policy
children: [
TextButton(
onPressed: () => launchUrl(
Uri.parse('https://selfprivacy.ru/privacy-policy'),
mode: LaunchMode.externalApplication,
),
child: Text('about_application_page.privacy_policy'.tr()),
),
],
),
child: const Text('Show about dialog'),
),
],
),
),

View File

@ -1,7 +1,6 @@
import 'package:flutter/material.dart';
import 'package:selfprivacy/logic/cubit/app_settings/app_settings_cubit.dart';
import 'package:selfprivacy/ui/components/brand_button/brand_button.dart';
import 'package:selfprivacy/ui/components/brand_text/brand_text.dart';
import 'package:selfprivacy/utils/route_transitions/basic.dart';
import 'package:easy_localization/easy_localization.dart';
@ -49,11 +48,16 @@ class _OnboardingPageState extends State<OnboardingPage> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 30),
BrandText.h2(
Text(
'onboarding.page1_title'.tr(),
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 20),
BrandText.body2('onboarding.page1_text'.tr()),
const SizedBox(height: 16),
Text(
'onboarding.page1_text'.tr(),
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 16),
Flexible(
child: Center(
child: Image.asset(
@ -86,34 +90,49 @@ class _OnboardingPageState extends State<OnboardingPage> {
maxHeight: MediaQuery.of(context).size.height,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 30),
BrandText.h2('onboarding.page2_title'.tr()),
const SizedBox(height: 20),
BrandText.body2('onboarding.page2_text'.tr()),
const SizedBox(height: 20),
Center(
child: Image.asset(
_fileName(
context: context,
path: 'assets/images/onboarding',
fileExtention: 'png',
fileName: 'logos_line',
),
),
Text(
'onboarding.page2_title'.tr(),
style: Theme.of(context).textTheme.headlineSmall,
),
Flexible(
child: Center(
child: Image.asset(
_fileName(
context: context,
path: 'assets/images/onboarding',
fileExtention: 'png',
fileName: 'onboarding2',
),
),
),
const SizedBox(height: 16),
Text(
'onboarding.page2_text'.tr(),
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 16),
Text(
'onboarding.page2_server_provider_title'.tr(),
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 16),
Text(
'onboarding.page2_server_provider_text'.tr(),
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 16),
Text(
'onboarding.page2_dns_provider_title'.tr(),
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 16),
Text(
'onboarding.page2_dns_provider_text'.tr(),
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 16),
Text(
'onboarding.page2_backup_provider_title'.tr(),
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 16),
Text(
'onboarding.page2_backup_provider_text'.tr(),
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 16),
BrandButton.rised(
onPressed: () {
context.read<AppSettingsCubit>().turnOffOnboarding();

View File

@ -1,3 +1,5 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:selfprivacy/logic/common_enum/common_enum.dart';
@ -82,7 +84,10 @@ class CpuChart extends StatelessWidget {
),
],
minY: 0,
maxY: 100,
// Maximal value of data by 100 step
maxY:
((data.map((final e) => e.value).reduce(max) - 1) / 100).ceil() *
100.0,
minX: 0,
titlesData: FlTitlesData(
topTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)),

View File

@ -9,6 +9,7 @@ import 'package:selfprivacy/logic/models/service.dart';
import 'package:selfprivacy/ui/components/brand_cards/filled_card.dart';
import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart';
import 'package:selfprivacy/ui/pages/server_storage/binds_migration/services_migration.dart';
import 'package:selfprivacy/utils/network_utils.dart';
import 'package:selfprivacy/utils/route_transitions/basic.dart';
import 'package:url_launcher/url_launcher.dart';
@ -59,7 +60,7 @@ class _ServicePageState extends State<ServicePage> {
if (service.url != null)
ListTile(
iconColor: Theme.of(context).colorScheme.onBackground,
onTap: () => _launchURL(service.url),
onTap: () => launchURL(service.url),
leading: const Icon(Icons.open_in_browser),
title: Text(
'service_page.open_in_browser'.tr(),
@ -232,15 +233,3 @@ class ServiceStatusCard extends StatelessWidget {
}
}
}
void _launchURL(final url) async {
try {
final Uri uri = Uri.parse(url);
await launchUrl(
uri,
mode: LaunchMode.externalApplication,
);
} catch (e) {
print(e);
}
}

View File

@ -1,34 +1,22 @@
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:selfprivacy/config/brand_theme.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
import 'package:selfprivacy/logic/cubit/client_jobs/client_jobs_cubit.dart';
import 'package:selfprivacy/logic/cubit/services/services_cubit.dart';
import 'package:selfprivacy/logic/models/job.dart';
import 'package:selfprivacy/logic/models/service.dart';
import 'package:selfprivacy/logic/models/state_types.dart';
import 'package:selfprivacy/ui/components/brand_cards/brand_cards.dart';
import 'package:selfprivacy/ui/components/brand_header/brand_header.dart';
import 'package:selfprivacy/ui/components/brand_switch/brand_switch.dart';
import 'package:selfprivacy/ui/components/brand_text/brand_text.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:selfprivacy/ui/pages/services/service_page.dart';
import 'package:selfprivacy/utils/network_utils.dart';
import 'package:selfprivacy/utils/route_transitions/basic.dart';
import 'package:selfprivacy/utils/ui_helpers.dart';
import 'package:url_launcher/url_launcher.dart';
const switchableServices = [
'bitwarden',
'nextcloud',
'pleroma',
'gitea',
'ocserv',
];
class ServicesPage extends StatefulWidget {
const ServicesPage({super.key});
@ -36,24 +24,16 @@ class ServicesPage extends StatefulWidget {
State<ServicesPage> createState() => _ServicesPageState();
}
void _launchURL(final url) async {
try {
final Uri uri = Uri.parse(url);
await launchUrl(
uri,
mode: LaunchMode.externalApplication,
);
} catch (e) {
print(e);
}
}
class _ServicesPageState extends State<ServicesPage> {
@override
Widget build(final BuildContext context) {
final isReady = context.watch<ServerInstallationCubit>().state
is ServerInstallationFinished;
final services = [...context.watch<ServicesCubit>().state.services];
services
.sort((final a, final b) => a.status.index.compareTo(b.status.index));
return Scaffold(
appBar: PreferredSize(
preferredSize: const Size.fromHeight(52),
@ -71,10 +51,7 @@ class _ServicesPageState extends State<ServicesPage> {
BrandText.body1('basis.services_title'.tr()),
const SizedBox(height: 24),
if (!isReady) ...[const NotReadyCard(), const SizedBox(height: 24)],
...context
.read<ServicesCubit>()
.state
.services
...services
.map(
(final service) => Padding(
padding: const EdgeInsets.only(
@ -100,24 +77,28 @@ class _Card extends StatelessWidget {
final isReady = context.watch<ServerInstallationCubit>().state
is ServerInstallationFinished;
final serviceState = context.watch<ServicesCubit>().state;
final jobsCubit = context.watch<JobsCubit>();
final jobState = jobsCubit.state;
final switchableService = switchableServices.contains(service.id);
final hasSwitchJob = switchableService &&
jobState is JobsStateWithJobs &&
jobState.clientJobList.any(
(final el) => el is ServiceToggleJob && el.id == service.id,
);
final isSwitchOn = isReady &&
(!switchableServices.contains(service.id) ||
serviceState.isEnableByType(service));
final config = context.watch<ServerInstallationCubit>().state;
final domainName = UiHelpers.getDomainName(config);
StateType getStatus(final ServiceStatus status) {
switch (status) {
case ServiceStatus.active:
return StateType.stable;
case ServiceStatus.activating:
return StateType.stable;
case ServiceStatus.deactivating:
return StateType.uninitialized;
case ServiceStatus.inactive:
return StateType.uninitialized;
case ServiceStatus.failed:
return StateType.error;
case ServiceStatus.off:
return StateType.uninitialized;
case ServiceStatus.reloading:
return StateType.stable;
}
}
return GestureDetector(
onTap: isReady
? () => Navigator.of(context)
@ -130,8 +111,7 @@ class _Card extends StatelessWidget {
Row(
children: [
IconStatusMask(
status:
isSwitchOn ? StateType.stable : StateType.uninitialized,
status: getStatus(service.status),
icon: SvgPicture.string(
service.svgIcon,
width: 30.0,
@ -139,33 +119,6 @@ class _Card extends StatelessWidget {
color: Theme.of(context).colorScheme.onBackground,
),
),
if (isReady && switchableService) ...[
const Spacer(),
Builder(
builder: (final context) {
late bool isActive;
if (hasSwitchJob) {
isActive = (jobState.clientJobList.firstWhere(
(final el) =>
el is ServiceToggleJob && el.id == service.id,
) as ServiceToggleJob)
.needToTurnOn;
} else {
isActive = serviceState.isEnableByType(service);
}
return BrandSwitch(
value: isActive,
onChanged: (final value) => jobsCubit.addJob(
ServiceToggleJob(
service: service,
needToTurnOn: value,
),
),
);
},
),
]
],
),
ClipRect(
@ -181,7 +134,7 @@ class _Card extends StatelessWidget {
Column(
children: [
GestureDetector(
onTap: () => _launchURL(
onTap: () => launchURL(
'https://${service.url}',
),
child: Text(
@ -215,22 +168,6 @@ class _Card extends StatelessWidget {
const SizedBox(height: 10),
],
),
if (hasSwitchJob)
Positioned(
bottom: 24,
left: 0,
right: 0,
child: BackdropFilter(
filter: ImageFilter.blur(
sigmaX: 3,
sigmaY: 2,
),
child: BrandText.h2(
'jobs.run_jobs'.tr(),
textAlign: TextAlign.center,
),
),
)
],
),
)

View File

@ -1,14 +1,17 @@
import 'package:cubit_form/cubit_form.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.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/forms/setup/initializing/dns_provider_form_cubit.dart';
import 'package:selfprivacy/logic/models/hive/server_details.dart';
import 'package:selfprivacy/logic/models/hive/server_domain.dart';
import 'package:selfprivacy/ui/components/brand_bottom_sheet/brand_bottom_sheet.dart';
import 'package:selfprivacy/ui/components/brand_button/filled_button.dart';
import 'package:selfprivacy/ui/components/brand_button/outlined_button.dart';
import 'package:selfprivacy/ui/components/brand_cards/outlined_card.dart';
import 'package:selfprivacy/ui/components/brand_md/brand_md.dart';
import 'package:selfprivacy/utils/network_utils.dart';
class DnsProviderPicker extends StatefulWidget {
const DnsProviderPicker({
@ -60,7 +63,7 @@ class _DnsProviderPickerState extends State<DnsProviderPicker> {
providerCubit: widget.formCubit,
providerInfo: ProviderPageInfo(
providerType: DnsProvider.digitalOcean,
pathToHow: 'how_cloudflare',
pathToHow: 'how_digital_ocean_dns',
image: Image.asset(
'assets/images/logos/digital_ocean.png',
width: 150,
@ -155,51 +158,175 @@ class ProviderSelectionPage extends StatelessWidget {
final ServerInstallationCubit serverInstallationCubit;
@override
Widget build(final BuildContext context) => Column(
children: [
Text(
'initializing.select_provider'.tr(),
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 10),
Text(
'initializing.dns_provider_description'.tr(),
),
const SizedBox(height: 10),
ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 320,
Widget build(final BuildContext context) => SizedBox(
width: double.infinity,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'initializing.connect_to_server'.tr(),
style: Theme.of(context).textTheme.headlineSmall,
),
child: Row(
children: [
InkWell(
onTap: () {
serverInstallationCubit
.setDnsProviderType(DnsProvider.cloudflare);
callback(DnsProvider.cloudflare);
},
child: Image.asset(
'assets/images/logos/cloudflare.png',
width: 150,
),
),
const SizedBox(
width: 20,
),
InkWell(
onTap: () {
serverInstallationCubit
.setDnsProviderType(DnsProvider.digitalOcean);
callback(DnsProvider.digitalOcean);
},
child: Image.asset(
'assets/images/logos/digital_ocean.png',
width: 150,
),
),
],
const SizedBox(height: 10),
Text(
'initializing.select_provider'.tr(),
style: Theme.of(context).textTheme.bodyMedium,
),
),
],
const SizedBox(height: 10),
OutlinedCard(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 40,
height: 40,
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(40),
color: const Color(0xFFD50C2D),
),
child: SvgPicture.asset(
'assets/images/logos/hetzner.svg',
),
),
const SizedBox(width: 16),
Text(
'Hetzner Cloud',
style: Theme.of(context).textTheme.titleMedium,
),
],
),
const SizedBox(height: 16),
Text(
'initializing.select_provider_countries_title'.tr(),
style: Theme.of(context).textTheme.bodyLarge,
),
Text(
'initializing.select_provider_countries_text_hetzner'
.tr(),
style: Theme.of(context).textTheme.bodySmall,
),
const SizedBox(height: 16),
Text(
'initializing.select_provider_price_title'.tr(),
style: Theme.of(context).textTheme.bodyLarge,
),
Text(
'initializing.select_provider_price_text_hetzner'.tr(),
style: Theme.of(context).textTheme.bodySmall,
),
const SizedBox(height: 16),
Text(
'initializing.select_provider_payment_title'.tr(),
style: Theme.of(context).textTheme.bodyLarge,
),
Text(
'initializing.select_provider_payment_text_hetzner'.tr(),
style: Theme.of(context).textTheme.bodySmall,
),
const SizedBox(height: 16),
Text(
'initializing.select_provider_email_notice'.tr(),
style: Theme.of(context).textTheme.bodySmall,
),
const SizedBox(height: 16),
FilledButton(
title: 'basis.select'.tr(),
onPressed: () {
serverInstallationCubit
.setDnsProviderType(DnsProvider.cloudflare);
callback(DnsProvider.cloudflare);
},
),
// Outlined button that will open website
BrandOutlinedButton(
onPressed: () =>
launchURL('https://cloud.digitalocean.com/'),
title: 'initializing.select_provider_site_button'.tr(),
),
],
),
),
),
const SizedBox(height: 16),
OutlinedCard(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 40,
height: 40,
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(40),
color: const Color(0xFF0080FF),
),
child: SvgPicture.asset(
'assets/images/logos/digital_ocean.svg',
),
),
const SizedBox(width: 16),
Text(
'Digital Ocean',
style: Theme.of(context).textTheme.titleMedium,
),
],
),
const SizedBox(height: 16),
Text(
'initializing.select_provider_countries_title'.tr(),
style: Theme.of(context).textTheme.bodyLarge,
),
Text(
'initializing.select_provider_countries_text_do'.tr(),
style: Theme.of(context).textTheme.bodySmall,
),
const SizedBox(height: 16),
Text(
'initializing.select_provider_price_title'.tr(),
style: Theme.of(context).textTheme.bodyLarge,
),
Text(
'initializing.select_provider_price_text_do'.tr(),
style: Theme.of(context).textTheme.bodySmall,
),
const SizedBox(height: 16),
Text(
'initializing.select_provider_payment_title'.tr(),
style: Theme.of(context).textTheme.bodyLarge,
),
Text(
'initializing.select_provider_payment_text_do'.tr(),
style: Theme.of(context).textTheme.bodySmall,
),
const SizedBox(height: 16),
FilledButton(
title: 'basis.select'.tr(),
onPressed: () {
serverInstallationCubit
.setDnsProviderType(DnsProvider.digitalOcean);
callback(DnsProvider.digitalOcean);
},
),
// Outlined button that will open website
BrandOutlinedButton(
onPressed: () =>
launchURL('https://www.digitalocean.com'),
title: 'initializing.select_provider_site_button'.tr(),
),
],
),
),
),
],
),
);
}

View File

@ -11,7 +11,6 @@ import 'package:selfprivacy/logic/cubit/forms/setup/initializing/domain_setup_cu
import 'package:selfprivacy/logic/cubit/forms/setup/initializing/root_user_form_cubit.dart';
import 'package:selfprivacy/ui/components/brand_bottom_sheet/brand_bottom_sheet.dart';
import 'package:selfprivacy/ui/components/brand_button/brand_button.dart';
import 'package:selfprivacy/ui/components/brand_cards/brand_cards.dart';
import 'package:selfprivacy/ui/components/brand_md/brand_md.dart';
import 'package:selfprivacy/ui/components/brand_text/brand_text.dart';
import 'package:selfprivacy/ui/components/brand_timer/brand_timer.dart';
@ -57,88 +56,92 @@ class InitializingPage extends StatelessWidget {
.pushReplacement(materialRoute(const RootPage()));
}
},
child: SafeArea(
child: Scaffold(
body: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Padding(
padding: paddingH15V0.copyWith(top: 10, bottom: 10),
child: cubit.state is ServerInstallationFinished
? const SizedBox(
height: 80,
)
: ProgressBar(
steps: const [
'Hosting',
'Server Type',
'CloudFlare',
'Backblaze',
'Domain',
'User',
'Server',
'Installation',
],
activeIndex: cubit.state.porgressBar,
),
child: Scaffold(
appBar: AppBar(
actions: [
if (cubit.state is ServerInstallationFinished)
IconButton(
icon: const Icon(Icons.check),
onPressed: () {
Navigator.of(context)
.pushReplacement(materialRoute(const RootPage()));
},
)
],
bottom: PreferredSize(
preferredSize: const Size.fromHeight(28),
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
child: ProgressBar(
steps: const [
'Hosting',
'Server Type',
'CloudFlare',
'Backblaze',
'Domain',
'User',
'Server',
'Installation',
],
activeIndex: cubit.state.porgressBar,
),
),
),
),
body: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(16.0, 0, 16.0, 0.0),
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: actualInitializingPage,
),
if (cubit.state.porgressBar ==
ServerSetupProgress.serverProviderFilled.index)
BrandText.h2(
'initializing.choose_location_type'.tr(),
),
_addCard(
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: actualInitializingPage,
),
),
ConstrainedBox(
constraints: BoxConstraints(
minHeight: MediaQuery.of(context).size.height -
MediaQuery.of(context).padding.top -
MediaQuery.of(context).padding.bottom -
566,
),
ConstrainedBox(
constraints: BoxConstraints(
minHeight: MediaQuery.of(context).size.height -
MediaQuery.of(context).padding.top -
MediaQuery.of(context).padding.bottom -
566,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
alignment: Alignment.center,
child: BrandButton.text(
title: cubit.state is ServerInstallationFinished
? 'basis.close'.tr()
: 'basis.later'.tr(),
onPressed: () {
Navigator.of(context).pushAndRemoveUntil(
materialRoute(const RootPage()),
(final predicate) => false,
);
},
),
),
if (cubit.state is ServerInstallationEmpty ||
cubit.state is ServerInstallationNotFinished)
Container(
alignment: Alignment.center,
child: BrandButton.text(
title: cubit.state is ServerInstallationFinished
? 'basis.close'.tr()
: 'basis.later'.tr(),
title: 'basis.connect_to_existing'.tr(),
onPressed: () {
Navigator.of(context).pushAndRemoveUntil(
materialRoute(const RootPage()),
(final predicate) => false,
Navigator.of(context).push(
materialRoute(
const RecoveryRouting(),
),
);
},
),
),
if (cubit.state is ServerInstallationFinished)
Container()
else
Container(
alignment: Alignment.center,
child: BrandButton.text(
title: 'basis.connect_to_existing'.tr(),
onPressed: () {
Navigator.of(context).push(
materialRoute(
const RecoveryRouting(),
),
);
},
),
)
],
),
)
],
),
],
),
),
],
),
),
),
@ -210,14 +213,11 @@ class InitializingPage extends StatelessWidget {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.asset(
'assets/images/logos/backblaze.png',
height: 50,
Text(
'${'initializing.connect_to_server_provider'.tr()}Backblaze',
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 10),
BrandText.h2('initializing.connect_backblaze_storage'.tr()),
const SizedBox(height: 10),
const Spacer(),
const SizedBox(height: 32),
CubitFormTextField(
formFieldCubit: context.read<BackblazeFormCubit>().keyId,
textAlign: TextAlign.center,
@ -226,7 +226,7 @@ class InitializingPage extends StatelessWidget {
hintText: 'KeyID',
),
),
const Spacer(),
const SizedBox(height: 16),
CubitFormTextField(
formFieldCubit:
context.read<BackblazeFormCubit>().applicationKey,
@ -236,7 +236,7 @@ class InitializingPage extends StatelessWidget {
hintText: 'Master Application Key',
),
),
const Spacer(),
const SizedBox(height: 32),
BrandButton.rised(
onPressed: formCubitState.isSubmitting
? null
@ -266,91 +266,85 @@ class InitializingPage extends StatelessWidget {
builder: (final context) {
final DomainSetupState state =
context.watch<DomainSetupCubit>().state;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.asset(
'assets/images/logos/cloudflare.png',
width: 150,
),
const SizedBox(height: 30),
BrandText.h2('basis.domain'.tr()),
const SizedBox(height: 10),
if (state is Empty)
BrandText.body2('initializing.no_connected_domains'.tr()),
if (state is Loading)
BrandText.body2(
state.type == LoadingTypes.loadingDomain
? 'initializing.loading_domain_list'.tr()
: 'basis.saving'.tr(),
return SizedBox(
width: double.infinity,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'initializing.use_this_domain'.tr(),
style: Theme.of(context).textTheme.headlineSmall,
),
if (state is MoreThenOne)
BrandText.body2(
'initializing.found_more_domains'.tr(),
const SizedBox(height: 16),
Text(
'initializing.use_this_domain_text'.tr(),
style: Theme.of(context).textTheme.bodyMedium,
),
if (state is Loaded) ...[
const SizedBox(height: 10),
Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: BrandText.h3(
const SizedBox(height: 32),
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,
),
),
SizedBox(
width: 56,
child: BrandButton.rised(
onPressed: () =>
context.read<DomainSetupCubit>().load(),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: const [
Icon(
Icons.refresh,
color: Colors.white,
),
],
),
),
),
],
)
],
if (state is Empty) ...[
const SizedBox(height: 30),
BrandButton.rised(
onPressed: () => context.read<DomainSetupCubit>().load(),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.refresh,
color: Colors.white,
),
const SizedBox(width: 10),
BrandText.buttonTitleText('domain.update_list'.tr()),
],
),
),
],
if (state is Empty) ...[
const SizedBox(height: 30),
BrandButton.rised(
onPressed: () => context.read<DomainSetupCubit>().load(),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.refresh,
color: Colors.white,
),
const SizedBox(width: 10),
BrandText.buttonTitleText('domain.update_list'.tr()),
],
),
),
],
if (state is Loaded) ...[
const SizedBox(height: 32),
BrandButton.rised(
onPressed: () =>
context.read<DomainSetupCubit>().saveDomain(),
text: 'initializing.save_domain'.tr(),
),
],
],
if (state is Loaded) ...[
const SizedBox(height: 30),
BrandButton.rised(
onPressed: () =>
context.read<DomainSetupCubit>().saveDomain(),
text: 'initializing.save_domain'.tr(),
),
],
const SizedBox(
height: 10,
width: double.infinity,
),
],
),
);
},
),
@ -367,12 +361,16 @@ class InitializingPage extends StatelessWidget {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
BrandText.h2('initializing.create_master_account'.tr()),
const SizedBox(height: 10),
BrandText.body2(
'initializing.enter_username_and_password'.tr(),
Text(
'initializing.create_master_account'.tr(),
style: Theme.of(context).textTheme.headlineSmall,
),
const Spacer(),
const SizedBox(height: 16),
Text(
'initializing.enter_username_and_password'.tr(),
style: Theme.of(context).textTheme.bodyMedium,
),
if (formCubitState.isErrorShown) const SizedBox(height: 16),
if (formCubitState.isErrorShown)
Text(
'users.username_rule'.tr(),
@ -380,7 +378,7 @@ class InitializingPage extends StatelessWidget {
color: Theme.of(context).colorScheme.error,
),
),
const SizedBox(height: 10),
const SizedBox(height: 32),
CubitFormTextField(
formFieldCubit: context.read<RootUserFormCubit>().userName,
textAlign: TextAlign.center,
@ -389,7 +387,7 @@ class InitializingPage extends StatelessWidget {
hintText: 'basis.username'.tr(),
),
),
const SizedBox(height: 10),
const SizedBox(height: 16),
BlocBuilder<FieldCubit<bool>, FieldCubitState<bool>>(
bloc: context.read<RootUserFormCubit>().isVisible,
builder: (final context, final state) {
@ -420,7 +418,7 @@ class InitializingPage extends StatelessWidget {
);
},
),
const Spacer(),
const SizedBox(height: 32),
BrandButton.rised(
onPressed: formCubitState.isSubmitting
? null
@ -440,11 +438,16 @@ class InitializingPage extends StatelessWidget {
builder: (final context) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Spacer(flex: 2),
BrandText.h2('initializing.final'.tr()),
const SizedBox(height: 10),
BrandText.body2('initializing.create_server'.tr()),
const Spacer(),
Text(
'initializing.final'.tr(),
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 16),
Text(
'initializing.create_server'.tr(),
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 128),
BrandButton.rised(
onPressed:
isLoading ? null : appConfigCubit.createServerAndSetDnsRecords,
@ -479,55 +482,64 @@ class InitializingPage extends StatelessWidget {
doneCount = 0;
}
return Builder(
builder: (final context) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 15),
BrandText.h4(
'initializing.checks'.tr(args: [doneCount.toString(), '4']),
),
const Spacer(flex: 2),
const SizedBox(height: 10),
BrandText.body2(text),
const SizedBox(height: 10),
if (doneCount == 0 && state.dnsMatches != null)
Column(
children: state.dnsMatches!.entries.map((final entry) {
final String domain = entry.key;
final bool isCorrect = entry.value;
return Row(
children: [
if (isCorrect) const Icon(Icons.check, color: Colors.green),
if (!isCorrect)
const Icon(Icons.schedule, color: Colors.amber),
const SizedBox(width: 10),
Text(domain),
],
);
}).toList(),
builder: (final context) => SizedBox(
width: double.infinity,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'initializing.checks'.tr(args: [doneCount.toString(), '4']),
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 10),
if (!state.isLoading)
Row(
children: [
BrandText.body2('initializing.until_the_next_check'.tr()),
BrandTimer(
startDateTime: state.timerStart!,
duration: state.duration!,
)
],
),
if (state.isLoading) BrandText.body2('initializing.check'.tr()),
],
const SizedBox(height: 16),
if (text != null)
Text(
text,
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 128),
const SizedBox(height: 10),
if (doneCount == 0 && state.dnsMatches != null)
Column(
children: state.dnsMatches!.entries.map((final entry) {
final String domain = entry.key;
final bool isCorrect = entry.value;
return Row(
children: [
if (isCorrect)
const Icon(Icons.check, color: Colors.green),
if (!isCorrect)
const Icon(Icons.schedule, color: Colors.amber),
const SizedBox(width: 10),
Text(domain),
],
);
}).toList(),
),
const SizedBox(height: 10),
if (!state.isLoading)
Row(
children: [
Text(
'initializing.until_the_next_check'.tr(),
style: Theme.of(context).textTheme.bodyMedium,
),
BrandTimer(
startDateTime: state.timerStart!,
duration: state.duration!,
)
],
),
if (state.isLoading)
Text(
'initializing.check'.tr(),
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
),
);
}
Widget _addCard(final Widget child) => Container(
height: 450,
padding: paddingH15V0,
child: BrandCards.big(child: child),
);
}
class _HowTo extends StatelessWidget {

View File

@ -1,13 +1,18 @@
import 'package:cubit_form/cubit_form.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.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/forms/setup/initializing/server_provider_form_cubit.dart';
import 'package:selfprivacy/logic/models/hive/server_details.dart';
import 'package:selfprivacy/ui/components/brand_bottom_sheet/brand_bottom_sheet.dart';
import 'package:selfprivacy/ui/components/brand_button/filled_button.dart';
import 'package:selfprivacy/ui/components/brand_button/outlined_button.dart';
import 'package:selfprivacy/ui/components/brand_cards/outlined_card.dart';
import 'package:selfprivacy/ui/components/brand_md/brand_md.dart';
import 'package:selfprivacy/ui/components/info_box/info_box.dart';
import 'package:selfprivacy/utils/network_utils.dart';
class ServerProviderPicker extends StatefulWidget {
const ServerProviderPicker({
@ -46,7 +51,7 @@ class _ServerProviderPickerState extends State<ServerProviderPicker> {
providerCubit: widget.formCubit,
providerInfo: ProviderPageInfo(
providerType: ServerProvider.hetzner,
pathToHow: 'hetzner_how',
pathToHow: 'how_hetzner',
image: Image.asset(
'assets/images/logos/hetzner.png',
width: 150,
@ -59,7 +64,7 @@ class _ServerProviderPickerState extends State<ServerProviderPicker> {
providerCubit: widget.formCubit,
providerInfo: ProviderPageInfo(
providerType: ServerProvider.digitalOcean,
pathToHow: 'hetzner_how',
pathToHow: 'how_digital_ocean',
image: Image.asset(
'assets/images/logos/digital_ocean.png',
width: 150,
@ -96,13 +101,16 @@ class ProviderInputDataPage extends StatelessWidget {
Widget build(final BuildContext context) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
providerInfo.image,
const SizedBox(height: 10),
Text(
'initializing.connect_to_server'.tr(),
style: Theme.of(context).textTheme.titleLarge,
"${'initializing.connect_to_server_provider'.tr()}${providerInfo.providerType.displayName}",
style: Theme.of(context).textTheme.headlineSmall,
),
const Spacer(),
const SizedBox(height: 16),
Text(
'initializing.connect_to_server_provider_text'.tr(),
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 32),
CubitFormTextField(
formFieldCubit: providerCubit.apiKey,
textAlign: TextAlign.center,
@ -111,13 +119,13 @@ class ProviderInputDataPage extends StatelessWidget {
hintText: 'Provider API Token',
),
),
const Spacer(),
const SizedBox(height: 32),
FilledButton(
title: 'basis.connect'.tr(),
onPressed: () => providerCubit.trySubmit(),
),
const SizedBox(height: 10),
OutlinedButton(
BrandOutlinedButton(
child: Text('initializing.how'.tr()),
onPressed: () => showModalBottomSheet<void>(
context: context,
@ -154,51 +162,177 @@ class ProviderSelectionPage extends StatelessWidget {
final ServerInstallationCubit serverInstallationCubit;
@override
Widget build(final BuildContext context) => Column(
children: [
Text(
'initializing.select_provider'.tr(),
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 10),
Text(
'initializing.server_provider_description'.tr(),
),
const SizedBox(height: 10),
ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 320,
Widget build(final BuildContext context) => SizedBox(
width: double.infinity,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'initializing.connect_to_server'.tr(),
style: Theme.of(context).textTheme.headlineSmall,
),
child: Row(
children: [
InkWell(
onTap: () {
serverInstallationCubit
.setServerProviderType(ServerProvider.hetzner);
callback(ServerProvider.hetzner);
},
child: Image.asset(
'assets/images/logos/hetzner.png',
width: 150,
),
),
const SizedBox(
width: 20,
),
InkWell(
onTap: () {
serverInstallationCubit
.setServerProviderType(ServerProvider.digitalOcean);
callback(ServerProvider.digitalOcean);
},
child: Image.asset(
'assets/images/logos/digital_ocean.png',
width: 150,
),
),
],
const SizedBox(height: 10),
Text(
'initializing.select_provider'.tr(),
style: Theme.of(context).textTheme.bodyMedium,
),
),
],
const SizedBox(height: 10),
OutlinedCard(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 40,
height: 40,
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(40),
color: const Color(0xFFD50C2D),
),
child: SvgPicture.asset(
'assets/images/logos/hetzner.svg',
),
),
const SizedBox(width: 16),
Text(
'Hetzner Cloud',
style: Theme.of(context).textTheme.titleMedium,
),
],
),
const SizedBox(height: 16),
Text(
'initializing.select_provider_countries_title'.tr(),
style: Theme.of(context).textTheme.bodyLarge,
),
Text(
'initializing.select_provider_countries_text_hetzner'
.tr(),
style: Theme.of(context).textTheme.bodySmall,
),
const SizedBox(height: 16),
Text(
'initializing.select_provider_price_title'.tr(),
style: Theme.of(context).textTheme.bodyLarge,
),
Text(
'initializing.select_provider_price_text_hetzner'.tr(),
style: Theme.of(context).textTheme.bodySmall,
),
const SizedBox(height: 16),
Text(
'initializing.select_provider_payment_title'.tr(),
style: Theme.of(context).textTheme.bodyLarge,
),
Text(
'initializing.select_provider_payment_text_hetzner'.tr(),
style: Theme.of(context).textTheme.bodySmall,
),
const SizedBox(height: 16),
Text(
'initializing.select_provider_email_notice'.tr(),
style: Theme.of(context).textTheme.bodySmall,
),
const SizedBox(height: 16),
FilledButton(
title: 'basis.select'.tr(),
onPressed: () {
serverInstallationCubit
.setServerProviderType(ServerProvider.hetzner);
callback(ServerProvider.hetzner);
},
),
// Outlined button that will open website
BrandOutlinedButton(
onPressed: () =>
launchURL('https://www.hetzner.com/cloud'),
title: 'initializing.select_provider_site_button'.tr(),
),
],
),
),
),
const SizedBox(height: 16),
OutlinedCard(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 40,
height: 40,
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(40),
color: const Color(0xFF0080FF),
),
child: SvgPicture.asset(
'assets/images/logos/digital_ocean.svg',
),
),
const SizedBox(width: 16),
Text(
'Digital Ocean',
style: Theme.of(context).textTheme.titleMedium,
),
],
),
const SizedBox(height: 16),
Text(
'initializing.select_provider_countries_title'.tr(),
style: Theme.of(context).textTheme.bodyLarge,
),
Text(
'initializing.select_provider_countries_text_do'.tr(),
style: Theme.of(context).textTheme.bodySmall,
),
const SizedBox(height: 16),
Text(
'initializing.select_provider_price_title'.tr(),
style: Theme.of(context).textTheme.bodyLarge,
),
Text(
'initializing.select_provider_price_text_do'.tr(),
style: Theme.of(context).textTheme.bodySmall,
),
const SizedBox(height: 16),
Text(
'initializing.select_provider_payment_title'.tr(),
style: Theme.of(context).textTheme.bodyLarge,
),
Text(
'initializing.select_provider_payment_text_do'.tr(),
style: Theme.of(context).textTheme.bodySmall,
),
const SizedBox(height: 16),
FilledButton(
title: 'basis.select'.tr(),
onPressed: () {
serverInstallationCubit
.setServerProviderType(ServerProvider.digitalOcean);
callback(ServerProvider.digitalOcean);
},
),
// Outlined button that will open website
BrandOutlinedButton(
onPressed: () =>
launchURL('https://www.digitalocean.com'),
title: 'initializing.select_provider_site_button'.tr(),
),
],
),
),
),
const SizedBox(height: 16),
InfoBox(text: 'initializing.select_provider_notice'.tr()),
],
),
);
}

View File

@ -1,10 +1,12 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:selfprivacy/config/brand_theme.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_settings/app_settings_cubit.dart';
import 'package:selfprivacy/logic/models/server_provider_location.dart';
import 'package:selfprivacy/logic/models/server_type.dart';
import 'package:selfprivacy/ui/components/brand_button/brand_button.dart';
import 'package:selfprivacy/ui/components/info_box/info_box.dart';
class ServerTypePicker extends StatefulWidget {
const ServerTypePicker({
@ -68,27 +70,43 @@ class SelectLocationPage extends StatelessWidget {
if ((snapshot.data as List<ServerProviderLocation>).isEmpty) {
return Text('initializing.no_locations_found'.tr());
}
return ListView(
padding: paddingH15V0,
return Column(
children: [
Text(
'initializing.choose_location_type'.tr(),
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 16),
Text(
'initializing.choose_location_type_text'.tr(),
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 16),
...(snapshot.data! as List<ServerProviderLocation>).map(
(final location) => InkWell(
onTap: () {
callback(location);
},
child: Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (location.flag != null) Text(location.flag!),
const SizedBox(height: 8),
Text(location.title),
const SizedBox(height: 8),
if (location.description != null)
Text(location.description!),
],
(final location) => SizedBox(
width: double.infinity,
child: InkWell(
onTap: () {
callback(location);
},
child: Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${location.flag ?? ''} ${location.title}',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
if (location.description != null)
Text(
location.description!,
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
),
),
),
@ -126,11 +144,33 @@ class SelectTypePage extends StatelessWidget {
if (snapshot.hasData) {
if ((snapshot.data as List<ServerType>).isEmpty) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'initializing.no_server_types_found'.tr(),
'initializing.locations_not_found'.tr(),
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 10),
const SizedBox(height: 16),
Text(
'initializing.locations_not_found_text'.tr(),
style: Theme.of(context).textTheme.bodyMedium,
),
LayoutBuilder(
builder: (final context, final constraints) => CustomPaint(
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(
onPressed: () {
backToLocationPickingCallback();
@ -140,51 +180,120 @@ class SelectTypePage extends StatelessWidget {
],
);
}
return ListView(
padding: paddingH15V0,
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'initializing.choose_server_type'.tr(),
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 16),
Text(
'initializing.choose_server_type_text'.tr(),
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 16),
...(snapshot.data! as List<ServerType>).map(
(final type) => InkWell(
onTap: () {
serverInstallationCubit.setServerType(type);
},
child: Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
type.title,
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 8),
Text(
'cores: ${type.cores.toString()}',
style: Theme.of(context).textTheme.bodySmall,
),
const SizedBox(height: 8),
Text(
'ram: ${type.ram.toString()}',
style: Theme.of(context).textTheme.bodySmall,
),
const SizedBox(height: 8),
Text(
'disk: ${type.disk.gibibyte.toString()}',
style: Theme.of(context).textTheme.bodySmall,
),
const SizedBox(height: 8),
Text(
'price: ${type.price.value.toString()} ${type.price.currency}',
style: Theme.of(context).textTheme.bodySmall,
),
],
(final type) => SizedBox(
width: double.infinity,
child: InkWell(
onTap: () {
serverInstallationCubit.setServerType(type);
},
child: Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
type.title,
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.toString()} ${type.price.currency}'
],
),
style:
Theme.of(context).textTheme.bodyLarge,
),
],
),
],
),
),
),
),
),
),
const SizedBox(height: 24),
const SizedBox(height: 16),
InfoBox(text: 'initializing.choose_server_type_notice'.tr()),
],
);
} else {

View File

@ -1,4 +1,5 @@
import 'package:selfprivacy/logic/models/json/dns_records.dart';
import 'package:url_launcher/url_launcher.dart';
enum DnsRecordsCategory {
services,
@ -133,3 +134,32 @@ DnsRecord? extractDkimRecord(final List<DnsRecord> records) {
return dkimRecord;
}
String getHostnameFromDomain(final String domain) {
// Replace all non-alphanumeric characters with an underscore
String hostname =
domain.split('.')[0].replaceAll(RegExp(r'[^a-zA-Z0-9]'), '-');
if (hostname.endsWith('-')) {
hostname = hostname.substring(0, hostname.length - 1);
}
if (hostname.startsWith('-')) {
hostname = hostname.substring(1);
}
if (hostname.isEmpty) {
hostname = 'selfprivacy-server';
}
return hostname;
}
void launchURL(final url) async {
try {
final Uri uri = Uri.parse(url);
await launchUrl(
uri,
mode: LaunchMode.externalApplication,
);
} catch (e) {
print(e);
}
}

View File

@ -6,17 +6,17 @@
#include "generated_plugin_registrant.h"
#include <dynamic_color/dynamic_color_plugin.h>
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
#include <gtk_theme_fl/gtk_theme_fl_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) dynamic_color_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "DynamicColorPlugin");
dynamic_color_plugin_register_with_registrar(dynamic_color_registrar);
g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar);
g_autoptr(FlPluginRegistrar) gtk_theme_fl_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "GtkThemeFlPlugin");
gtk_theme_fl_plugin_register_with_registrar(gtk_theme_fl_registrar);
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);

View File

@ -3,8 +3,8 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
dynamic_color
flutter_secure_storage_linux
gtk_theme_fl
url_launcher_linux
)

View File

@ -308,7 +308,7 @@ packages:
name: dynamic_color
url: "https://pub.dartlang.org"
source: hosted
version: "1.4.0"
version: "1.5.4"
easy_localization:
dependency: "direct main"
description:
@ -602,13 +602,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
gtk_theme_fl:
dependency: "direct main"
description:
name: gtk_theme_fl
url: "https://pub.dartlang.org"
source: hosted
version: "0.0.1"
hive:
dependency: "direct main"
description:
@ -764,7 +757,7 @@ packages:
source: hosted
version: "0.12.12"
material_color_utilities:
dependency: transitive
dependency: "direct main"
description:
name: material_color_utilities
url: "https://pub.dartlang.org"
@ -1188,20 +1181,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.1"
system_theme:
dependency: "direct main"
description:
name: system_theme
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
system_theme_web:
dependency: transitive
description:
name: system_theme_web
url: "https://pub.dartlang.org"
source: hosted
version: "0.0.2"
term_glyph:
dependency: transitive
description:
@ -1414,4 +1393,4 @@ packages:
version: "3.1.1"
sdks:
dart: ">=2.17.0 <3.0.0"
flutter: ">=3.0.0"
flutter: ">=3.3.0"

View File

@ -1,7 +1,7 @@
name: selfprivacy
description: selfprivacy.org
publish_to: 'none'
version: 0.7.0+16
version: 0.8.0+17
environment:
sdk: '>=2.17.0 <3.0.0'
@ -14,7 +14,7 @@ dependencies:
cubit_form: ^2.0.1
device_info_plus: ^4.0.1
dio: ^4.0.4
dynamic_color: ^1.4.0
dynamic_color: ^1.5.4
easy_localization: ^3.0.0
either_option: ^2.0.1-dev.1
equatable: ^2.0.3
@ -30,7 +30,6 @@ dependencies:
graphql: ^5.1.1
graphql_codegen: ^0.10.2
graphql_flutter: ^5.1.0
gtk_theme_fl: ^0.0.1
hive: ^2.2.3
hive_flutter: ^1.1.0
http: ^0.13.5
@ -38,6 +37,7 @@ dependencies:
ionicons: ^0.1.2
json_annotation: ^4.6.0
local_auth: ^2.0.2
material_color_utilities: ^0.1.5
modal_bottom_sheet: ^2.0.1
nanoid: ^1.0.0
package_info: ^2.0.2
@ -45,7 +45,6 @@ dependencies:
provider: ^6.0.2
pub_semver: ^2.1.1
share_plus: ^4.0.4
system_theme: ^2.0.0
timezone: ^0.8.0
url_launcher: ^6.0.20
wakelock: ^0.6.1+1

View File

@ -9,7 +9,6 @@
#include <connectivity_plus_windows/connectivity_plus_windows_plugin.h>
#include <dynamic_color/dynamic_color_plugin_c_api.h>
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
#include <system_theme/system_theme_plugin.h>
#include <url_launcher_windows/url_launcher_windows.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
@ -19,8 +18,6 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("DynamicColorPluginCApi"));
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
SystemThemePluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("SystemThemePlugin"));
UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
}

View File

@ -6,7 +6,6 @@ list(APPEND FLUTTER_PLUGIN_LIST
connectivity_plus_windows
dynamic_color
flutter_secure_storage_windows
system_theme
url_launcher_windows
)