Merge pull request 'New Year release party 🎄🎉 (SP 0.8.0)' (#157) from release-party into master

Reviewed-on: kherel/selfprivacy.org.app#157
Reviewed-by: NaiJi  <naiji@udongein.xyz>
pull/158/head^2
Inex Code 2022-12-31 07:40:45 +02:00
commit dcb265b9f4
27 changed files with 3790 additions and 568 deletions

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

@ -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",
@ -79,8 +80,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,20 +275,45 @@
"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 a server",
"select_provider": "Select your provider",
"place_where_data": "A place where your data and SelfPrivacy services will reside:",
"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": "Autorize 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_cloudflare": "Connect CloudFlare",
"select_dns": "Now let's select a DNS provider",
"select_dns_text": "",
"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",
@ -444,4 +476,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": "Ваши личные, приватные и независимые сервисы.",
@ -80,7 +81,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,19 +275,43 @@
"no_ssh_notice": "Для этого пользователя созданы только SSH и Email аккаунты. Единая авторизация для всех сервисов ещё не реализована."
},
"initializing": {
"connect_to_server": "Подключите сервер",
"place_where_data": "Здесь будут жить ваши данные и SelfPrivacy-сервисы:",
"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_cloudflare": "Подключите CloudFlare",
"manage_domain_dns": "Для управления DNS вашего домена",
"use_this_domain": "Используем этот домен?",
"use_this_domain_text": "Указанный вами токен даёт контроль над этим доменом",
"cloudflare_api_token": "CloudFlare API ключ",
"connect_backblaze_storage": "Подключите облачное хранилище Backblaze",
"no_connected_domains": "На данный момент подлюченных доменов нет",

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

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

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

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

@ -1,17 +1,12 @@
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';
@ -21,14 +16,6 @@ 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});
@ -54,6 +41,10 @@ class _ServicesPageState extends State<ServicesPage> {
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 +62,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 +88,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 +122,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 +130,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(
@ -215,22 +179,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

@ -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';
@ -56,88 +55,91 @@ 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)
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(),
),
);
},
),
)
],
),
)
],
),
],
),
),
],
),
),
),
@ -189,15 +191,16 @@ class InitializingPage extends StatelessWidget {
builder: (final context) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.asset(
'assets/images/logos/cloudflare.png',
width: 150,
Text(
'${'initializing.connect_to_server_provider'.tr()}Cloudflare',
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 10),
BrandText.h2('initializing.connect_cloudflare'.tr()),
const SizedBox(height: 10),
BrandText.body2('initializing.manage_domain_dns'.tr()),
const Spacer(),
const SizedBox(height: 16),
Text(
'initializing.manage_domain_dns'.tr(),
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 32),
CubitFormTextField(
formFieldCubit: context.read<DnsProviderFormCubit>().apiKey,
textAlign: TextAlign.center,
@ -206,7 +209,7 @@ class InitializingPage extends StatelessWidget {
hintText: 'initializing.cloudflare_api_token'.tr(),
),
),
const Spacer(),
const SizedBox(height: 32),
BrandButton.rised(
onPressed: () =>
context.read<DnsProviderFormCubit>().trySubmit(),
@ -236,14 +239,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,
@ -252,7 +252,7 @@ class InitializingPage extends StatelessWidget {
hintText: 'KeyID',
),
),
const Spacer(),
const SizedBox(height: 16),
CubitFormTextField(
formFieldCubit:
context.read<BackblazeFormCubit>().applicationKey,
@ -262,7 +262,7 @@ class InitializingPage extends StatelessWidget {
hintText: 'Master Application Key',
),
),
const Spacer(),
const SizedBox(height: 32),
BrandButton.rised(
onPressed: formCubitState.isSubmitting
? null
@ -292,91 +292,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,
),
],
),
);
},
),
@ -393,12 +387,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(),
@ -406,7 +404,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,
@ -415,7 +413,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) {
@ -446,7 +444,7 @@ class InitializingPage extends StatelessWidget {
);
},
),
const Spacer(),
const SizedBox(height: 32),
BrandButton.rised(
onPressed: formCubitState.isSubmitting
? null
@ -466,11 +464,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,
@ -505,55 +508,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/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:url_launcher/url_launcher.dart';
class ServerProviderPicker extends StatefulWidget {
const ServerProviderPicker({
@ -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,189 @@ 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.place_where_data'.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()),
],
),
);
}
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,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

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