chore: Merge digital-ocean-dns into refactoring

pull/213/head
NaiJi ✨ 2023-04-12 05:32:56 -03:00
commit 15c0586ab3
117 changed files with 4496 additions and 3059 deletions

View File

@ -14,3 +14,6 @@ max_line_length = 150
[*.md]
trim_trailing_whitespace = false
[*.json]
indent_size = 4

View File

@ -29,16 +29,16 @@ linter:
# producing the lint.
rules:
avoid_print: false # Uncomment to disable the `avoid_print` rule
prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
always_use_package_imports: true
no_adjacent_strings_in_list: true
unnecessary_statements: true
always_declare_return_types: true
always_put_required_named_parameters_first: true
always_put_control_body_on_new_line: true
always_put_required_named_parameters_first: true
always_use_package_imports: true
avoid_escaping_inner_quotes: true
avoid_setters_without_getters: true
collection_methods_unrelated_type: true
combinators_ordering: true
eol_at_end_of_file: true
no_adjacent_strings_in_list: true
prefer_constructors_over_static_methods: true
prefer_expression_function_bodies: true
prefer_final_in_for_each: true
@ -48,12 +48,18 @@ linter:
prefer_if_elements_to_conditional_expressions: true
prefer_mixin: true
prefer_null_aware_method_calls: true
prefer_single_quotes: true
require_trailing_commas: true
sized_box_shrink_expand: true
sort_constructors_first: true
unawaited_futures: true
unnecessary_await_in_return: true
unnecessary_null_aware_operator_on_extension_on_nullable: true
unnecessary_null_checks: true
unnecessary_parenthesis: true
unnecessary_statements: true
unnecessary_to_list_in_spreads: true
unreachable_from_main: true
use_enums: true
use_if_null_to_convert_nulls_to_bools: true
use_is_even_rather_than_modulo: true
@ -61,6 +67,7 @@ linter:
use_named_constants: true
use_setters_to_change_properties: true
use_string_buffers: true
use_string_in_part_of_directives: true
use_super_parameters: true
use_to_and_as_if_applicable: true

View File

@ -2,6 +2,7 @@
"test": "en-test",
"locale": "en",
"basis": {
"app_name": "SelfPrivacy",
"providers": "Providers",
"providers_title": "Your Data Center",
"select": "Select",
@ -46,7 +47,8 @@
},
"console_page": {
"title": "Console",
"waiting": "Waiting for initialization…"
"waiting": "Waiting for initialization…",
"copy": "Copy"
},
"about_us_page": {
"title": "About us"
@ -59,8 +61,11 @@
},
"application_settings": {
"title": "Application settings",
"system_dark_theme_title": "System default theme",
"system_dark_theme_description": "Use light or dark theme depending on system settings",
"dark_theme_title": "Dark theme",
"dark_theme_description": "Switch your application theme",
"dangerous_settings": "Dangerous settings",
"reset_config_title": "Reset application config",
"reset_config_description": "Reset api keys and root user",
"delete_server_title": "Delete server",
@ -251,6 +256,7 @@
"subtitle": "Private VPN server"
},
"users": {
"details_title": "User details",
"add_new_user": "Add a first user",
"new_user": "New user",
"delete_user": "Delete user",
@ -335,7 +341,20 @@
"create_master_account": "Create master account",
"enter_username_and_password": "Enter username and strong password",
"finish": "Everything is initialized",
"checks": "Checks have been completed \n{} out of {}"
"checks": "Checks have been completed \n{} out of {}",
"steps": {
"hosting": "Hosting",
"server_type": "Server type",
"dns_provider": "DNS provider",
"backups_provider": "Backups",
"domain": "Domain",
"master_account": "Master account",
"server": "Server",
"dns_setup": "DNS setup",
"nixos_installation": "NixOS installation",
"server_reboot": "Server reboot",
"final_checks": "Final checks"
}
},
"recovering": {
"generic_error": "Operation failed, please try again.",
@ -479,5 +498,19 @@
"root_name": "Cannot be 'root'",
"length_not_equal": "Length is [], should be {}",
"length_longer": "Length is [], should be shorter than or equal to {}"
},
"support": {
"title": "SelfPrivacy Support"
},
"developer_settings": {
"title": "Developer settings",
"subtitle": "These settings are for debugging only. Don't change them unless you know what you're doing.",
"server_setup": "Server setup",
"use_staging_acme": "Use staging ACME server",
"use_staging_acme_description": "Rebuild your app to change this value.",
"routing": "App routing",
"reset_onboarding": "Reset onboarding switch",
"reset_onboarding_description": "Reset onboarding switch to show onboarding screen again",
"cubit_statuses": "Cubit loading statuses"
}
}

View File

@ -12,6 +12,7 @@ import 'package:selfprivacy/logic/cubit/providers/providers_cubit.dart';
import 'package:selfprivacy/logic/cubit/server_jobs/server_jobs_cubit.dart';
import 'package:selfprivacy/logic/cubit/server_volumes/server_volume_cubit.dart';
import 'package:selfprivacy/logic/cubit/services/services_cubit.dart';
import 'package:selfprivacy/logic/cubit/support_system/support_system_cubit.dart';
import 'package:selfprivacy/logic/cubit/users/users_cubit.dart';
import 'package:selfprivacy/logic/cubit/provider_volumes/provider_volume_cubit.dart';
@ -23,7 +24,9 @@ class BlocAndProviderConfig extends StatelessWidget {
@override
Widget build(final BuildContext context) {
const isDark = false;
const isAutoDark = true;
final serverInstallationCubit = ServerInstallationCubit()..load();
final supportSystemCubit = SupportSystemCubit();
final usersCubit = UsersCubit(serverInstallationCubit);
final servicesCubit = ServicesCubit(serverInstallationCubit);
final backupsCubit = BackupsCubit(serverInstallationCubit);
@ -41,9 +44,13 @@ class BlocAndProviderConfig extends StatelessWidget {
BlocProvider(
create: (final _) => AppSettingsCubit(
isDarkModeOn: isDark,
isAutoDarkModeOn: isAutoDark,
isOnboardingShowing: true,
)..load(),
),
BlocProvider(
create: (final _) => supportSystemCubit,
),
BlocProvider(
create: (final _) => serverInstallationCubit,
lazy: false,

View File

@ -2,53 +2,16 @@ import 'package:flutter/material.dart';
class BrandColors {
static const Color blue = Color(0xFF093CEF);
static const Color white = Colors.white;
static const Color black = Colors.black;
static const Color gray1 = Color(0xFF555555);
static const Color gray2 = Color(0xFF7C7C7C);
static const Color gray3 = Color(0xFFFAFAFA);
static const Color gray4 = Color(0xFFDDDDDD);
static const Color gray5 = Color(0xFFEDEEF1);
static Color gray6 = const Color(0xFF181818).withOpacity(0.7);
static const Color grey7 = Color(0xFFABABAB);
static const Color red1 = Color(0xFFFA0E0E);
static const Color red2 = Color(0xFFE65527);
static const Color green1 = Color(0xFF00AF54);
static const Color green2 = Color(0xFF0F8849);
static Color get navBackgroundLight => white.withOpacity(0.8);
static Color get navBackgroundDark => black.withOpacity(0.8);
static const List<Color> uninitializedGradientColors = [
Color(0xFF555555),
Color(0xFFABABAB),
];
static const List<Color> stableGradientColors = [
Color(0xFF093CEF),
Color(0xFF14A1CB),
];
static const List<Color> progressGradientColors = [
Color(0xFF093CEF),
Color(0xFF14A1CB),
];
static const List<Color> warningGradientColors = [
Color(0xFFEF4E09),
Color(0xFFEFD135),
];
static const Color primary = blue;
static const Color headlineColor = black;
static const Color inactive = gray2;
static const Color scaffoldBackground = gray3;
static const Color inputInactive = gray4;
static const Color textColor1 = black;
static const Color textColor2 = gray1;
static const Color dividerColor = gray5;
static const Color warning = red1;
}

View File

@ -18,9 +18,6 @@ class HiveConfig {
Hive.registerAdapter(BackblazeCredentialAdapter());
Hive.registerAdapter(BackblazeBucketAdapter());
Hive.registerAdapter(ServerVolumeAdapter());
Hive.registerAdapter(DnsProviderAdapter());
Hive.registerAdapter(ServerProviderAdapter());
Hive.registerAdapter(UserTypeAdapter());
await Hive.openBox(BNames.appSettingsBox);
@ -35,8 +32,8 @@ class HiveConfig {
final Box<User> deprecatedUsers = Hive.box<User>(BNames.usersDeprecated);
if (deprecatedUsers.isNotEmpty) {
final Box<User> users = Hive.box<User>(BNames.usersBox);
users.addAll(deprecatedUsers.values.toList());
deprecatedUsers.clear();
await users.addAll(deprecatedUsers.values.toList());
await deprecatedUsers.clear();
}
await Hive.openBox(BNames.serverInstallationBox, encryptionCipher: cipher);
@ -63,6 +60,9 @@ class BNames {
/// A boolean field of [appSettingsBox] box.
static String isDarkModeOn = 'isDarkModeOn';
/// A boolean field of [appSettingsBox] box.
static String isAutoDarkModeOn = 'isAutoDarkModeOn';
/// A boolean field of [appSettingsBox] box.
static String isOnboardingShowing = 'isOnboardingShowing';

View File

@ -1,80 +0,0 @@
import 'package:flutter/material.dart';
import 'package:selfprivacy/utils/named_font_weight.dart';
import 'package:selfprivacy/config/brand_colors.dart';
const TextStyle defaultTextStyle = TextStyle(
fontSize: 15,
color: BrandColors.textColor1,
);
final TextStyle headline1Style = defaultTextStyle.copyWith(
fontSize: 40,
fontWeight: NamedFontWeight.extraBold,
color: BrandColors.headlineColor,
);
final TextStyle headline2Style = defaultTextStyle.copyWith(
fontSize: 24,
fontWeight: NamedFontWeight.extraBold,
color: BrandColors.headlineColor,
);
final TextStyle onboardingTitle = defaultTextStyle.copyWith(
fontSize: 30,
fontWeight: NamedFontWeight.extraBold,
color: BrandColors.headlineColor,
);
final TextStyle headline3Style = defaultTextStyle.copyWith(
fontSize: 20,
fontWeight: NamedFontWeight.extraBold,
color: BrandColors.headlineColor,
);
final TextStyle headline4Style = defaultTextStyle.copyWith(
fontSize: 18,
fontWeight: NamedFontWeight.medium,
color: BrandColors.headlineColor,
);
final TextStyle headline4UnderlinedStyle = defaultTextStyle.copyWith(
fontSize: 18,
fontWeight: NamedFontWeight.medium,
color: BrandColors.headlineColor,
decoration: TextDecoration.underline,
);
final TextStyle headline5Style = defaultTextStyle.copyWith(
fontSize: 15,
fontWeight: NamedFontWeight.medium,
color: BrandColors.headlineColor.withOpacity(0.8),
);
const TextStyle body1Style = defaultTextStyle;
final TextStyle body2Style = defaultTextStyle.copyWith(
color: BrandColors.textColor2,
);
final TextStyle buttonTitleText = defaultTextStyle.copyWith(
color: BrandColors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
height: 1,
);
final TextStyle mediumStyle =
defaultTextStyle.copyWith(fontSize: 13, height: 1.53);
final TextStyle smallStyle =
defaultTextStyle.copyWith(fontSize: 11, height: 1.45);
const TextStyle progressTextStyleLight = TextStyle(
fontSize: 11,
color: BrandColors.textColor1,
height: 1.7,
);
final TextStyle progressTextStyleDark = progressTextStyleLight.copyWith(
color: BrandColors.white,
);

View File

@ -20,7 +20,13 @@ class RequestLoggingLink extends Link {
final Request request, [
final NextLink? forward,
]) async* {
_logToAppConsole(request);
getIt.get<ConsoleModel>().addMessage(
GraphQlRequestMessage(
operation: request.operation,
variables: request.variables,
context: request.context,
),
);
yield* forward!(request);
}
}
@ -29,7 +35,13 @@ class ResponseLoggingParser extends ResponseParser {
@override
Response parseResponse(final Map<String, dynamic> body) {
final response = super.parseResponse(body);
_logToAppConsole(response);
getIt.get<ConsoleModel>().addMessage(
GraphQlResponseMessage(
data: response.data,
errors: response.errors,
context: response.context,
),
);
return response;
}

View File

@ -65,9 +65,11 @@ class ConsoleInterceptor extends InterceptorsWrapper {
final RequestInterceptorHandler handler,
) async {
addMessage(
Message(
text:
'request-uri: ${options.uri}\nheaders: ${options.headers}\ndata: ${options.data}',
RestApiRequestMessage(
method: options.method,
data: options.data.toString(),
headers: options.headers,
uri: options.uri,
),
);
return super.onRequest(options, handler);
@ -79,9 +81,11 @@ class ConsoleInterceptor extends InterceptorsWrapper {
final ResponseInterceptorHandler handler,
) async {
addMessage(
Message(
text:
'response-uri: ${response.realUri}\ncode: ${response.statusCode}\ndata: ${response.toString()}\n',
RestApiResponseMessage(
method: response.requestOptions.method,
statusCode: response.statusCode,
data: response.data.toString(),
uri: response.realUri,
),
);
return super.onResponse(

View File

@ -15,10 +15,12 @@ part 'app_settings_state.dart';
class AppSettingsCubit extends Cubit<AppSettingsState> {
AppSettingsCubit({
required final bool isDarkModeOn,
required final bool isAutoDarkModeOn,
required final bool isOnboardingShowing,
}) : super(
AppSettingsState(
isDarkModeOn: isDarkModeOn,
isAutoDarkModeOn: isAutoDarkModeOn,
isOnboardingShowing: isOnboardingShowing,
),
);
@ -27,10 +29,12 @@ class AppSettingsCubit extends Cubit<AppSettingsState> {
void load() async {
final bool? isDarkModeOn = box.get(BNames.isDarkModeOn);
final bool? isAutoDarkModeOn = box.get(BNames.isAutoDarkModeOn);
final bool? isOnboardingShowing = box.get(BNames.isOnboardingShowing);
emit(
state.copyWith(
isDarkModeOn: isDarkModeOn,
isAutoDarkModeOn: isAutoDarkModeOn,
isOnboardingShowing: isOnboardingShowing,
),
);
@ -49,9 +53,14 @@ class AppSettingsCubit extends Cubit<AppSettingsState> {
emit(state.copyWith(isDarkModeOn: isDarkModeOn));
}
void turnOffOnboarding() {
box.put(BNames.isOnboardingShowing, false);
void updateAutoDarkMode({required final bool isAutoDarkModeOn}) {
box.put(BNames.isAutoDarkModeOn, isAutoDarkModeOn);
emit(state.copyWith(isAutoDarkModeOn: isAutoDarkModeOn));
}
emit(state.copyWith(isOnboardingShowing: false));
void turnOffOnboarding({final bool isOnboardingShowing = false}) {
box.put(BNames.isOnboardingShowing, isOnboardingShowing);
emit(state.copyWith(isOnboardingShowing: isOnboardingShowing));
}
}

View File

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

View File

@ -15,9 +15,9 @@ class ApiDevicesCubit
@override
void load() async {
if (serverInstallationCubit.state is ServerInstallationFinished) {
_refetch();
}
// if (serverInstallationCubit.state is ServerInstallationFinished) {
_refetch();
// }
}
Future<void> refresh() async {

View File

@ -22,9 +22,6 @@ class DomainSetupCubit extends Cubit<DomainSetupState> {
}
}
@override
Future<void> close() => super.close();
Future<void> saveDomain() async {
assert(state is Loaded, 'wrong state');
final String domainName = (state as Loaded).domain;

View File

@ -36,10 +36,6 @@ class UserFormCubit extends FormCubit {
@override
FutureOr<void> onSubmit() {
print('onSubmit');
print('initialUser: $initialUser');
print('login: ${login.state.value}');
print('password: ${password.state.value}');
if (initialUser == null) {
final User user = User(
login: login.state.value,

View File

@ -1,3 +1,5 @@
import 'dart:async';
import 'package:easy_localization/easy_localization.dart';
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server_api.dart';
@ -20,7 +22,7 @@ class ApiProviderVolumeCubit
@override
Future<void> load() async {
if (serverInstallationCubit.state is ServerInstallationFinished) {
_refetch();
unawaited(_refetch());
}
}
@ -31,7 +33,7 @@ class ApiProviderVolumeCubit
Future<void> refresh() async {
emit(const ApiProviderVolumeState([], LoadingStatus.refreshing, false));
_refetch();
unawaited(_refetch());
}
Future<void> _refetch() async {
@ -56,14 +58,14 @@ class ApiProviderVolumeCubit
await ApiController.currentVolumeProviderApiFactory!
.getVolumeProvider()
.attachVolume(volume.providerVolume!, server.id);
refresh();
unawaited(refresh());
}
Future<void> detachVolume(final DiskVolume volume) async {
await ApiController.currentVolumeProviderApiFactory!
.getVolumeProvider()
.detachVolume(volume.providerVolume!);
refresh();
unawaited(refresh());
}
Future<bool> resizeVolume(
@ -125,14 +127,14 @@ class ApiProviderVolumeCubit
await Future.delayed(const Duration(seconds: 10));
await ServerApi().mountVolume(volume!.name);
refresh();
unawaited(refresh());
}
Future<void> deleteVolume(final DiskVolume volume) async {
await ApiController.currentVolumeProviderApiFactory!
.getVolumeProvider()
.deleteVolume(volume.providerVolume!);
refresh();
unawaited(refresh());
}
@override

View File

@ -1,3 +1,5 @@
import 'dart:async';
import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server_api.dart';
import 'package:selfprivacy/logic/common_enum/common_enum.dart';
import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart';
@ -14,21 +16,21 @@ class RecoveryKeyCubit
@override
void load() async {
if (serverInstallationCubit.state is ServerInstallationFinished) {
final RecoveryKeyStatus? status = await _getRecoveryKeyStatus();
if (status == null) {
emit(state.copyWith(loadingStatus: LoadingStatus.error));
} else {
emit(
state.copyWith(
status: status,
loadingStatus: LoadingStatus.success,
),
);
}
// if (serverInstallationCubit.state is ServerInstallationFinished) {
final RecoveryKeyStatus? status = await _getRecoveryKeyStatus();
if (status == null) {
emit(state.copyWith(loadingStatus: LoadingStatus.error));
} else {
emit(state.copyWith(loadingStatus: LoadingStatus.uninitialized));
emit(
state.copyWith(
status: status,
loadingStatus: LoadingStatus.success,
),
);
}
// } else {
// emit(state.copyWith(loadingStatus: LoadingStatus.uninitialized));
// }
}
Future<RecoveryKeyStatus?> _getRecoveryKeyStatus() async {
@ -60,7 +62,7 @@ class RecoveryKeyCubit
final GenericResult<String> response =
await api.generateRecoveryToken(expirationDate, numberOfUses);
if (response.success) {
refresh();
unawaited(refresh());
return response.data;
} else {
throw GenerationError(response.message ?? 'Unknown error');

View File

@ -186,7 +186,7 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
void setDnsApiToken(final String dnsApiToken) async {
if (state is ServerInstallationRecovery) {
setAndValidateCloudflareToken(dnsApiToken);
await setAndValidateCloudflareToken(dnsApiToken);
return;
}
await repository.setDnsApiToken(dnsApiToken);

View File

@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:io';
import 'package:basic_utils/basic_utils.dart';

View File

@ -24,7 +24,7 @@ class ApiServerVolumeCubit
@override
Future<void> load() async {
if (serverInstallationCubit.state is ServerInstallationFinished) {
reload();
unawaited(reload());
}
}

View File

@ -53,7 +53,7 @@ class ServicesCubit extends ServerInstallationDependendCubit<ServicesState> {
}
await Future.delayed(const Duration(seconds: 2));
reload();
unawaited(reload());
await Future.delayed(const Duration(seconds: 10));
emit(
state.copyWith(
@ -62,7 +62,7 @@ class ServicesCubit extends ServerInstallationDependendCubit<ServicesState> {
.toList(),
),
);
reload();
unawaited(reload());
}
Future<void> moveService(

View File

@ -0,0 +1,19 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
part 'support_system_state.dart';
class SupportSystemCubit extends Cubit<SupportSystemState> {
SupportSystemCubit() : super(const SupportSystemState('about'));
void showArticle({
required final String article,
final BuildContext? context,
}) {
emit(SupportSystemState(article));
if (context != null) {
Scaffold.of(context).openEndDrawer();
}
}
}

View File

@ -0,0 +1,12 @@
part of 'support_system_cubit.dart';
class SupportSystemState extends Equatable {
const SupportSystemState(
this.currentArticle,
);
final String currentArticle;
@override
List<Object> get props => [currentArticle];
}

View File

@ -1,3 +1,5 @@
import 'dart:async';
import 'package:easy_localization/easy_localization.dart';
import 'package:hive/hive.dart';
import 'package:selfprivacy/config/get_it_config.dart';
@ -39,7 +41,7 @@ class UsersCubit extends ServerInstallationDependendCubit<UsersState> {
);
}
refresh();
unawaited(refresh());
}
Future<void> refresh() async {

View File

@ -1,16 +1,19 @@
import 'package:flutter/material.dart';
import 'package:selfprivacy/config/brand_colors.dart';
import 'package:selfprivacy/config/text_themes.dart';
class NavigationService {
final GlobalKey<ScaffoldMessengerState> scaffoldMessengerKey =
GlobalKey<ScaffoldMessengerState>();
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
NavigatorState? get navigator => navigatorKey.currentState;
void showPopUpDialog(final AlertDialog dialog) {
final BuildContext context = navigatorKey.currentState!.overlay!.context;
final BuildContext? context = navigatorKey.currentContext;
if (context == null) {
showSnackBar(
'Could not show dialog. This should not happen, please report this.',
);
return;
}
showDialog(
context: context,
@ -21,8 +24,7 @@ class NavigationService {
void showSnackBar(final String text) {
final ScaffoldMessengerState state = scaffoldMessengerKey.currentState!;
final SnackBar snack = SnackBar(
backgroundColor: BrandColors.black.withOpacity(0.8),
content: Text(text, style: buttonTitleText),
content: Text(text),
duration: const Duration(seconds: 2),
);
state.showSnackBar(snack);

View File

@ -23,8 +23,8 @@ class ServerHostingDetailsAdapter extends TypeAdapter<ServerHostingDetails> {
volume: fields[4] as ServerVolume,
apiToken: fields[5] as String,
provider: fields[6] == null
? ServerProvider.hetzner
: fields[6] as ServerProvider,
? ServerProviderType.hetzner
: fields[6] as ServerProviderType,
startTime: fields[2] as DateTime?,
);
}
@ -109,34 +109,34 @@ class ServerVolumeAdapter extends TypeAdapter<ServerVolume> {
typeId == other.typeId;
}
class ServerProviderAdapter extends TypeAdapter<ServerProvider> {
class ServerProviderTypeAdapter extends TypeAdapter<ServerProviderType> {
@override
final int typeId = 101;
@override
ServerProvider read(BinaryReader reader) {
ServerProviderType read(BinaryReader reader) {
switch (reader.readByte()) {
case 0:
return ServerProvider.unknown;
return ServerProviderType.unknown;
case 1:
return ServerProvider.hetzner;
return ServerProviderType.hetzner;
case 2:
return ServerProvider.digitalOcean;
return ServerProviderType.digitalOcean;
default:
return ServerProvider.unknown;
return ServerProviderType.unknown;
}
}
@override
void write(BinaryWriter writer, ServerProvider obj) {
void write(BinaryWriter writer, ServerProviderType obj) {
switch (obj) {
case ServerProvider.unknown:
case ServerProviderType.unknown:
writer.writeByte(0);
break;
case ServerProvider.hetzner:
case ServerProviderType.hetzner:
writer.writeByte(1);
break;
case ServerProvider.digitalOcean:
case ServerProviderType.digitalOcean:
writer.writeByte(2);
break;
}
@ -148,7 +148,7 @@ class ServerProviderAdapter extends TypeAdapter<ServerProvider> {
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is ServerProviderAdapter &&
other is ServerProviderTypeAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@ -19,8 +19,9 @@ class ServerDomainAdapter extends TypeAdapter<ServerDomain> {
return ServerDomain(
domainName: fields[0] as String,
zoneId: fields[1] as String,
provider:
fields[2] == null ? DnsProvider.cloudflare : fields[2] as DnsProvider,
provider: fields[2] == null
? DnsProviderType.cloudflare
: fields[2] as DnsProviderType,
);
}
@ -47,34 +48,34 @@ class ServerDomainAdapter extends TypeAdapter<ServerDomain> {
typeId == other.typeId;
}
class DnsProviderAdapter extends TypeAdapter<DnsProvider> {
class DnsProviderTypeAdapter extends TypeAdapter<DnsProviderType> {
@override
final int typeId = 100;
@override
DnsProvider read(BinaryReader reader) {
DnsProviderType read(BinaryReader reader) {
switch (reader.readByte()) {
case 0:
return DnsProvider.unknown;
return DnsProviderType.unknown;
case 1:
return DnsProvider.cloudflare;
return DnsProviderType.cloudflare;
case 2:
return DnsProvider.digitalOcean;
return DnsProviderType.digitalOcean;
default:
return DnsProvider.unknown;
return DnsProviderType.unknown;
}
}
@override
void write(BinaryWriter writer, DnsProvider obj) {
void write(BinaryWriter writer, DnsProviderType obj) {
switch (obj) {
case DnsProvider.unknown:
case DnsProviderType.unknown:
writer.writeByte(0);
break;
case DnsProvider.cloudflare:
case DnsProviderType.cloudflare:
writer.writeByte(1);
break;
case DnsProvider.digitalOcean:
case DnsProviderType.digitalOcean:
writer.writeByte(2);
break;
}
@ -86,7 +87,7 @@ class DnsProviderAdapter extends TypeAdapter<DnsProvider> {
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is DnsProviderAdapter &&
other is DnsProviderTypeAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

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

View File

@ -5,9 +5,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:selfprivacy/config/brand_colors.dart';
import 'package:selfprivacy/config/hive_config.dart';
import 'package:selfprivacy/theming/factory/app_theme_factory.dart';
import 'package:selfprivacy/ui/pages/setup/initializing/initializing.dart';
import 'package:selfprivacy/ui/pages/onboarding/onboarding.dart';
import 'package:selfprivacy/ui/pages/root_route.dart';
import 'package:selfprivacy/ui/router/router.dart';
import 'package:wakelock/wakelock.dart';
import 'package:timezone/data/latest.dart' as tz;
@ -20,7 +18,7 @@ import 'package:selfprivacy/logic/cubit/app_settings/app_settings_cubit.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await HiveConfig.init();
await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
// await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
try {
/// Wakelock support for Linux
@ -43,21 +41,20 @@ void main() async {
fallbackColor: BrandColors.primary,
);
BlocOverrides.runZoned(
() => runApp(
Localization(
child: MyApp(
lightThemeData: lightThemeData,
darkThemeData: darkThemeData,
),
Bloc.observer = SimpleBlocObserver();
runApp(
Localization(
child: SelfprivacyApp(
lightThemeData: lightThemeData,
darkThemeData: darkThemeData,
),
),
blocObserver: SimpleBlocObserver(),
);
}
class MyApp extends StatelessWidget {
const MyApp({
class SelfprivacyApp extends StatelessWidget {
SelfprivacyApp({
required this.lightThemeData,
required this.darkThemeData,
super.key,
@ -66,42 +63,42 @@ class MyApp extends StatelessWidget {
final ThemeData lightThemeData;
final ThemeData darkThemeData;
final _appRouter = RootRouter(getIt.get<NavigationService>().navigatorKey);
@override
Widget build(final BuildContext context) => Localization(
child: AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle.light, // Manually changing appbar color
child: BlocAndProviderConfig(
child: BlocBuilder<AppSettingsCubit, AppSettingsState>(
builder: (
final BuildContext context,
final AppSettingsState appSettings,
) =>
MaterialApp(
scaffoldMessengerKey:
getIt.get<NavigationService>().scaffoldMessengerKey,
navigatorKey: getIt.get<NavigationService>().navigatorKey,
localizationsDelegates: context.localizationDelegates,
supportedLocales: context.supportedLocales,
locale: context.locale,
debugShowCheckedModeBanner: false,
title: 'SelfPrivacy',
theme: lightThemeData,
darkTheme: darkThemeData,
themeMode:
appSettings.isDarkModeOn ? ThemeMode.dark : ThemeMode.light,
home: appSettings.isOnboardingShowing
? const OnboardingPage(nextPage: InitializingPage())
: const RootPage(),
builder: (final BuildContext context, final Widget? widget) {
Widget error = const Text('...rendering error...');
if (widget is Scaffold || widget is Navigator) {
error = Scaffold(body: Center(child: error));
}
ErrorWidget.builder =
(final FlutterErrorDetails errorDetails) => error;
return widget!;
},
),
child: BlocAndProviderConfig(
child: BlocBuilder<AppSettingsCubit, AppSettingsState>(
builder: (
final BuildContext context,
final AppSettingsState appSettings,
) =>
MaterialApp.router(
routeInformationParser: _appRouter.defaultRouteParser(),
routerDelegate: _appRouter.delegate(),
scaffoldMessengerKey:
getIt.get<NavigationService>().scaffoldMessengerKey,
localizationsDelegates: context.localizationDelegates,
supportedLocales: context.supportedLocales,
locale: context.locale,
debugShowCheckedModeBanner: false,
title: 'SelfPrivacy',
theme: lightThemeData,
darkTheme: darkThemeData,
themeMode: appSettings.isAutoDarkModeOn
? ThemeMode.system
: appSettings.isDarkModeOn
? ThemeMode.dark
: ThemeMode.light,
builder: (final BuildContext context, final Widget? widget) {
Widget error = const Text('...rendering error...');
if (widget is Scaffold || widget is Navigator) {
error = Scaffold(body: Center(child: error));
}
ErrorWidget.builder =
(final FlutterErrorDetails errorDetails) => error;
return widget!;
},
),
),
),

View File

@ -1,13 +0,0 @@
import 'package:flutter/material.dart';
class BrandAlert extends AlertDialog {
BrandAlert({
super.key,
final String? title,
final String? contentText,
super.actions,
}) : super(
title: title != null ? Text(title) : null,
content: title != null ? Text(contentText!) : null,
);
}

View File

@ -1,56 +0,0 @@
import 'package:flutter/material.dart';
import 'package:selfprivacy/config/brand_colors.dart';
class BrandBottomSheet extends StatelessWidget {
const BrandBottomSheet({
required this.child,
super.key,
this.isExpended = false,
});
final Widget child;
final bool isExpended;
@override
Widget build(final BuildContext context) {
final double mainHeight = MediaQuery.of(context).size.height -
MediaQuery.of(context).padding.top -
300;
late Widget innerWidget;
if (isExpended) {
innerWidget = Scaffold(
body: child,
);
} else {
final ThemeData themeData = Theme.of(context);
innerWidget = Material(
color: themeData.scaffoldBackgroundColor,
child: IntrinsicHeight(child: child),
);
}
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,
),
),
],
);
}
}

View File

@ -1,55 +0,0 @@
import 'package:flutter/material.dart';
class BrandCards {
static Widget big({required final Widget child}) => _BrandCard(
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 15,
),
shadow: bigShadow,
borderRadius: BorderRadius.circular(20),
child: child,
);
static Widget small({required final Widget child}) => _BrandCard(
padding: const EdgeInsets.symmetric(
horizontal: 15,
vertical: 10,
),
shadow: bigShadow,
borderRadius: BorderRadius.circular(10),
child: child,
);
}
class _BrandCard extends StatelessWidget {
const _BrandCard({
required this.child,
required this.padding,
required this.shadow,
required this.borderRadius,
});
final Widget child;
final EdgeInsets padding;
final List<BoxShadow> shadow;
final BorderRadius borderRadius;
@override
Widget build(final BuildContext context) => Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: borderRadius,
boxShadow: shadow,
),
padding: padding,
child: child,
);
}
final List<BoxShadow> bigShadow = [
BoxShadow(
offset: const Offset(0, 4),
blurRadius: 8,
color: Colors.black.withOpacity(.08),
)
];

View File

@ -25,5 +25,8 @@ class BrandHeader extends StatelessWidget {
onBackButtonPressed ?? () => Navigator.of(context).pop(),
)
: null,
actions: const [
SizedBox.shrink(),
],
);
}

View File

@ -1,147 +0,0 @@
import 'package:flutter/material.dart';
import 'package:selfprivacy/ui/components/pre_styled_buttons/flash_fab.dart';
import 'package:selfprivacy/ui/helpers/widget_size.dart';
class BrandHeroScreen extends StatelessWidget {
const BrandHeroScreen({
required this.children,
super.key,
this.hasBackButton = true,
this.hasFlashButton = true,
this.heroIcon,
this.heroIconWidget,
this.heroTitle = '',
this.heroSubtitle,
this.onBackButtonPressed,
});
final List<Widget> children;
final bool hasBackButton;
final bool hasFlashButton;
final IconData? heroIcon;
final Widget? heroIconWidget;
final String heroTitle;
final String? heroSubtitle;
final VoidCallback? onBackButtonPressed;
@override
Widget build(final BuildContext context) {
final Widget heroIconWidget = this.heroIconWidget ??
Icon(
heroIcon ?? Icons.help,
size: 48.0,
color: Theme.of(context).colorScheme.onBackground,
);
final bool hasHeroIcon = heroIcon != null || this.heroIconWidget != null;
return Scaffold(
floatingActionButton: hasFlashButton ? const BrandFab() : null,
body: CustomScrollView(
slivers: [
HeroSliverAppBar(
heroTitle: heroTitle,
hasHeroIcon: hasHeroIcon,
hasBackButton: hasBackButton,
onBackButtonPressed: onBackButtonPressed,
heroIconWidget: heroIconWidget,
),
if (heroSubtitle != null)
SliverPadding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 4.0,
),
sliver: SliverList(
delegate: SliverChildListDelegate([
Text(
heroSubtitle!,
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: Theme.of(context).colorScheme.onBackground,
),
textAlign: hasHeroIcon ? TextAlign.center : TextAlign.start,
),
]),
),
),
SliverPadding(
padding: const EdgeInsets.all(16.0),
sliver: SliverList(
delegate: SliverChildListDelegate(children),
),
),
],
),
);
}
}
class HeroSliverAppBar extends StatefulWidget {
const HeroSliverAppBar({
required this.heroTitle,
required this.hasHeroIcon,
required this.hasBackButton,
required this.onBackButtonPressed,
required this.heroIconWidget,
super.key,
});
final String heroTitle;
final bool hasHeroIcon;
final bool hasBackButton;
final VoidCallback? onBackButtonPressed;
final Widget heroIconWidget;
@override
State<HeroSliverAppBar> createState() => _HeroSliverAppBarState();
}
class _HeroSliverAppBarState extends State<HeroSliverAppBar> {
Size _size = Size.zero;
@override
Widget build(final BuildContext context) => SliverAppBar(
expandedHeight:
widget.hasHeroIcon ? 148.0 + _size.height : 72.0 + _size.height,
primary: true,
pinned: true,
stretch: true,
leading: widget.hasBackButton
? IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: widget.onBackButtonPressed ??
() => Navigator.of(context).pop(),
)
: null,
flexibleSpace: FlexibleSpaceBar(
title: LayoutBuilder(
builder: (final context, final constraints) => SizedBox(
width: constraints.maxWidth - 72.0,
child: WidgetSize(
onChange: (final Size size) => setState(() => _size = size),
child: Text(
widget.heroTitle,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: Theme.of(context).colorScheme.onSurface,
),
overflow: TextOverflow.fade,
textAlign: TextAlign.center,
),
),
),
),
expandedTitleScale: 1.2,
centerTitle: true,
collapseMode: CollapseMode.pin,
titlePadding: const EdgeInsets.only(
bottom: 12.0,
top: 16.0,
),
background: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
const SizedBox(height: 72.0),
if (widget.hasHeroIcon) widget.heroIconWidget,
],
),
),
);
}

View File

@ -27,14 +27,14 @@ class BrandLinearIndicator extends StatelessWidget {
alignment: Alignment.centerLeft,
child: AnimatedSlide(
duration: const Duration(milliseconds: 400),
curve: Curves.easeInOut,
curve: Curves.easeInOutCubicEmphasized,
offset: Offset(
-(1 - value),
0,
),
child: AnimatedContainer(
duration: const Duration(milliseconds: 400),
curve: Curves.easeInOut,
curve: Curves.easeInOutCubicEmphasized,
width: constraints.maxWidth,
decoration: BoxDecoration(
color: color,

View File

@ -2,8 +2,6 @@ import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:flutter/services.dart' show rootBundle;
import 'package:easy_localization/easy_localization.dart';
import 'package:selfprivacy/config/brand_colors.dart';
import 'package:selfprivacy/config/text_themes.dart';
import 'package:url_launcher/url_launcher_string.dart';
class BrandMarkdown extends StatefulWidget {
@ -37,24 +35,7 @@ class _BrandMarkdownState extends State<BrandMarkdown> {
@override
Widget build(final BuildContext context) {
final bool isDark = Theme.of(context).brightness == Brightness.dark;
final MarkdownStyleSheet markdown = MarkdownStyleSheet(
p: defaultTextStyle.copyWith(
color: isDark ? BrandColors.white : null,
),
h1: headline1Style.copyWith(
color: isDark ? BrandColors.white : null,
),
h2: headline2Style.copyWith(
color: isDark ? BrandColors.white : null,
),
h3: headline3Style.copyWith(
color: isDark ? BrandColors.white : null,
),
h4: headline4Style.copyWith(
color: isDark ? BrandColors.white : null,
),
);
final MarkdownStyleSheet markdown = MarkdownStyleSheet();
return MarkdownBody(
shrinkWrap: true,
styleSheet: markdown,

View File

@ -1,40 +0,0 @@
import 'package:flutter/material.dart';
import 'package:selfprivacy/config/brand_colors.dart';
// TODO: Delete this file.
class BrandRadio extends StatelessWidget {
const BrandRadio({
required this.isChecked,
super.key,
});
final bool isChecked;
@override
Widget build(final BuildContext context) => Container(
height: 20,
width: 20,
alignment: Alignment.center,
padding: const EdgeInsets.all(2),
decoration: BoxDecoration(
shape: BoxShape.circle,
border: _getBorder(),
),
child: isChecked
? Container(
height: 10,
width: 10,
decoration: const BoxDecoration(
shape: BoxShape.circle,
color: BrandColors.primary,
),
)
: null,
);
BoxBorder? _getBorder() => Border.all(
color: isChecked ? BrandColors.primary : BrandColors.gray1,
width: 2,
);
}

View File

@ -1,19 +0,0 @@
import 'package:flutter/material.dart';
class BrandSwitch extends StatelessWidget {
const BrandSwitch({
required this.onChanged,
required this.value,
super.key,
});
final ValueChanged<bool> onChanged;
final bool value;
@override
Widget build(final BuildContext context) => Switch(
activeColor: Theme.of(context).colorScheme.primary,
value: value,
onChanged: onChanged,
);
}

View File

@ -1,60 +0,0 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart';
class BrandTabBar extends StatefulWidget {
const BrandTabBar({super.key, this.controller});
final TabController? controller;
@override
State<BrandTabBar> createState() => _BrandTabBarState();
}
class _BrandTabBarState extends State<BrandTabBar> {
int? currentIndex;
@override
void initState() {
currentIndex = widget.controller!.index;
widget.controller!.addListener(_listener);
super.initState();
}
void _listener() {
if (currentIndex != widget.controller!.index) {
setState(() {
currentIndex = widget.controller!.index;
});
}
}
@override
void dispose() {
widget.controller ?? widget.controller!.removeListener(_listener);
super.dispose();
}
@override
Widget build(final BuildContext context) => NavigationBar(
destinations: [
_getIconButton('basis.providers'.tr(), BrandIcons.server, 0),
_getIconButton('basis.services'.tr(), BrandIcons.box, 1),
_getIconButton('basis.users'.tr(), BrandIcons.users, 2),
_getIconButton('basis.more'.tr(), Icons.menu_rounded, 3),
],
onDestinationSelected: (final index) {
widget.controller!.animateTo(index);
},
selectedIndex: currentIndex ?? 0,
labelBehavior: NavigationDestinationLabelBehavior.onlyShowSelected,
);
NavigationDestination _getIconButton(
final String label,
final IconData iconData,
final int index,
) =>
NavigationDestination(
icon: Icon(iconData),
label: label,
);
}

View File

@ -1,238 +0,0 @@
import 'package:flutter/material.dart';
import 'package:selfprivacy/config/text_themes.dart';
export 'package:selfprivacy/utils/extensions/text_extensions.dart';
// TODO: Delete this file
enum TextType {
h1, // right now only at onboarding and opened providers
h2, // cards titles
h3, // titles in about page
h4, // caption
h5, // Table data
body1, // normal
body2, // with opacity
medium,
small,
onboardingTitle,
buttonTitleText, // risen button title text,
h4Underlined,
}
class BrandText extends StatelessWidget {
factory BrandText.h4(
final String? text, {
final TextStyle? style,
final TextAlign? textAlign,
}) =>
BrandText(
text,
type: TextType.h4,
style: style,
softWrap: true,
overflow: TextOverflow.ellipsis,
maxLines: 2,
textAlign: textAlign,
);
factory BrandText.onboardingTitle(
final String text, {
final TextStyle? style,
}) =>
BrandText(
text,
type: TextType.onboardingTitle,
style: style,
);
factory BrandText.h3(
final String text, {
final TextStyle? style,
final TextAlign? textAlign,
}) =>
BrandText(
text,
type: TextType.h3,
style: style,
textAlign: textAlign,
overflow: TextOverflow.ellipsis,
);
factory BrandText.h4Underlined(
final String? text, {
final TextStyle? style,
final TextAlign? textAlign,
}) =>
BrandText(
text,
type: TextType.h4Underlined,
style: style,
softWrap: true,
overflow: TextOverflow.ellipsis,
maxLines: 2,
textAlign: textAlign,
);
factory BrandText.h1(
final String? text, {
final TextStyle? style,
final TextOverflow? overflow,
final bool? softWrap,
}) =>
BrandText(
text,
type: TextType.h1,
style: style,
);
factory BrandText.h2(
final String? text, {
final TextStyle? style,
final TextAlign? textAlign,
}) =>
BrandText(
text,
type: TextType.h2,
style: style,
textAlign: textAlign,
);
factory BrandText.body1(final String? text, {final TextStyle? style}) =>
BrandText(
text,
type: TextType.body1,
style: style,
);
factory BrandText.small(final String text, {final TextStyle? style}) =>
BrandText(
text,
type: TextType.small,
style: style,
);
factory BrandText.body2(final String? text, {final TextStyle? style}) =>
BrandText(
text,
type: TextType.body2,
style: style,
);
factory BrandText.buttonTitleText(
final String? text, {
final TextStyle? style,
}) =>
BrandText(
text,
type: TextType.buttonTitleText,
style: style,
);
factory BrandText.h5(
final String? text, {
final TextStyle? style,
final TextAlign? textAlign,
}) =>
BrandText(
text,
type: TextType.h5,
style: style,
textAlign: textAlign,
);
factory BrandText.medium(
final String? text, {
final TextStyle? style,
final TextAlign? textAlign,
}) =>
BrandText(
text,
type: TextType.medium,
style: style,
textAlign: textAlign,
);
const BrandText(
this.text, {
required this.type,
super.key,
this.style,
this.overflow,
this.softWrap,
this.textAlign,
this.maxLines,
});
final String? text;
final TextStyle? style;
final TextType type;
final TextOverflow? overflow;
final bool? softWrap;
final TextAlign? textAlign;
final int? maxLines;
@override
Text build(final BuildContext context) {
TextStyle style;
final bool isDark = Theme.of(context).brightness == Brightness.dark;
switch (type) {
case TextType.h1:
style = isDark
? headline1Style.copyWith(color: Colors.white)
: headline1Style;
break;
case TextType.h2:
style = isDark
? headline2Style.copyWith(color: Colors.white)
: headline2Style;
break;
case TextType.h3:
style = isDark
? headline3Style.copyWith(color: Colors.white)
: headline3Style;
break;
case TextType.h4:
style = isDark
? headline4Style.copyWith(color: Colors.white)
: headline4Style;
break;
case TextType.h4Underlined:
style = isDark
? headline4UnderlinedStyle.copyWith(color: Colors.white)
: headline4UnderlinedStyle;
break;
case TextType.h5:
style = isDark
? headline5Style.copyWith(color: Colors.white)
: headline5Style;
break;
case TextType.body1:
style = isDark ? body1Style.copyWith(color: Colors.white) : body1Style;
break;
case TextType.body2:
style = isDark
? body2Style.copyWith(color: Colors.white.withOpacity(0.6))
: body2Style;
break;
case TextType.small:
style = isDark ? smallStyle.copyWith(color: Colors.white) : smallStyle;
break;
case TextType.onboardingTitle:
style = isDark
? onboardingTitle.copyWith(color: Colors.white)
: onboardingTitle;
break;
case TextType.medium:
style =
isDark ? mediumStyle.copyWith(color: Colors.white) : mediumStyle;
break;
case TextType.buttonTitleText:
style = !isDark
? buttonTitleText.copyWith(color: Colors.white)
: buttonTitleText;
break;
}
if (this.style != null) {
style = style.merge(this.style);
}
return Text(
text!,
style: style,
maxLines: maxLines,
overflow: overflow,
softWrap: softWrap,
textAlign: textAlign,
);
}
}

View File

@ -2,7 +2,6 @@ import 'dart:async';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:selfprivacy/ui/components/brand_text/brand_text.dart';
import 'package:selfprivacy/utils/named_font_weight.dart';
class BrandTimer extends StatefulWidget {
@ -52,11 +51,12 @@ class _BrandTimerState extends State<BrandTimer> {
}
@override
Widget build(final BuildContext context) => BrandText.medium(
_timeString,
style: const TextStyle(
fontWeight: NamedFontWeight.demiBold,
),
Widget build(final BuildContext context) => Text(
_timeString ?? '',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontWeight: NamedFontWeight.demiBold,
color: Theme.of(context).colorScheme.onSurface,
),
);
void _getTime() {

View File

@ -1,7 +1,4 @@
import 'package:flutter/material.dart';
import 'package:selfprivacy/ui/components/brand_text/brand_text.dart';
enum BrandButtonTypes { rised, text, iconText }
class BrandButton {
static ConstrainedBox rised({
@ -58,53 +55,4 @@ class BrandButton {
),
child: TextButton(onPressed: onPressed, child: Text(title)),
);
static IconTextButton emptyWithIconText({
required final VoidCallback onPressed,
required final String title,
required final Icon icon,
final Key? key,
}) =>
IconTextButton(
key: key,
title: title,
onPressed: onPressed,
icon: icon,
);
}
class IconTextButton extends StatelessWidget {
const IconTextButton({
super.key,
this.onPressed,
this.title,
this.icon,
});
final VoidCallback? onPressed;
final String? title;
final Icon? icon;
@override
Widget build(final BuildContext context) => Material(
color: Colors.transparent,
child: InkWell(
onTap: onPressed,
child: Container(
height: 48,
width: double.infinity,
alignment: Alignment.center,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
BrandText.body1(title),
Padding(
padding: const EdgeInsets.all(12.0),
child: icon,
)
],
),
),
),
);
}

View File

@ -1,7 +1,8 @@
import 'package:flutter/material.dart';
class ActionButton extends StatelessWidget {
const ActionButton({
/// Basically a [TextButton] to be used in dialogs
class DialogActionButton extends StatelessWidget {
const DialogActionButton({
super.key,
this.text,
this.onPressed,

View File

@ -1,6 +1,16 @@
import 'package:flutter/material.dart';
/// For some reason original [SegmentedButton] does not have animations.
///
/// The [SegmentedButtons] was written for SelfPrivacy before [SegmentedButton] was introduced.
/// While it doesn't have that much options to pass, it has cute little animation.
/// It is based on [ToggleButtons].
class SegmentedButtons extends StatelessWidget {
/// Creates a segmented buttons widget. This is a SelfPrivacy implementation.
///
/// Provide the button titles in [titles] as a [List<String>].
/// Current selection is provided in [isSelected] as a [List<bool>].
/// This widget will call [onPressed] with the index of the button that was pressed.
const SegmentedButtons({
required this.isSelected,
required this.onPressed,
@ -8,15 +18,24 @@ class SegmentedButtons extends StatelessWidget {
super.key,
});
/// The current selection state of the buttons.
///
/// The length of this list must be equal to the length of [titles].
/// Several buttons can be selected at the same time.
final List<bool> isSelected;
/// The callback that is called when a button is pressed.
/// It will be called with the index of the button that was pressed.
final Function(int)? onPressed;
/// The titles of the buttons.
final List<String> titles;
@override
Widget build(final BuildContext context) => LayoutBuilder(
builder: (final context, final constraints) => ToggleButtons(
constraints: BoxConstraints(
minWidth: (constraints.maxWidth - 8) / 3,
minWidth: (constraints.maxWidth - 8) / titles.length,
minHeight: 40 + Theme.of(context).visualDensity.vertical * 4,
),
borderRadius: BorderRadius.circular(48),
@ -38,7 +57,7 @@ class SegmentedButtons extends StatelessWidget {
opacity: isSelected[index] ? 1 : 0,
child: AnimatedScale(
duration: const Duration(milliseconds: 200),
curve: Curves.easeInOut,
curve: Curves.easeInOutCubicEmphasized,
alignment: Alignment.centerLeft,
scale: isSelected[index] ? 1 : 0,
child: Icon(
@ -53,7 +72,7 @@ class SegmentedButtons extends StatelessWidget {
? const EdgeInsets.only(left: 24)
: EdgeInsets.zero,
duration: const Duration(milliseconds: 200),
curve: Curves.easeInOut,
curve: Curves.easeInOutCubicEmphasized,
child: Text(
title,
style: Theme.of(context).textTheme.labelLarge,

View File

@ -0,0 +1,113 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
class ProgressDrawer extends StatelessWidget {
/// A [Drawer] that displays a list of steps and the current step.
/// Used in setup wizards. The [trailing] widget is displayed at the bottom.
/// The [steps] are translated using [EasyLocalization].
const ProgressDrawer({
required this.steps,
required this.currentStep,
required this.constraints,
required this.trailing,
required this.title,
super.key,
});
final List<String> steps;
final int currentStep;
final Widget trailing;
final BoxConstraints constraints;
final String title;
@override
Widget build(final BuildContext context) => SizedBox(
width: 300,
height: constraints.maxHeight,
child: Drawer(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
title,
style: Theme.of(context).textTheme.titleLarge,
),
),
Flexible(
fit: FlexFit.tight,
child: SingleChildScrollView(
child: Column(
children: [
...steps.map((final step) {
final index = steps.indexOf(step);
return _StepIndicator(
title: step.tr(),
isCurrent: index == currentStep,
isCompleted: index < currentStep,
);
}),
],
),
),
),
// const Spacer(),
Padding(
padding: const EdgeInsets.all(16.0),
child: trailing,
),
],
),
),
);
}
class _StepIndicator extends StatelessWidget {
const _StepIndicator({
required this.title,
required this.isCompleted,
required this.isCurrent,
});
final String title;
final bool isCompleted;
final bool isCurrent;
@override
Widget build(final BuildContext context) => ListTile(
selected: isCurrent,
leading: isCurrent
? const _StepCurrentIcon()
: isCompleted
? const _StepCompletedIcon()
: const _StepPendingIcon(),
title: Text(
title,
),
textColor: Theme.of(context).colorScheme.onSurfaceVariant,
iconColor: Theme.of(context).colorScheme.onSurfaceVariant,
);
}
class _StepCompletedIcon extends StatelessWidget {
const _StepCompletedIcon();
@override
Widget build(final BuildContext context) => const Icon(Icons.check_circle);
}
class _StepPendingIcon extends StatelessWidget {
const _StepPendingIcon();
@override
Widget build(final BuildContext context) => const Icon(Icons.circle_outlined);
}
class _StepCurrentIcon extends StatelessWidget {
const _StepCurrentIcon();
@override
Widget build(final BuildContext context) =>
const Icon(Icons.build_circle_outlined);
}

View File

@ -0,0 +1,52 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:selfprivacy/logic/cubit/support_system/support_system_cubit.dart';
import 'package:selfprivacy/ui/components/brand_md/brand_md.dart';
class SupportDrawer extends StatelessWidget {
const SupportDrawer({
super.key,
});
@override
Widget build(final BuildContext context) {
final currentArticle =
context.watch<SupportSystemCubit>().state.currentArticle;
return Drawer(
width: 440,
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
Row(
children: [
const SizedBox(width: 8),
const Icon(Icons.help_outline),
const SizedBox(width: 16),
Text(
'support.title'.tr(),
style: Theme.of(context).textTheme.titleLarge,
),
const Spacer(),
IconButton(
onPressed: () => Scaffold.of(context).closeEndDrawer(),
icon: const Icon(Icons.chevron_right_outlined),
),
],
),
const SizedBox(height: 8),
Padding(
padding: const EdgeInsets.all(8.0),
child: BrandMarkdown(
fileName: currentArticle,
),
),
],
),
),
),
);
}
}

View File

@ -1,26 +0,0 @@
import 'package:flutter/material.dart';
class BrandError extends StatelessWidget {
const BrandError({super.key, this.error, this.stackTrace});
final Object? error;
final StackTrace? stackTrace;
@override
Widget build(final BuildContext context) => SafeArea(
child: Scaffold(
body: Center(
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(error.toString()),
const Text('stackTrace: '),
Text(stackTrace.toString()),
],
),
),
),
),
);
}

View File

@ -6,10 +6,8 @@ import 'package:selfprivacy/logic/cubit/client_jobs/client_jobs_cubit.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
import 'package:selfprivacy/logic/cubit/server_jobs/server_jobs_cubit.dart';
import 'package:selfprivacy/logic/models/json/server_job.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/buttons/brand_button.dart';
import 'package:selfprivacy/ui/components/brand_loader/brand_loader.dart';
import 'package:selfprivacy/ui/components/brand_text/brand_text.dart';
import 'package:selfprivacy/ui/components/jobs_content/server_job_card.dart';
import 'package:selfprivacy/ui/helpers/modals.dart';
@ -32,7 +30,12 @@ class JobsContent extends StatelessWidget {
if (state is JobsStateEmpty) {
widgets = [
const SizedBox(height: 80),
Center(child: BrandText.body1('jobs.empty'.tr())),
Center(
child: Text(
'jobs.empty'.tr(),
style: Theme.of(context).textTheme.bodyLarge,
),
),
];
if (installationState is ServerInstallationFinished) {
@ -65,38 +68,49 @@ class JobsContent extends StatelessWidget {
];
} else if (state is JobsStateWithJobs) {
widgets = [
...state.clientJobList
.map(
(final j) => Row(
children: [
Expanded(
child: BrandCards.small(
child: Text(j.title),
...state.clientJobList.map(
(final j) => Row(
children: [
Expanded(
child: Card(
color: Theme.of(context).colorScheme.surfaceVariant,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 15,
vertical: 10,
),
),
const SizedBox(width: 10),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor:
Theme.of(context).colorScheme.errorContainer,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
onPressed: () =>
context.read<JobsCubit>().removeJob(j.id),
child: Text(
'basis.remove'.tr(),
style: TextStyle(
color:
Theme.of(context).colorScheme.onErrorContainer,
),
j.title,
style:
Theme.of(context).textTheme.labelLarge?.copyWith(
color: Theme.of(context)
.colorScheme
.onSurfaceVariant,
),
),
),
],
),
),
)
.toList(),
const SizedBox(width: 10),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor:
Theme.of(context).colorScheme.errorContainer,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
onPressed: () => context.read<JobsCubit>().removeJob(j.id),
child: Text(
'basis.remove'.tr(),
style: TextStyle(
color: Theme.of(context).colorScheme.onErrorContainer,
),
),
),
],
),
),
const SizedBox(height: 20),
BrandButton.rised(
onPressed: () => context.read<JobsCubit>().applyAll(),
@ -109,8 +123,9 @@ class JobsContent extends StatelessWidget {
children: [
const SizedBox(height: 15),
Center(
child: BrandText.h2(
child: Text(
'jobs.title'.tr(),
style: Theme.of(context).textTheme.headlineSmall,
),
),
const SizedBox(height: 20),

View File

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

View File

@ -1,7 +1,7 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:selfprivacy/ui/components/brand_cards/filled_card.dart';
import 'package:selfprivacy/ui/pages/setup/initializing/initializing.dart';
import 'package:selfprivacy/utils/route_transitions/basic.dart';
import 'package:selfprivacy/ui/components/cards/filled_card.dart';
import 'package:selfprivacy/ui/router/router.dart';
import 'package:easy_localization/easy_localization.dart';
class NotReadyCard extends StatelessWidget {
@ -13,11 +13,7 @@ class NotReadyCard extends StatelessWidget {
child: ListTile(
contentPadding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
onTap: () => Navigator.of(context).push(
materialRoute(
const InitializingPage(),
),
),
onTap: () => context.pushRoute(const InitializingRoute()),
title: Text(
'not_ready_card.in_menu'.tr(),
style: Theme.of(context).textTheme.titleSmall?.copyWith(

View File

@ -1,13 +1,17 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:ionicons/ionicons.dart';
import 'package:selfprivacy/logic/cubit/client_jobs/client_jobs_cubit.dart';
import 'package:selfprivacy/ui/components/brand_bottom_sheet/brand_bottom_sheet.dart';
import 'package:selfprivacy/ui/components/jobs_content/jobs_content.dart';
import 'package:selfprivacy/ui/helpers/modals.dart';
class BrandFab extends StatefulWidget {
const BrandFab({super.key});
const BrandFab({
this.extended = false,
super.key,
});
final bool extended;
@override
State<BrandFab> createState() => _BrandFabState();
@ -56,28 +60,40 @@ class _BrandFabState extends State<BrandFab>
child: FloatingActionButton(
onPressed: () {
// TODO: Make a hero animation to the screen
showBrandBottomSheet(
showModalBottomSheet(
context: context,
builder: (final BuildContext context) => const BrandBottomSheet(
isExpended: true,
child: JobsContent(),
),
builder: (final BuildContext context) => const JobsContent(),
);
},
child: AnimatedBuilder(
animation: _colorTween,
builder: (final BuildContext context, final Widget? child) {
final double v = _animationController.value;
final IconData icon =
v > 0.5 ? Ionicons.flash : Ionicons.flash_outline;
return Transform.scale(
scale: 1 + (v < 0.5 ? v : 1 - v) * 2,
child: Icon(
icon,
color: _colorTween.value,
isExtended: widget.extended,
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
AnimatedBuilder(
animation: _colorTween,
builder: (final BuildContext context, final Widget? child) {
final double v = _animationController.value;
final IconData icon =
v > 0.5 ? Ionicons.flash : Ionicons.flash_outline;
return Transform.scale(
scale: 1 + (v < 0.5 ? v : 1 - v) * 2,
child: Icon(
icon,
color: _colorTween.value,
),
);
},
),
if (widget.extended)
const SizedBox(
width: 8,
),
);
},
if (widget.extended)
Text(
'jobs.title'.tr(),
),
],
),
),
);

View File

@ -1,6 +1,4 @@
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';
class ProgressBar extends StatefulWidget {
@ -65,7 +63,7 @@ class _ProgressBarState extends State<ProgressBar> {
Container(
alignment: Alignment.centerLeft,
decoration: BoxDecoration(
color: BrandColors.gray4,
color: const Color(0xFFDDDDDD),
borderRadius: BorderRadius.circular(5),
),
child: LayoutBuilder(
@ -119,3 +117,13 @@ class _ProgressBarState extends State<ProgressBar> {
);
}
}
const TextStyle progressTextStyleLight = TextStyle(
fontSize: 11,
color: Colors.black,
height: 1.7,
);
final TextStyle progressTextStyleDark = progressTextStyleLight.copyWith(
color: Colors.white,
);

View File

@ -72,7 +72,10 @@ class ServiceConsumptionTitle extends StatelessWidget {
service.svgIcon,
width: 24.0,
height: 24.0,
color: Theme.of(context).colorScheme.onBackground,
colorFilter: ColorFilter.mode(
Theme.of(context).colorScheme.onBackground,
BlendMode.srcIn,
),
),
),
const SizedBox(width: 16),

View File

@ -1,36 +0,0 @@
import 'package:flutter/material.dart';
import 'package:selfprivacy/config/brand_colors.dart';
// TODO: Delete this file.
class SwitcherBlock extends StatelessWidget {
const SwitcherBlock({
required this.child,
required this.isActive,
required this.onChange,
super.key,
});
final Widget child;
final bool isActive;
final ValueChanged<bool> onChange;
@override
Widget build(final BuildContext context) => Container(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(child: child),
const SizedBox(width: 5),
Switch(
activeColor: BrandColors.green1,
activeTrackColor: BrandColors.green2,
onChanged: onChange,
value: isActive,
),
],
),
);
}

View File

@ -1,21 +1,7 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/ui/components/action_button/action_button.dart';
import 'package:selfprivacy/ui/components/brand_alert/brand_alert.dart';
Future<T?> showBrandBottomSheet<T>({
required final BuildContext context,
required final WidgetBuilder builder,
}) =>
showCupertinoModalBottomSheet<T>(
builder: builder,
barrierColor: Colors.black45,
context: context,
shadow: const BoxShadow(color: Colors.transparent),
backgroundColor: Colors.transparent,
);
import 'package:selfprivacy/ui/components/buttons/dialog_action_button.dart';
void showPopUpAlert({
required final String description,
@ -26,16 +12,16 @@ void showPopUpAlert({
final String? cancelButtonTitle,
}) {
getIt.get<NavigationService>().showPopUpDialog(
BrandAlert(
title: alertTitle ?? 'basis.alert'.tr(),
contentText: description,
AlertDialog(
title: Text(alertTitle ?? 'basis.alert'.tr()),
content: Text(description),
actions: [
ActionButton(
DialogActionButton(
text: actionButtonTitle,
isRed: true,
onPressed: actionButtonOnPressed,
),
ActionButton(
DialogActionButton(
text: cancelButtonTitle ?? 'basis.cancel'.tr(),
onPressed: cancelButtonOnPressed,
),

View File

@ -1,14 +1,26 @@
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
/// A helper widget that calls a callback when its size changes.
///
/// This is useful when you want to know the size of a widget, and use it in
/// another leaf of the tree.
///
/// The [onChange] callback is called after the widget is rendered, and the
/// size of the widget is different from the previous render.
class WidgetSize extends StatefulWidget {
/// Creates a helper widget that calls a callback when its size changes.
const WidgetSize({
required this.onChange,
required this.child,
super.key,
});
/// The child widget, the size of which is to be measured.
final Widget child;
final Function onChange;
/// The callback to be called when the size of the widget changes.
final Function(Size) onChange;
@override
State<WidgetSize> createState() => _WidgetSizeState();
@ -34,6 +46,11 @@ class _WidgetSizeState extends State<WidgetSize> {
}
final newSize = context.size;
if (newSize == null) {
return;
}
if (oldSize == newSize) {
return;
}

View File

@ -0,0 +1,195 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:ionicons/ionicons.dart';
import 'package:selfprivacy/logic/cubit/client_jobs/client_jobs_cubit.dart';
import 'package:selfprivacy/ui/components/jobs_content/jobs_content.dart';
import 'package:selfprivacy/ui/components/drawers/support_drawer.dart';
import 'package:selfprivacy/ui/helpers/widget_size.dart';
import 'package:selfprivacy/utils/breakpoints.dart';
class BrandHeroScreen extends StatelessWidget {
const BrandHeroScreen({
required this.children,
super.key,
this.hasBackButton = true,
this.hasFlashButton = false,
this.heroIcon,
this.heroIconWidget,
this.heroTitle = '',
this.heroSubtitle,
this.onBackButtonPressed,
this.bodyPadding = const EdgeInsets.all(16.0),
this.ignoreBreakpoints = false,
this.hasSupportDrawer = false,
});
final List<Widget> children;
final bool hasBackButton;
final bool hasFlashButton;
final IconData? heroIcon;
final Widget? heroIconWidget;
final String heroTitle;
final String? heroSubtitle;
final VoidCallback? onBackButtonPressed;
final EdgeInsetsGeometry bodyPadding;
/// On non-mobile screens the buttons of the app bar are hidden.
/// This is because this widget implies that it is nested inside a bigger layout.
/// If it is not nested, set this to true.
final bool ignoreBreakpoints;
/// Usually support drawer is provided by the parent layout.
/// If it is not provided, set this to true.
final bool hasSupportDrawer;
@override
Widget build(final BuildContext context) {
final Widget heroIconWidget = this.heroIconWidget ??
Icon(
heroIcon ?? Icons.help,
size: 48.0,
color: Theme.of(context).colorScheme.onBackground,
);
final bool hasHeroIcon = heroIcon != null || this.heroIconWidget != null;
return Scaffold(
endDrawerEnableOpenDragGesture: false,
endDrawer: hasSupportDrawer ? const SupportDrawer() : null,
body: CustomScrollView(
slivers: [
HeroSliverAppBar(
heroTitle: heroTitle,
hasHeroIcon: hasHeroIcon,
hasBackButton: hasBackButton,
onBackButtonPressed: onBackButtonPressed,
heroIconWidget: heroIconWidget,
hasFlashButton: hasFlashButton,
ignoreBreakpoints: ignoreBreakpoints,
),
if (heroSubtitle != null)
SliverPadding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 4.0,
),
sliver: SliverList(
delegate: SliverChildListDelegate([
Text(
heroSubtitle!,
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: Theme.of(context).colorScheme.onBackground,
),
textAlign: hasHeroIcon ? TextAlign.center : TextAlign.start,
),
]),
),
),
SliverPadding(
padding: bodyPadding,
sliver: SliverList(
delegate: SliverChildListDelegate(children),
),
),
],
),
);
}
}
class HeroSliverAppBar extends StatefulWidget {
const HeroSliverAppBar({
required this.heroTitle,
required this.hasHeroIcon,
required this.hasBackButton,
required this.onBackButtonPressed,
required this.heroIconWidget,
required this.hasFlashButton,
required this.ignoreBreakpoints,
super.key,
});
final String heroTitle;
final bool hasHeroIcon;
final bool hasBackButton;
final bool hasFlashButton;
final VoidCallback? onBackButtonPressed;
final Widget heroIconWidget;
final bool ignoreBreakpoints;
@override
State<HeroSliverAppBar> createState() => _HeroSliverAppBarState();
}
class _HeroSliverAppBarState extends State<HeroSliverAppBar> {
Size _size = Size.zero;
@override
Widget build(final BuildContext context) {
final isMobile =
widget.ignoreBreakpoints ? true : Breakpoints.small.isActive(context);
final isJobsListEmpty = context.watch<JobsCubit>().state is JobsStateEmpty;
return SliverAppBar(
expandedHeight:
widget.hasHeroIcon ? 148.0 + _size.height : 72.0 + _size.height,
primary: true,
pinned: isMobile,
stretch: true,
surfaceTintColor: isMobile ? null : Colors.transparent,
leading: (widget.hasBackButton && isMobile)
? const AutoLeadingButton()
: const SizedBox.shrink(),
actions: [
if (widget.hasFlashButton && isMobile)
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: IconButton(
onPressed: () {
showModalBottomSheet(
context: context,
builder: (final BuildContext context) => const JobsContent(),
);
},
icon: Icon(
isJobsListEmpty ? Ionicons.flash_outline : Ionicons.flash,
),
color: isJobsListEmpty
? Theme.of(context).colorScheme.onBackground
: Theme.of(context).colorScheme.primary,
),
),
const SizedBox.shrink(),
],
flexibleSpace: FlexibleSpaceBar(
title: LayoutBuilder(
builder: (final context, final constraints) => SizedBox(
width: constraints.maxWidth - 72.0,
child: WidgetSize(
onChange: (final Size size) => setState(() => _size = size),
child: Text(
widget.heroTitle,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: Theme.of(context).colorScheme.onSurface,
),
overflow: TextOverflow.fade,
textAlign: TextAlign.center,
),
),
),
),
expandedTitleScale: 1.2,
centerTitle: true,
collapseMode: CollapseMode.pin,
titlePadding: const EdgeInsets.only(
bottom: 12.0,
top: 16.0,
),
background: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
const SizedBox(height: 72.0),
if (widget.hasHeroIcon) widget.heroIconWidget,
],
),
),
);
}
}

View File

@ -0,0 +1,70 @@
import 'package:flutter/material.dart';
import 'package:selfprivacy/utils/breakpoints.dart';
class ResponsiveLayoutWithInfobox extends StatelessWidget {
const ResponsiveLayoutWithInfobox({
required this.primaryColumn,
this.topChild,
this.secondaryColumn,
super.key,
});
final Widget? topChild;
final Widget primaryColumn;
final Widget? secondaryColumn;
@override
Widget build(final BuildContext context) {
final hasSecondaryColumn = secondaryColumn != null;
final hasTopChild = topChild != null;
if (Breakpoints.large.isActive(context)) {
return LayoutBuilder(
builder: (final context, final constraints) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (hasTopChild)
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: constraints.maxWidth * 0.9,
child: topChild,
),
],
),
if (hasTopChild) const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: hasSecondaryColumn
? constraints.maxWidth * 0.7
: constraints.maxWidth * 0.9,
child: primaryColumn,
),
if (hasSecondaryColumn) const SizedBox(width: 16),
if (hasSecondaryColumn)
SizedBox(
width: constraints.maxWidth * 0.2,
child: secondaryColumn,
),
],
),
],
),
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (hasTopChild) topChild!,
const SizedBox(height: 16),
primaryColumn,
const SizedBox(height: 32),
if (hasSecondaryColumn) secondaryColumn!,
],
);
}
}

View File

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

View File

@ -1,26 +1,27 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
import 'package:selfprivacy/logic/cubit/backups/backups_cubit.dart';
import 'package:selfprivacy/logic/models/json/backup.dart';
import 'package:selfprivacy/logic/models/state_types.dart';
import 'package:selfprivacy/ui/components/brand_button/brand_button.dart';
import 'package:selfprivacy/ui/components/brand_cards/outlined_card.dart';
import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart';
import 'package:selfprivacy/ui/components/buttons/brand_button.dart';
import 'package:selfprivacy/ui/components/cards/outlined_card.dart';
import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart';
import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart';
import 'package:selfprivacy/ui/components/brand_text/brand_text.dart';
import 'package:selfprivacy/ui/helpers/modals.dart';
GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
class BackupDetails extends StatefulWidget {
const BackupDetails({super.key});
@RoutePage()
class BackupDetailsPage extends StatefulWidget {
const BackupDetailsPage({super.key});
@override
State<BackupDetails> createState() => _BackupDetailsState();
State<BackupDetailsPage> createState() => _BackupDetailsPageState();
}
class _BackupDetailsState extends State<BackupDetails>
class _BackupDetailsPageState extends State<BackupDetailsPage>
with SingleTickerProviderStateMixin {
@override
Widget build(final BuildContext context) {
@ -57,7 +58,10 @@ class _BackupDetailsState extends State<BackupDetails>
text: 'backup.initialize'.tr(),
),
if (backupStatus == BackupStatusEnum.initializing)
BrandText.body1('backup.waiting_for_rebuild'.tr()),
Text(
'backup.waiting_for_rebuild'.tr(),
style: Theme.of(context).textTheme.bodyMedium,
),
if (backupStatus != BackupStatusEnum.initializing &&
backupStatus != BackupStatusEnum.noKey)
OutlinedCard(
@ -227,7 +231,10 @@ class _BackupDetailsState extends State<BackupDetails>
),
),
if (backupStatus == BackupStatusEnum.error)
BrandText.body1(backupError.toString()),
Text(
backupError.toString(),
style: Theme.of(context).textTheme.bodyMedium,
),
],
);
}

View File

@ -1,3 +1,4 @@
import 'package:auto_route/auto_route.dart';
import 'package:cubit_form/cubit_form.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
@ -5,11 +6,12 @@ import 'package:selfprivacy/logic/common_enum/common_enum.dart';
import 'package:selfprivacy/logic/cubit/devices/devices_cubit.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
import 'package:selfprivacy/logic/models/json/api_token.dart';
import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart';
import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart';
import 'package:selfprivacy/ui/components/info_box/info_box.dart';
import 'package:selfprivacy/ui/pages/devices/new_device.dart';
import 'package:selfprivacy/utils/route_transitions/basic.dart';
@RoutePage()
class DevicesScreen extends StatefulWidget {
const DevicesScreen({super.key});
@ -25,7 +27,7 @@ class _DevicesScreenState extends State<DevicesScreen> {
return RefreshIndicator(
onRefresh: () async {
context.read<ApiDevicesCubit>().refresh();
await context.read<ApiDevicesCubit>().refresh();
},
child: BrandHeroScreen(
heroTitle: 'devices.main_screen.header'.tr(),
@ -90,8 +92,7 @@ class _DevicesInfo extends StatelessWidget {
),
),
...devicesStatus.otherDevices
.map((final device) => _DeviceTile(device: device))
.toList(),
.map((final device) => _DeviceTile(device: device)),
],
);
}

View File

@ -3,8 +3,8 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:selfprivacy/logic/cubit/devices/devices_cubit.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
import 'package:selfprivacy/ui/components/brand_button/brand_button.dart';
import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart';
import 'package:selfprivacy/ui/components/buttons/brand_button.dart';
import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart';
class NewDeviceScreen extends StatelessWidget {
const NewDeviceScreen({super.key});

View File

@ -1,13 +1,15 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/desired_dns_record.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
import 'package:selfprivacy/logic/cubit/dns_records/dns_records_cubit.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/components/cards/filled_card.dart';
import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart';
import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart';
@RoutePage()
class DnsDetailsPage extends StatefulWidget {
const DnsDetailsPage({super.key});
@ -158,8 +160,7 @@ class _DnsDetailsPageState extends State<DnsDetailsPage> {
),
],
),
)
.toList(),
),
const SizedBox(height: 16.0),
ListTile(
title: Text(
@ -200,8 +201,7 @@ class _DnsDetailsPageState extends State<DnsDetailsPage> {
),
],
),
)
.toList(),
),
],
);
}

View File

@ -1,67 +1,73 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:selfprivacy/config/brand_theme.dart';
import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server_api.dart';
import 'package:selfprivacy/ui/components/brand_header/brand_header.dart';
import 'package:selfprivacy/ui/components/brand_text/brand_text.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
import 'package:selfprivacy/ui/components/brand_md/brand_md.dart';
import 'package:package_info/package_info.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart';
import 'package:url_launcher/url_launcher.dart';
@RoutePage()
class AboutApplicationPage extends StatelessWidget {
const AboutApplicationPage({super.key});
@override
Widget build(final BuildContext context) => SafeArea(
child: Scaffold(
appBar: PreferredSize(
preferredSize: const Size.fromHeight(52),
child: BrandHeader(
title: 'about_application_page.title'.tr(),
hasBackButton: true,
Widget build(final BuildContext context) {
final bool isReady = context.watch<ServerInstallationCubit>().state
is ServerInstallationFinished;
return BrandHeroScreen(
hasBackButton: true,
hasFlashButton: false,
heroTitle: 'about_application_page.title'.tr(),
children: [
FutureBuilder(
future: _packageVersion(),
builder: (final context, final snapshot) => Text(
'about_application_page.application_version_text'
.tr(args: [snapshot.data.toString()]),
style: Theme.of(context).textTheme.bodyLarge,
),
),
if (isReady)
FutureBuilder(
future: _apiVersion(),
builder: (final context, final snapshot) => Text(
'about_application_page.api_version_text'
.tr(args: [snapshot.data.toString()]),
style: Theme.of(context).textTheme.bodyLarge,
),
),
body: ListView(
padding: paddingH15V0,
const SizedBox(height: 10),
// Button to call showAboutDialog
TextButton(
onPressed: () => showAboutDialog(
context: context,
applicationName: 'SelfPrivacy',
applicationLegalese: '© 2022 SelfPrivacy',
// Link to privacy policy
children: [
const SizedBox(height: 10),
FutureBuilder(
future: _packageVersion(),
builder: (final context, final snapshot) => BrandText.body1(
'about_application_page.application_version_text'
.tr(args: [snapshot.data.toString()]),
),
),
FutureBuilder(
future: _apiVersion(),
builder: (final context, final snapshot) => BrandText.body1(
'about_application_page.api_version_text'
.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()),
),
],
onPressed: () => launchUrl(
Uri.parse('https://selfprivacy.ru/privacy-policy'),
mode: LaunchMode.externalApplication,
),
child: const Text('Show about dialog'),
child: Text('about_application_page.privacy_policy'.tr()),
),
],
),
child: const Text('Show about dialog'),
),
);
const SizedBox(height: 8),
const Divider(height: 0),
const SizedBox(height: 8),
const BrandMarkdown(
fileName: 'about',
),
],
);
}
Future<String> _packageVersion() async {
String packageVersion = 'unknown';

View File

@ -1,229 +0,0 @@
import 'package:flutter/material.dart';
import 'package:selfprivacy/config/brand_colors.dart';
import 'package:selfprivacy/config/brand_theme.dart';
import 'package:selfprivacy/logic/cubit/app_settings/app_settings_cubit.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
import 'package:selfprivacy/ui/components/action_button/action_button.dart';
import 'package:selfprivacy/ui/components/brand_alert/brand_alert.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/utils/named_font_weight.dart';
import 'package:easy_localization/easy_localization.dart';
class AppSettingsPage extends StatefulWidget {
const AppSettingsPage({super.key});
@override
State<AppSettingsPage> createState() => _AppSettingsPageState();
}
class _AppSettingsPageState extends State<AppSettingsPage> {
@override
Widget build(final BuildContext context) {
final bool isDarkModeOn =
context.watch<AppSettingsCubit>().state.isDarkModeOn;
return SafeArea(
child: Builder(
builder: (final context) => Scaffold(
appBar: PreferredSize(
preferredSize: const Size.fromHeight(52),
child: BrandHeader(
title: 'application_settings.title'.tr(),
hasBackButton: true,
),
),
body: ListView(
padding: paddingH15V0,
children: [
const Divider(height: 1),
Container(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: _TextColumn(
title: 'application_settings.dark_theme_title'.tr(),
value:
'application_settings.dark_theme_description'.tr(),
hasWarning: false,
),
),
const SizedBox(width: 5),
BrandSwitch(
value: Theme.of(context).brightness == Brightness.dark,
onChanged: (final value) => context
.read<AppSettingsCubit>()
.updateDarkMode(isDarkModeOn: !isDarkModeOn),
),
],
),
),
const Divider(height: 0),
Container(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: _TextColumn(
title: 'application_settings.reset_config_title'.tr(),
value: 'application_settings.reset_config_description'
.tr(),
hasWarning: false,
),
),
const SizedBox(width: 5),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: BrandColors.red1,
),
child: Text(
'basis.reset'.tr(),
style: const TextStyle(
color: BrandColors.white,
fontWeight: NamedFontWeight.demiBold,
),
),
onPressed: () {
showDialog(
context: context,
builder: (final _) => BrandAlert(
title: 'modals.are_you_sure'.tr(),
contentText: 'modals.purge_all_keys'.tr(),
actions: [
ActionButton(
text: 'modals.purge_all_keys_confirm'.tr(),
isRed: true,
onPressed: () {
context
.read<ServerInstallationCubit>()
.clearAppConfig();
Navigator.of(context).pop();
},
),
ActionButton(
text: 'basis.cancel'.tr(),
),
],
),
);
},
),
],
),
),
const Divider(height: 0),
_deleteServer(context)
],
),
),
),
);
}
Widget _deleteServer(final BuildContext context) {
final bool isDisabled =
context.watch<ServerInstallationCubit>().state.serverDetails == null;
return Container(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: _TextColumn(
title: 'application_settings.delete_server_title'.tr(),
value: 'application_settings.delete_server_description'.tr(),
hasWarning: false,
),
),
const SizedBox(width: 5),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: BrandColors.red1,
),
onPressed: isDisabled
? null
: () {
showDialog(
context: context,
builder: (final _) => BrandAlert(
title: 'modals.are_you_sure'.tr(),
contentText: 'modals.delete_server_volume'.tr(),
actions: [
ActionButton(
text: 'modals.yes'.tr(),
isRed: true,
onPressed: () async {
showDialog(
context: context,
builder: (final context) => Container(
alignment: Alignment.center,
child: const CircularProgressIndicator(),
),
);
await context
.read<ServerInstallationCubit>()
.serverDelete();
if (!mounted) {
return;
}
Navigator.of(context).pop();
},
),
ActionButton(
text: 'basis.cancel'.tr(),
),
],
),
);
},
child: Text(
'basis.delete'.tr(),
style: const TextStyle(
color: BrandColors.white,
fontWeight: NamedFontWeight.demiBold,
),
),
),
],
),
);
}
}
class _TextColumn extends StatelessWidget {
const _TextColumn({
required this.title,
required this.value,
this.hasWarning = false,
});
final String title;
final String value;
final bool hasWarning;
@override
Widget build(final BuildContext context) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
BrandText.body1(
title,
style: TextStyle(color: hasWarning ? BrandColors.warning : null),
),
const SizedBox(height: 5),
BrandText.body1(
value,
style: const TextStyle(
fontSize: 13,
height: 1.53,
color: BrandColors.gray1,
).merge(TextStyle(color: hasWarning ? BrandColors.warning : null)),
),
],
);
}

View File

@ -0,0 +1,151 @@
import 'dart:async';
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:selfprivacy/logic/cubit/app_settings/app_settings_cubit.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
import 'package:selfprivacy/ui/components/buttons/dialog_action_button.dart';
import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart';
import 'package:easy_localization/easy_localization.dart';
@RoutePage()
class AppSettingsPage extends StatefulWidget {
const AppSettingsPage({super.key});
@override
State<AppSettingsPage> createState() => _AppSettingsPageState();
}
class _AppSettingsPageState extends State<AppSettingsPage> {
@override
Widget build(final BuildContext context) {
final bool isDarkModeOn =
context.watch<AppSettingsCubit>().state.isDarkModeOn;
final bool isSystemDarkModeOn =
context.watch<AppSettingsCubit>().state.isAutoDarkModeOn;
return BrandHeroScreen(
hasBackButton: true,
hasFlashButton: false,
bodyPadding: const EdgeInsets.symmetric(vertical: 16),
heroTitle: 'application_settings.title'.tr(),
children: [
SwitchListTile(
title: Text('application_settings.system_dark_theme_title'.tr()),
subtitle:
Text('application_settings.system_dark_theme_description'.tr()),
value: isSystemDarkModeOn,
onChanged: (final value) => context
.read<AppSettingsCubit>()
.updateAutoDarkMode(isAutoDarkModeOn: !isSystemDarkModeOn),
),
SwitchListTile(
title: Text('application_settings.dark_theme_title'.tr()),
subtitle: Text('application_settings.dark_theme_description'.tr()),
value: Theme.of(context).brightness == Brightness.dark,
onChanged: isSystemDarkModeOn
? null
: (final value) => context
.read<AppSettingsCubit>()
.updateDarkMode(isDarkModeOn: !isDarkModeOn),
),
const Divider(height: 0),
Padding(
padding: const EdgeInsets.all(16),
child: Text(
'application_settings.dangerous_settings'.tr(),
style: Theme.of(context).textTheme.labelLarge!.copyWith(
color: Theme.of(context).colorScheme.error,
),
),
),
const _ResetAppTile(),
// const Divider(height: 0),
_deleteServer(context)
],
);
}
Widget _deleteServer(final BuildContext context) {
final bool isDisabled =
context.watch<ServerInstallationCubit>().state.serverDetails == null;
return ListTile(
title: Text('application_settings.delete_server_title'.tr()),
subtitle: Text('application_settings.delete_server_description'.tr()),
textColor: isDisabled
? Theme.of(context).colorScheme.onBackground.withOpacity(0.5)
: Theme.of(context).colorScheme.onBackground,
onTap: isDisabled
? null
: () {
showDialog(
context: context,
builder: (final _) => AlertDialog(
title: Text('modals.are_you_sure'.tr()),
content: Text('modals.delete_server_volume'.tr()),
actions: [
DialogActionButton(
text: 'modals.yes'.tr(),
isRed: true,
onPressed: () async {
unawaited(
showDialog(
context: context,
builder: (final context) => Container(
alignment: Alignment.center,
child: const CircularProgressIndicator(),
),
),
);
await context
.read<ServerInstallationCubit>()
.serverDelete();
if (!mounted) {
return;
}
Navigator.of(context).pop();
},
),
DialogActionButton(
text: 'basis.cancel'.tr(),
),
],
),
);
},
);
}
}
class _ResetAppTile extends StatelessWidget {
const _ResetAppTile();
@override
Widget build(final BuildContext context) => ListTile(
title: Text('application_settings.reset_config_title'.tr()),
subtitle: Text('application_settings.reset_config_description'.tr()),
onTap: () {
showDialog(
context: context,
builder: (final _) => AlertDialog(
title: Text('modals.are_you_sure'.tr()),
content: Text('modals.purge_all_keys'.tr()),
actions: [
DialogActionButton(
text: 'modals.purge_all_keys_confirm'.tr(),
isRed: true,
onPressed: () {
context.read<ServerInstallationCubit>().clearAppConfig();
Navigator.of(context).pop();
},
),
DialogActionButton(
text: 'basis.cancel'.tr(),
),
],
),
);
},
);
}

View File

@ -0,0 +1,85 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:selfprivacy/logic/api_maps/staging_options.dart';
import 'package:selfprivacy/logic/cubit/app_settings/app_settings_cubit.dart';
import 'package:selfprivacy/logic/cubit/devices/devices_cubit.dart';
import 'package:selfprivacy/logic/cubit/recovery_key/recovery_key_cubit.dart';
import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart';
import 'package:easy_localization/easy_localization.dart';
@RoutePage()
class DeveloperSettingsPage extends StatefulWidget {
const DeveloperSettingsPage({super.key});
@override
State<DeveloperSettingsPage> createState() => _DeveloperSettingsPageState();
}
class _DeveloperSettingsPageState extends State<DeveloperSettingsPage> {
@override
Widget build(final BuildContext context) => BrandHeroScreen(
hasBackButton: true,
hasFlashButton: false,
bodyPadding: const EdgeInsets.symmetric(vertical: 16),
heroTitle: 'developer_settings.title'.tr(),
heroSubtitle: 'developer_settings.subtitle'.tr(),
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Text(
'developer_settings.server_setup'.tr(),
style: Theme.of(context).textTheme.labelLarge!.copyWith(
color: Theme.of(context).colorScheme.secondary,
),
),
),
SwitchListTile(
title: Text('developer_settings.use_staging_acme'.tr()),
subtitle:
Text('developer_settings.use_staging_acme_description'.tr()),
value: StagingOptions.stagingAcme,
onChanged: null,
),
Padding(
padding: const EdgeInsets.all(16),
child: Text(
'developer_settings.routing'.tr(),
style: Theme.of(context).textTheme.labelLarge!.copyWith(
color: Theme.of(context).colorScheme.secondary,
),
),
),
ListTile(
title: Text('developer_settings.reset_onboarding'.tr()),
subtitle:
Text('developer_settings.reset_onboarding_description'.tr()),
enabled:
!context.watch<AppSettingsCubit>().state.isOnboardingShowing,
onTap: () => context
.read<AppSettingsCubit>()
.turnOffOnboarding(isOnboardingShowing: true),
),
Padding(
padding: const EdgeInsets.all(16),
child: Text(
'developer_settings.cubit_statuses'.tr(),
style: Theme.of(context).textTheme.labelLarge!.copyWith(
color: Theme.of(context).colorScheme.secondary,
),
),
),
ListTile(
title: const Text('ApiDevicesCubit'),
subtitle: Text(
context.watch<ApiDevicesCubit>().state.status.toString(),
),
),
ListTile(
title: const Text('RecoveryKeyCubit'),
subtitle: Text(
context.watch<RecoveryKeyCubit>().state.loadingStatus.toString(),
),
),
],
);
}

View File

@ -1,20 +1,21 @@
import 'package:auto_route/auto_route.dart';
import 'dart:collection';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:selfprivacy/config/brand_colors.dart';
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/models/message.dart';
import 'package:selfprivacy/ui/components/brand_header/brand_header.dart';
import 'package:selfprivacy/ui/components/list_tiles/log_list_tile.dart';
class Console extends StatefulWidget {
const Console({super.key});
@RoutePage()
class ConsolePage extends StatefulWidget {
const ConsolePage({super.key});
@override
State<Console> createState() => _ConsoleState();
State<ConsolePage> createState() => _ConsolePageState();
}
class _ConsoleState extends State<Console> {
class _ConsolePageState extends State<ConsolePage> {
@override
void initState() {
getIt.get<ConsoleModel>().addListener(update);
@ -28,21 +29,31 @@ class _ConsoleState extends State<Console> {
super.dispose();
}
void update() => setState(() => {});
bool paused = false;
void update() {
if (!paused) {
setState(() => {});
}
}
@override
Widget build(final BuildContext context) => SafeArea(
child: Scaffold(
appBar: PreferredSize(
preferredSize: const Size.fromHeight(53),
child: Column(
children: [
BrandHeader(
title: 'console_page.title'.tr(),
hasBackButton: true,
),
],
appBar: AppBar(
title: Text('console_page.title'.tr()),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => Navigator.of(context).pop(),
),
actions: [
IconButton(
icon: Icon(
paused ? Icons.play_arrow_outlined : Icons.pause_outlined,
),
onPressed: () => setState(() => paused = !paused),
),
],
),
body: FutureBuilder(
future: getIt.allReady(),
@ -61,30 +72,7 @@ class _ConsoleState extends State<Console> {
const SizedBox(height: 20),
...UnmodifiableListView(
messages
.map((final message) {
final bool isError =
message.type == MessageType.warning;
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: RichText(
text: TextSpan(
style: DefaultTextStyle.of(context).style,
children: <TextSpan>[
TextSpan(
text:
'${message.timeString}${isError ? '(Error)' : ''}: \n',
style: TextStyle(
fontWeight: FontWeight.bold,
color:
isError ? BrandColors.red1 : null,
),
),
TextSpan(text: message.text),
],
),
),
);
})
.map((final message) => LogListItem(message: message))
.toList()
.reversed,
),

View File

@ -1,3 +1,4 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:ionicons/ionicons.dart';
@ -5,23 +6,13 @@ import 'package:selfprivacy/config/brand_theme.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
import 'package:selfprivacy/logic/cubit/server_volumes/server_volume_cubit.dart';
import 'package:selfprivacy/logic/cubit/services/services_cubit.dart';
import 'package:selfprivacy/ui/components/brand_cards/filled_card.dart';
import 'package:selfprivacy/ui/components/cards/filled_card.dart';
import 'package:selfprivacy/ui/components/brand_header/brand_header.dart';
import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart';
import 'package:selfprivacy/ui/pages/devices/devices.dart';
import 'package:selfprivacy/ui/pages/recovery_key/recovery_key.dart';
import 'package:selfprivacy/ui/pages/server_storage/binds_migration/services_migration.dart';
import 'package:selfprivacy/ui/pages/setup/initializing/initializing.dart';
import 'package:selfprivacy/ui/pages/onboarding/onboarding.dart';
import 'package:selfprivacy/ui/pages/root_route.dart';
import 'package:selfprivacy/ui/pages/users/users.dart';
import 'package:selfprivacy/utils/route_transitions/basic.dart';
import 'package:selfprivacy/ui/pages/more/about_us.dart';
import 'package:selfprivacy/ui/pages/more/app_settings/app_setting.dart';
import 'package:selfprivacy/ui/pages/more/console.dart';
import 'package:selfprivacy/ui/pages/more/about_application.dart';
import 'package:selfprivacy/utils/breakpoints.dart';
import 'package:selfprivacy/ui/router/router.dart';
@RoutePage()
class MorePage extends StatelessWidget {
const MorePage({super.key});
@ -34,12 +25,14 @@ class MorePage extends StatelessWidget {
context.watch<ApiServerVolumeCubit>().state.usesBinds;
return Scaffold(
appBar: PreferredSize(
preferredSize: const Size.fromHeight(52),
child: BrandHeader(
title: 'basis.more'.tr(),
),
),
appBar: Breakpoints.small.isActive(context)
? PreferredSize(
preferredSize: const Size.fromHeight(52),
child: BrandHeader(
title: 'basis.more'.tr(),
),
)
: null,
body: ListView(
children: [
Padding(
@ -50,7 +43,7 @@ class MorePage extends StatelessWidget {
_MoreMenuItem(
title: 'storage.start_migration_button'.tr(),
iconData: Icons.drive_file_move_outline,
goTo: ServicesMigrationPage(
goTo: () => ServicesMigrationRoute(
diskStatus: context
.watch<ApiServerVolumeCubit>()
.state
@ -77,7 +70,7 @@ class MorePage extends StatelessWidget {
_MoreMenuItem(
title: 'more_page.configuration_wizard'.tr(),
iconData: Icons.change_history_outlined,
goTo: const InitializingPage(),
goTo: () => const InitializingRoute(),
subtitle: 'not_ready_card.in_menu'.tr(),
accent: true,
),
@ -85,47 +78,43 @@ class MorePage extends StatelessWidget {
_MoreMenuItem(
title: 'more_page.create_ssh_key'.tr(),
iconData: Ionicons.key_outline,
goTo: const UserDetails(
goTo: () => UserDetailsRoute(
login: 'root',
),
),
if (isReady)
_MoreMenuItem(
iconData: Icons.password_outlined,
goTo: const RecoveryKey(),
goTo: () => const RecoveryKeyRoute(),
title: 'recovery_key.key_main_header'.tr(),
),
if (isReady)
_MoreMenuItem(
iconData: Icons.devices_outlined,
goTo: const DevicesScreen(),
goTo: () => const DevicesRoute(),
title: 'devices.main_screen.header'.tr(),
),
_MoreMenuItem(
title: 'more_page.application_settings'.tr(),
iconData: Icons.settings_outlined,
goTo: const AppSettingsPage(),
),
_MoreMenuItem(
title: 'more_page.about_project'.tr(),
iconData: BrandIcons.engineer,
goTo: const AboutUsPage(),
goTo: () => const AppSettingsRoute(),
),
_MoreMenuItem(
title: 'more_page.about_application'.tr(),
iconData: BrandIcons.fire,
goTo: const AboutApplicationPage(),
goTo: () => const AboutApplicationRoute(),
longGoTo: const DeveloperSettingsRoute(),
),
if (!isReady)
_MoreMenuItem(
title: 'more_page.onboarding'.tr(),
iconData: BrandIcons.start,
goTo: const OnboardingPage(nextPage: RootPage()),
goTo: () => const OnboardingRoute(),
),
_MoreMenuItem(
title: 'more_page.console'.tr(),
iconData: BrandIcons.terminal,
goTo: const Console(),
goTo: () => const ConsoleRoute(),
),
],
),
@ -140,14 +129,16 @@ class _MoreMenuItem extends StatelessWidget {
const _MoreMenuItem({
required this.iconData,
required this.title,
required this.goTo,
this.subtitle,
this.goTo,
this.longGoTo,
this.accent = false,
});
final IconData iconData;
final String title;
final Widget? goTo;
final PageRouteInfo Function() goTo;
final PageRouteInfo? longGoTo;
final String? subtitle;
final bool accent;
@ -160,9 +151,9 @@ class _MoreMenuItem extends StatelessWidget {
tertiary: accent,
child: ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
onTap: goTo != null
? () => Navigator.of(context).push(materialRoute(goTo!))
: null,
onTap: () => context.pushRoute(goTo()),
onLongPress:
longGoTo != null ? () => context.pushRoute(longGoTo!) : null,
leading: Icon(
iconData,
size: 24,

View File

@ -1,13 +1,14 @@
import 'package:auto_route/auto_route.dart';
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/utils/route_transitions/basic.dart';
import 'package:selfprivacy/ui/components/buttons/brand_button.dart';
import 'package:selfprivacy/ui/router/router.dart';
import 'package:easy_localization/easy_localization.dart';
@RoutePage()
class OnboardingPage extends StatefulWidget {
const OnboardingPage({required this.nextPage, super.key});
const OnboardingPage({super.key});
final Widget nextPage;
@override
State<OnboardingPage> createState() => _OnboardingPageState();
}
@ -22,14 +23,14 @@ class _OnboardingPageState extends State<OnboardingPage> {
@override
Widget build(final BuildContext context) => Scaffold(
body: PageView(
controller: pageController,
children: [
_withPadding(firstPage()),
_withPadding(secondPage()),
],
),
);
body: PageView(
controller: pageController,
children: [
_withPadding(firstPage()),
_withPadding(secondPage()),
],
),
);
Widget _withPadding(final Widget child) => Padding(
padding: const EdgeInsets.symmetric(
@ -76,7 +77,7 @@ class _OnboardingPageState extends State<OnboardingPage> {
pageController.animateToPage(
1,
duration: const Duration(milliseconds: 300),
curve: Curves.easeIn,
curve: Curves.easeInOutCubicEmphasized,
);
},
text: 'basis.next'.tr(),
@ -142,10 +143,10 @@ class _OnboardingPageState extends State<OnboardingPage> {
BrandButton.rised(
onPressed: () {
context.read<AppSettingsCubit>().turnOffOnboarding();
Navigator.of(context).pushAndRemoveUntil(
materialRoute(widget.nextPage),
(final route) => false,
);
context.router.replaceAll([
const RootRoute(),
const InitializingRoute(),
]);
},
text: 'basis.got_it'.tr(),
),

View File

@ -1,3 +1,4 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:selfprivacy/config/brand_theme.dart';
@ -10,13 +11,12 @@ import 'package:selfprivacy/ui/components/brand_header/brand_header.dart';
import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart';
import 'package:selfprivacy/ui/components/icon_status_mask/icon_status_mask.dart';
import 'package:selfprivacy/ui/components/not_ready_card/not_ready_card.dart';
import 'package:selfprivacy/ui/pages/backup_details/backup_details.dart';
import 'package:selfprivacy/ui/pages/dns_details/dns_details.dart';
import 'package:selfprivacy/ui/pages/server_details/server_details_screen.dart';
import 'package:selfprivacy/utils/route_transitions/basic.dart';
import 'package:selfprivacy/ui/router/router.dart';
import 'package:selfprivacy/utils/breakpoints.dart';
GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
@RoutePage()
class ProvidersPage extends StatefulWidget {
const ProvidersPage({super.key});
@ -61,12 +61,14 @@ class _ProvidersPageState extends State<ProvidersPage> {
}
return Scaffold(
appBar: PreferredSize(
preferredSize: const Size.fromHeight(52),
child: BrandHeader(
title: 'basis.providers_title'.tr(),
),
),
appBar: Breakpoints.small.isActive(context)
? PreferredSize(
preferredSize: const Size.fromHeight(52),
child: BrandHeader(
title: 'basis.providers_title'.tr(),
),
)
: null,
body: ListView(
padding: paddingH15V0,
children: [
@ -81,8 +83,7 @@ class _ProvidersPageState extends State<ProvidersPage> {
subtitle: diskStatus.isDiskOkay
? 'storage.status_ok'.tr()
: 'storage.status_error'.tr(),
onTap: () => Navigator.of(context)
.push(materialRoute(const ServerDetailsScreen())),
onTap: () => context.pushRoute(const ServerDetailsRoute()),
),
const SizedBox(height: 16),
_Card(
@ -92,11 +93,7 @@ class _ProvidersPageState extends State<ProvidersPage> {
subtitle: appConfig.isDomainSelected
? appConfig.serverDomain!.domainName
: '',
onTap: () => Navigator.of(context).push(
materialRoute(
const DnsDetailsPage(),
),
),
onTap: () => context.pushRoute(const DnsDetailsRoute()),
),
const SizedBox(height: 16),
// TODO: When backups are fixed, show this card
@ -108,8 +105,7 @@ class _ProvidersPageState extends State<ProvidersPage> {
icon: BrandIcons.save,
title: 'backup.card_title'.tr(),
subtitle: isBackupInitialized ? 'backup.card_subtitle'.tr() : '',
onTap: () => Navigator.of(context)
.push(materialRoute(const BackupDetails())),
onTap: () => context.pushRoute(const BackupDetailsRoute()),
),
],
),

View File

@ -1,4 +1,4 @@
import 'package:cubit_form/cubit_form.dart';
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@ -6,20 +6,21 @@ import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/common_enum/common_enum.dart';
import 'package:selfprivacy/logic/cubit/recovery_key/recovery_key_cubit.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
import 'package:selfprivacy/ui/components/brand_button/brand_button.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/components/buttons/brand_button.dart';
import 'package:selfprivacy/ui/components/cards/filled_card.dart';
import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart';
import 'package:selfprivacy/ui/pages/recovery_key/recovery_key_receiving.dart';
import 'package:selfprivacy/utils/route_transitions/basic.dart';
class RecoveryKey extends StatefulWidget {
const RecoveryKey({super.key});
@RoutePage()
class RecoveryKeyPage extends StatefulWidget {
const RecoveryKeyPage({super.key});
@override
State<RecoveryKey> createState() => _RecoveryKeyState();
State<RecoveryKeyPage> createState() => _RecoveryKeyPageState();
}
class _RecoveryKeyState extends State<RecoveryKey> {
class _RecoveryKeyPageState extends State<RecoveryKeyPage> {
@override
void initState() {
super.initState();
@ -250,7 +251,7 @@ class _RecoveryKeyConfigurationState extends State<RecoveryKeyConfiguration> {
setState(() {
_isLoading = false;
});
Navigator.of(context).push(
await Navigator.of(context).push(
materialRoute(
RecoveryKeyReceiving(recoveryKey: token), // TO DO
),

View File

@ -1,7 +1,7 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:selfprivacy/ui/components/brand_button/brand_button.dart';
import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart';
import 'package:selfprivacy/ui/components/buttons/brand_button.dart';
import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart';
import 'package:selfprivacy/ui/components/info_box/info_box.dart';
class RecoveryKeyReceiving extends StatelessWidget {

View File

@ -1,89 +1,153 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:selfprivacy/logic/cubit/app_settings/app_settings_cubit.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
import 'package:selfprivacy/ui/components/brand_tab_bar/brand_tab_bar.dart';
import 'package:selfprivacy/ui/pages/more/more.dart';
import 'package:selfprivacy/ui/pages/providers/providers.dart';
import 'package:selfprivacy/ui/pages/services/services.dart';
import 'package:selfprivacy/ui/pages/users/users.dart';
import 'package:selfprivacy/ui/layouts/root_scaffold_with_navigation.dart';
import 'package:selfprivacy/ui/router/root_destinations.dart';
import 'package:selfprivacy/ui/components/pre_styled_buttons/flash_fab.dart';
import 'package:selfprivacy/ui/router/router.dart';
class RootPage extends StatefulWidget {
@RoutePage()
class RootPage extends StatefulWidget implements AutoRouteWrapper {
const RootPage({super.key});
@override
State<RootPage> createState() => _RootPageState();
@override
Widget wrappedRoute(final BuildContext context) => this;
}
class _RootPageState extends State<RootPage> with TickerProviderStateMixin {
late TabController tabController;
bool shouldUseSplitView() => false;
late final AnimationController _controller = AnimationController(
duration: const Duration(milliseconds: 400),
vsync: this,
);
late final Animation<double> _animation = CurvedAnimation(
parent: _controller,
curve: Curves.fastOutSlowIn,
);
@override
void initState() {
tabController = TabController(length: 4, vsync: this);
tabController.addListener(() {
setState(() {
tabController.index == 2
? _controller.forward()
: _controller.reverse();
});
});
super.initState();
}
@override
void dispose() {
tabController.dispose();
_controller.dispose();
super.dispose();
}
final destinations = rootDestinations;
@override
Widget build(final BuildContext context) {
final bool isReady = context.watch<ServerInstallationCubit>().state
is ServerInstallationFinished;
return Provider<ChangeTab>(
create: (final _) => ChangeTab(tabController.animateTo),
child: Scaffold(
body: TabBarView(
controller: tabController,
children: const [
ProvidersPage(),
ServicesPage(),
UsersPage(),
MorePage(),
if (context.read<AppSettingsCubit>().state.isOnboardingShowing) {
context.router.replace(const OnboardingRoute());
}
return AutoRouter(
builder: (final context, final child) {
final currentDestinationIndex = destinations.indexWhere(
(final destination) =>
context.router.isRouteActive(destination.route.routeName),
);
final isOtherRouterActive =
context.router.root.current.name != RootRoute.name;
final routeName = getRouteTitle(context.router.current.name).tr();
return RootScaffoldWithNavigation(
title: routeName,
destinations: destinations,
showBottomBar:
!(currentDestinationIndex == -1 && !isOtherRouterActive),
showFab: isReady,
child: child,
);
},
);
}
}
class MainScreenNavigationRail extends StatelessWidget {
const MainScreenNavigationRail({
required this.destinations,
super.key,
});
final List<RouteDestination> destinations;
@override
Widget build(final BuildContext context) {
int? activeIndex = destinations.indexWhere(
(final destination) =>
context.router.isRouteActive(destination.route.routeName),
);
if (activeIndex == -1) {
activeIndex = null;
}
return Padding(
padding: const EdgeInsets.all(8.0),
child: SizedBox(
height: MediaQuery.of(context).size.height,
width: 72,
child: LayoutBuilder(
builder: (final context, final constraints) => SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(minHeight: constraints.maxHeight),
child: IntrinsicHeight(
child: NavigationRail(
backgroundColor: Colors.transparent,
labelType: NavigationRailLabelType.all,
destinations: destinations
.map(
(final destination) => NavigationRailDestination(
icon: Icon(destination.icon),
label: Text(destination.label),
),
)
.toList(),
selectedIndex: activeIndex,
onDestinationSelected: (final index) {
context.router.replaceAll([destinations[index].route]);
},
),
),
),
),
),
),
);
}
}
class MainScreenNavigationDrawer extends StatelessWidget {
const MainScreenNavigationDrawer({
required this.destinations,
super.key,
});
final List<RouteDestination> destinations;
@override
Widget build(final BuildContext context) {
int? activeIndex = destinations.indexWhere(
(final destination) =>
context.router.isRouteActive(destination.route.routeName),
);
if (activeIndex == -1) {
activeIndex = null;
}
return SizedBox(
height: MediaQuery.of(context).size.height,
width: 296,
child: LayoutBuilder(
builder: (final context, final constraints) => NavigationDrawer(
// backgroundColor: Theme.of(context).colorScheme.surfaceVariant,
// surfaceTintColor: Colors.transparent,
key: const Key('PrimaryNavigationDrawer'),
selectedIndex: activeIndex,
onDestinationSelected: (final index) {
context.router.replaceAll([destinations[index].route]);
},
children: [
const SizedBox(height: 18),
...destinations.map(
(final destination) => NavigationDrawerDestination(
icon: Icon(destination.icon),
label: Text(destination.label),
),
),
],
),
bottomNavigationBar: BrandTabBar(
controller: tabController,
),
floatingActionButton: isReady
? SizedBox(
height: 104 + 16,
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.end,
children: [
ScaleTransition(
scale: _animation,
child: const AddUserFab(),
),
const SizedBox(height: 16),
const BrandFab(),
],
),
)
: null,
),
);
}

View File

@ -1,7 +1,7 @@
import 'package:auto_route/auto_route.dart';
import 'package:cubit_form/cubit_form.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:selfprivacy/config/brand_colors.dart';
import 'package:selfprivacy/logic/common_enum/common_enum.dart';
import 'package:selfprivacy/logic/cubit/client_jobs/client_jobs_cubit.dart';
import 'package:selfprivacy/logic/cubit/metrics/metrics_cubit.dart';
@ -10,18 +10,17 @@ import 'package:selfprivacy/logic/cubit/server_installation/server_installation_
import 'package:selfprivacy/logic/cubit/server_volumes/server_volume_cubit.dart';
import 'package:selfprivacy/logic/models/auto_upgrade_settings.dart';
import 'package:selfprivacy/logic/models/job.dart';
import 'package:selfprivacy/ui/components/brand_button/segmented_buttons.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/components/buttons/segmented_buttons.dart';
import 'package:selfprivacy/ui/components/cards/filled_card.dart';
import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart';
import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart';
import 'package:selfprivacy/ui/components/brand_loader/brand_loader.dart';
import 'package:selfprivacy/ui/components/brand_text/brand_text.dart';
import 'package:selfprivacy/ui/components/list_tiles/list_tile_on_surface_variant.dart';
import 'package:selfprivacy/ui/pages/server_details/charts/cpu_chart.dart';
import 'package:selfprivacy/ui/pages/server_details/charts/network_charts.dart';
import 'package:selfprivacy/ui/pages/server_storage/storage_card.dart';
import 'package:selfprivacy/utils/breakpoints.dart';
import 'package:selfprivacy/utils/extensions/duration.dart';
import 'package:selfprivacy/utils/named_font_weight.dart';
import 'package:selfprivacy/utils/route_transitions/basic.dart';
import 'package:timezone/timezone.dart';
@ -32,6 +31,7 @@ part 'time_zone/time_zone.dart';
var navigatorKey = GlobalKey<NavigatorState>();
@RoutePage()
class ServerDetailsScreen extends StatefulWidget {
const ServerDetailsScreen({super.key});
@ -75,6 +75,7 @@ class _ServerDetailsScreenState extends State<ServerDetailsScreen>
return BlocProvider(
create: (final context) => context.read<ServerDetailsCubit>()..check(),
child: BrandHeroScreen(
hasFlashButton: true,
heroIcon: BrandIcons.server,
heroTitle: 'server.card_title'.tr(),
heroSubtitle: 'server.description'.tr(),

View File

@ -23,15 +23,13 @@ class _TextDetails extends StatelessWidget {
),
),
),
...details.metadata
.map(
(final metadata) => ListTileOnSurfaceVariant(
leadingIcon: metadata.type.icon,
title: metadata.trId.tr(),
subtitle: metadata.value,
),
)
.toList(),
...details.metadata.map(
(final metadata) => ListTileOnSurfaceVariant(
leadingIcon: metadata.type.icon,
title: metadata.trId.tr(),
subtitle: metadata.value,
),
),
],
),
);
@ -39,24 +37,6 @@ class _TextDetails extends StatelessWidget {
throw Exception('wrong state');
}
}
Widget getRowTitle(final String title) => Padding(
padding: const EdgeInsets.only(right: 10),
child: BrandText.h5(
title,
textAlign: TextAlign.right,
),
);
Widget getRowValue(final String title, {final bool isBold = false}) =>
BrandText.body1(
title,
style: isBold
? const TextStyle(
fontWeight: NamedFontWeight.demiBold,
)
: null,
);
}
class _TempMessage extends StatelessWidget {
@ -69,7 +49,10 @@ class _TempMessage extends StatelessWidget {
Widget build(final BuildContext context) => SizedBox(
height: MediaQuery.of(context).size.height - 100,
child: Center(
child: BrandText.body2(message),
child: Text(
message,
style: Theme.of(context).textTheme.bodyMedium,
),
),
);
}

View File

@ -57,66 +57,72 @@ class _SelectTimezoneState extends State<SelectTimezone> {
}
@override
Widget build(final BuildContext context) => Scaffold(
appBar: AppBar(
title: isSearching
? TextField(
readOnly: false,
textAlign: TextAlign.start,
textInputAction: TextInputAction.next,
enabled: true,
controller: searchController,
decoration: InputDecoration(
errorText: null,
hintText: 'server.timezone_search_bar'.tr(),
),
)
: Padding(
padding: const EdgeInsets.only(top: 4.0),
child: Text('server.select_timezone'.tr()),
Widget build(final BuildContext context) {
final isDesktop = Breakpoints.mediumAndUp.isActive(context);
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
title: (isDesktop || isSearching)
? TextField(
readOnly: false,
textAlign: TextAlign.start,
textInputAction: TextInputAction.next,
enabled: true,
controller: searchController,
decoration: InputDecoration(
errorText: null,
hintText: 'server.timezone_search_bar'.tr(),
),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: isSearching
? () => setState(() => isSearching = false)
: () => Navigator.of(context).pop(),
),
actions: [
if (!isSearching)
IconButton(
icon: const Icon(Icons.search),
onPressed: () => setState(() => isSearching = true),
)
: Padding(
padding: const EdgeInsets.only(top: 4.0),
child: Text('server.select_timezone'.tr()),
),
],
leading: !isDesktop
? IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: isSearching
? () => setState(() => isSearching = false)
: () => Navigator.of(context).pop(),
)
: null,
actions: [
if (!isSearching && !isDesktop)
IconButton(
icon: const Icon(Icons.search),
onPressed: () => setState(() => isSearching = true),
),
],
),
body: SafeArea(
child: ListView(
controller: scrollController,
children: locations
.where(
(final Location location) => timezoneFilterValue == null
? true
: location.name
.toLowerCase()
.contains(timezoneFilterValue!) ||
Duration(
milliseconds: location.currentTimeZone.offset,
)
.toDayHourMinuteFormat()
.contains(timezoneFilterValue!),
)
.toList()
.asMap()
.map(
(final key, final value) => locationToListTile(key, value),
)
.values
.toList(),
),
body: SafeArea(
child: ListView(
controller: scrollController,
children: locations
.where(
(final Location location) => timezoneFilterValue == null
? true
: location.name
.toLowerCase()
.contains(timezoneFilterValue!) ||
Duration(
milliseconds: location.currentTimeZone.offset,
)
.toDayHourMinuteFormat()
.contains(timezoneFilterValue!),
)
.toList()
.asMap()
.map(
(final key, final value) => locationToListTile(key, value),
)
.values
.toList(),
),
),
);
),
);
}
MapEntry<int, Container> locationToListTile(
MapEntry<int, ListTile> locationToListTile(
final int key,
final Location location,
) {
@ -126,46 +132,19 @@ class _SelectTimezoneState extends State<SelectTimezone> {
return MapEntry(
key,
Container(
height: 75,
padding: const EdgeInsets.symmetric(horizontal: 20),
decoration: const BoxDecoration(
border: Border(
bottom: BorderSide(
color: BrandColors.dividerColor,
),
),
ListTile(
title: Text(
location.name,
),
child: InkWell(
onTap: () {
context.read<ServerDetailsCubit>().repository.setTimezone(
location.name,
);
Navigator.of(context).pop();
},
child: Container(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
BrandText.body1(
location.name,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
BrandText.small(
'GMT ${duration.toDayHourMinuteFormat()} ${area.isNotEmpty ? '($area)' : ''}',
style: const TextStyle(
fontSize: 13,
),
),
],
),
),
subtitle: Text(
'GMT ${duration.toDayHourMinuteFormat()} ${area.isNotEmpty ? '($area)' : ''}',
),
onTap: () {
context.read<ServerDetailsCubit>().repository.setTimezone(
location.name,
);
Navigator.of(context).pop();
},
),
);
}

View File

@ -2,8 +2,8 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:selfprivacy/logic/cubit/server_jobs/server_jobs_cubit.dart';
import 'package:selfprivacy/logic/models/json/server_job.dart';
import 'package:selfprivacy/ui/components/brand_button/brand_button.dart';
import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart';
import 'package:selfprivacy/ui/components/buttons/brand_button.dart';
import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart';
import 'package:selfprivacy/ui/components/brand_linear_indicator/brand_linear_indicator.dart';
import 'package:selfprivacy/ui/pages/root_route.dart';
import 'package:selfprivacy/utils/route_transitions/basic.dart';

View File

@ -1,21 +1,19 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:selfprivacy/logic/cubit/server_jobs/server_jobs_cubit.dart';
import 'package:selfprivacy/logic/cubit/services/services_cubit.dart';
import 'package:selfprivacy/logic/models/disk_size.dart';
import 'package:selfprivacy/logic/models/service.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/buttons/brand_button.dart';
import 'package:selfprivacy/ui/components/brand_header/brand_header.dart';
import 'package:selfprivacy/ui/components/info_box/info_box.dart';
import 'package:selfprivacy/logic/models/disk_status.dart';
import 'package:selfprivacy/ui/components/jobs_content/jobs_content.dart';
import 'package:selfprivacy/ui/components/storage_list_items/server_storage_list_item.dart';
import 'package:selfprivacy/ui/components/storage_list_items/service_migration_list_item.dart';
import 'package:selfprivacy/ui/helpers/modals.dart';
import 'package:selfprivacy/ui/pages/root_route.dart';
import 'package:selfprivacy/utils/route_transitions/basic.dart';
@RoutePage()
class ServicesMigrationPage extends StatefulWidget {
const ServicesMigrationPage({
required this.services,
@ -110,22 +108,20 @@ class _ServicesMigrationPageState extends State<ServicesMigrationPage> {
),
child: Column(
children: [
...widget.diskStatus.diskVolumes
.map(
(final volume) => Column(
children: [
ServerStorageListItem(
volume: recalculatedDiskUsages(
volume,
widget.services,
),
dense: true,
),
const SizedBox(height: headerVerticalPadding),
],
...widget.diskStatus.diskVolumes.map(
(final volume) => Column(
children: [
ServerStorageListItem(
volume: recalculatedDiskUsages(
volume,
widget.services,
),
dense: true,
),
)
.toList(),
const SizedBox(height: headerVerticalPadding),
],
),
),
],
),
),
@ -138,23 +134,21 @@ class _ServicesMigrationPageState extends State<ServicesMigrationPage> {
children: <Widget>[
if (widget.services.isEmpty)
const Center(child: CircularProgressIndicator()),
...widget.services
.map(
(final service) => Column(
children: [
const SizedBox(height: 8),
ServiceMigrationListItem(
service: service,
diskStatus: widget.diskStatus,
selectedVolume: serviceToDisk[service.id]!,
onChange: onChange,
),
const SizedBox(height: 4),
const Divider(),
],
...widget.services.map(
(final service) => Column(
children: [
const SizedBox(height: 8),
ServiceMigrationListItem(
service: service,
diskStatus: widget.diskStatus,
selectedVolume: serviceToDisk[service.id]!,
onChange: onChange,
),
)
.toList(),
const SizedBox(height: 4),
const Divider(),
],
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: InfoBox(
@ -180,17 +174,10 @@ class _ServicesMigrationPageState extends State<ServicesMigrationPage> {
}
}
}
Navigator.of(context).pushAndRemoveUntil(
materialRoute(const RootPage()),
(final predicate) => false,
);
showBrandBottomSheet(
context.router.popUntilRoot();
showModalBottomSheet(
context: context,
builder: (final BuildContext context) =>
const BrandBottomSheet(
isExpended: true,
child: JobsContent(),
),
builder: (final BuildContext context) => const JobsContent(),
);
},
),

View File

@ -1,3 +1,4 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart';
@ -5,12 +6,11 @@ import 'package:selfprivacy/logic/cubit/provider_volumes/provider_volume_cubit.d
import 'package:selfprivacy/logic/cubit/server_volumes/server_volume_cubit.dart';
import 'package:selfprivacy/logic/models/disk_size.dart';
import 'package:selfprivacy/logic/models/price.dart';
import 'package:selfprivacy/ui/components/brand_button/brand_button.dart';
import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart';
import 'package:selfprivacy/ui/components/buttons/brand_button.dart';
import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart';
import 'package:selfprivacy/logic/models/disk_status.dart';
import 'package:selfprivacy/ui/pages/root_route.dart';
import 'package:selfprivacy/utils/route_transitions/basic.dart';
@RoutePage()
class ExtendingVolumePage extends StatefulWidget {
const ExtendingVolumePage({
required this.diskVolumeToResize,
@ -155,10 +155,7 @@ class _ExtendingVolumePageState extends State<ExtendingVolumePage> {
DiskSize.fromGibibyte(_currentSliderGbValue),
context.read<ApiServerVolumeCubit>().reload,
);
Navigator.of(context).pushAndRemoveUntil(
materialRoute(const RootPage()),
(final predicate) => false,
);
context.router.popUntilRoot();
},
child: Text('storage.extend_volume_button.title'.tr()),
),

View File

@ -1,16 +1,17 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
import 'package:selfprivacy/logic/cubit/services/services_cubit.dart';
import 'package:selfprivacy/logic/models/service.dart';
import 'package:selfprivacy/ui/components/brand_button/outlined_button.dart';
import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart';
import 'package:selfprivacy/ui/components/buttons/outlined_button.dart';
import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart';
import 'package:selfprivacy/logic/models/disk_status.dart';
import 'package:selfprivacy/ui/pages/server_storage/extending_volume.dart';
import 'package:selfprivacy/ui/components/storage_list_items/server_storage_list_item.dart';
import 'package:selfprivacy/utils/route_transitions/basic.dart';
import 'package:selfprivacy/ui/router/router.dart';
@RoutePage()
class ServerStoragePage extends StatefulWidget {
const ServerStoragePage({
required this.diskStatus,
@ -45,28 +46,26 @@ class _ServerStoragePageState extends State<ServerStoragePage> {
heroTitle: 'storage.card_title'.tr(),
children: [
// ...sections,
...widget.diskStatus.diskVolumes
.map(
(final volume) => Column(
mainAxisSize: MainAxisSize.min,
children: [
ServerStorageSection(
volume: volume,
diskStatus: widget.diskStatus,
services: services
.where(
(final service) =>
service.storageUsage.volume == volume.name,
)
.toList(),
),
const SizedBox(height: 16),
const Divider(),
const SizedBox(height: 16),
],
...widget.diskStatus.diskVolumes.map(
(final volume) => Column(
mainAxisSize: MainAxisSize.min,
children: [
ServerStorageSection(
volume: volume,
diskStatus: widget.diskStatus,
services: services
.where(
(final service) =>
service.storageUsage.volume == volume.name,
)
.toList(),
),
)
.toList(),
const SizedBox(height: 16),
const Divider(),
const SizedBox(height: 16),
],
),
),
const SizedBox(height: 8),
],
);
@ -93,24 +92,20 @@ class ServerStorageSection extends StatelessWidget {
volume: volume,
),
const SizedBox(height: 16),
...services
.map(
(final service) => ServerConsumptionListTile(
service: service,
volume: volume,
),
)
.toList(),
...services.map(
(final service) => ServerConsumptionListTile(
service: service,
volume: volume,
),
),
if (volume.isResizable) ...[
const SizedBox(height: 16),
BrandOutlinedButton(
title: 'storage.extend_volume_button.title'.tr(),
onPressed: () => Navigator.of(context).push(
materialRoute(
ExtendingVolumePage(
diskVolumeToResize: volume,
diskStatus: diskStatus,
),
onPressed: () => context.pushRoute(
ExtendingVolumeRoute(
diskVolumeToResize: volume,
diskStatus: diskStatus,
),
),
),
@ -138,7 +133,10 @@ class ServerConsumptionListTile extends StatelessWidget {
service.svgIcon,
width: 24.0,
height: 24.0,
color: Theme.of(context).colorScheme.onBackground,
colorFilter: ColorFilter.mode(
Theme.of(context).colorScheme.onBackground,
BlendMode.srcIn,
),
),
rightSideText: service.storageUsage.used.toString(),
percentage: service.storageUsage.used.byte / volume.sizeTotal.byte,

View File

@ -1,12 +1,12 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart';
import 'package:selfprivacy/logic/cubit/providers/providers_cubit.dart';
import 'package:selfprivacy/ui/components/icon_status_mask/icon_status_mask.dart';
import 'package:selfprivacy/logic/models/disk_status.dart';
import 'package:selfprivacy/ui/pages/server_storage/server_storage.dart';
import 'package:selfprivacy/ui/components/storage_list_items/server_storage_list_item.dart';
import 'package:selfprivacy/utils/route_transitions/basic.dart';
import 'package:selfprivacy/ui/router/router.dart';
class StorageCard extends StatelessWidget {
const StorageCard({
@ -45,13 +45,8 @@ class StorageCard extends StatelessWidget {
clipBehavior: Clip.antiAlias,
child: InkResponse(
highlightShape: BoxShape.rectangle,
onTap: () => Navigator.of(context).push(
materialRoute(
ServerStoragePage(
diskStatus: diskStatus,
),
),
),
onTap: () =>
context.pushRoute(ServerStorageRoute(diskStatus: diskStatus)),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(

View File

@ -1,3 +1,4 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
@ -6,12 +7,12 @@ import 'package:selfprivacy/logic/cubit/server_volumes/server_volume_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/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/ui/components/cards/filled_card.dart';
import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart';
import 'package:selfprivacy/ui/router/router.dart';
import 'package:selfprivacy/utils/launch_url.dart';
import 'package:selfprivacy/utils/route_transitions/basic.dart';
@RoutePage()
class ServicePage extends StatefulWidget {
const ServicePage({required this.serviceId, super.key});
@ -46,11 +47,15 @@ class _ServicePageState extends State<ServicePage> {
return BrandHeroScreen(
hasBackButton: true,
hasFlashButton: true,
heroIconWidget: SvgPicture.string(
service.svgIcon,
width: 48.0,
height: 48.0,
color: Theme.of(context).colorScheme.onBackground,
colorFilter: ColorFilter.mode(
Theme.of(context).colorScheme.onBackground,
BlendMode.srcIn,
),
),
heroTitle: service.displayName,
children: [
@ -108,14 +113,12 @@ class _ServicePageState extends State<ServicePage> {
ListTile(
iconColor: Theme.of(context).colorScheme.onBackground,
// Open page ServicesMigrationPage
onTap: () => Navigator.of(context).push(
materialRoute(
ServicesMigrationPage(
services: [service],
diskStatus:
context.read<ApiServerVolumeCubit>().state.diskStatus,
isMigration: false,
),
onTap: () => context.pushRoute(
ServicesMigrationRoute(
services: [service],
diskStatus:
context.read<ApiServerVolumeCubit>().state.diskStatus,
isMigration: false,
),
),
leading: const Icon(Icons.drive_file_move_outlined),

View File

@ -1,3 +1,4 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:selfprivacy/config/brand_theme.dart';
@ -5,17 +6,16 @@ import 'package:selfprivacy/logic/cubit/server_installation/server_installation_
import 'package:selfprivacy/logic/cubit/services/services_cubit.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_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/ui/router/router.dart';
import 'package:selfprivacy/utils/breakpoints.dart';
import 'package:selfprivacy/utils/launch_url.dart';
import 'package:selfprivacy/utils/route_transitions/basic.dart';
import 'package:selfprivacy/utils/ui_helpers.dart';
@RoutePage()
class ServicesPage extends StatefulWidget {
const ServicesPage({super.key});
@ -34,32 +34,35 @@ class _ServicesPageState extends State<ServicesPage> {
.sort((final a, final b) => a.status.index.compareTo(b.status.index));
return Scaffold(
appBar: PreferredSize(
preferredSize: const Size.fromHeight(52),
child: BrandHeader(
title: 'basis.services'.tr(),
),
),
appBar: Breakpoints.small.isActive(context)
? PreferredSize(
preferredSize: const Size.fromHeight(52),
child: BrandHeader(
title: 'basis.services'.tr(),
),
)
: null,
body: RefreshIndicator(
onRefresh: () async {
context.read<ServicesCubit>().reload();
await context.read<ServicesCubit>().reload();
},
child: ListView(
padding: paddingH15V0,
children: [
BrandText.body1('basis.services_title'.tr()),
Text(
'basis.services_title'.tr(),
style: Theme.of(context).textTheme.bodyLarge,
),
const SizedBox(height: 24),
if (!isReady) ...[const NotReadyCard(), const SizedBox(height: 24)],
...services
.map(
(final service) => Padding(
padding: const EdgeInsets.only(
bottom: 30,
),
child: _Card(service: service),
),
)
.toList()
...services.map(
(final service) => Padding(
padding: const EdgeInsets.only(
bottom: 30,
),
child: _Card(service: service),
),
)
],
),
),
@ -98,81 +101,106 @@ class _Card extends StatelessWidget {
}
}
return GestureDetector(
onTap: isReady
? () => Navigator.of(context)
.push(materialRoute(ServicePage(serviceId: service.id)))
: null,
child: BrandCards.big(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
IconStatusMask(
status: getStatus(service.status),
icon: SvgPicture.string(
service.svgIcon,
width: 30.0,
height: 30.0,
color: Theme.of(context).colorScheme.onBackground,
),
),
],
),
ClipRect(
child: Stack(
return Card(
clipBehavior: Clip.antiAlias,
child: InkResponse(
highlightShape: BoxShape.rectangle,
onTap: isReady
? () => context.pushRoute(
ServiceRoute(serviceId: service.id),
)
: null,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 10),
BrandText.h2(service.displayName),
const SizedBox(height: 10),
if (service.url != '' && service.url != null)
Column(
children: [
GestureDetector(
onTap: () => launchURL(
service.url,
),
child: Text(
'${service.url}',
style: TextStyle(
color:
Theme.of(context).colorScheme.secondary,
decoration: TextDecoration.underline,
),
),
),
const SizedBox(height: 10),
],
),
if (service.id == 'mailserver')
Column(
children: [
Text(
domainName,
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
decoration: TextDecoration.underline,
),
),
const SizedBox(height: 10),
],
),
BrandText.body2(service.loginInfo),
const SizedBox(height: 10),
BrandText.body2(service.description),
const SizedBox(height: 10),
],
IconStatusMask(
status: getStatus(service.status),
icon: SvgPicture.string(
service.svgIcon,
width: 30.0,
height: 30.0,
colorFilter: const ColorFilter.mode(
Colors.white,
BlendMode.srcIn,
),
),
),
],
),
)
],
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 12),
Text(
service.displayName,
style: Theme.of(context).textTheme.headlineMedium,
),
const SizedBox(height: 8),
if (service.url != '' && service.url != null)
Column(
children: [
_ServiceLink(
url: service.url ?? '',
),
const SizedBox(height: 10),
],
),
if (service.id == 'mailserver')
Column(
children: [
_ServiceLink(
url: domainName,
isActive: false,
),
const SizedBox(height: 10),
],
),
Text(
service.loginInfo,
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 10),
Text(
service.description,
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 10),
],
)
],
),
),
),
);
}
}
class _ServiceLink extends StatelessWidget {
const _ServiceLink({
required this.url,
this.isActive = true,
});
final String url;
final bool isActive;
@override
Widget build(final BuildContext context) => GestureDetector(
onTap: isActive
? () => launchURL(
url,
)
: null,
child: Text(
url,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.primary,
decoration: TextDecoration.underline,
),
),
);
}

View File

@ -6,11 +6,10 @@ 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_domain.dart';
import 'package:selfprivacy/ui/components/brand_button/brand_button.dart';
import 'package:selfprivacy/ui/components/brand_bottom_sheet/brand_bottom_sheet.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/buttons/brand_button.dart';
import 'package:selfprivacy/ui/components/buttons/outlined_button.dart';
import 'package:selfprivacy/ui/components/cards/outlined_card.dart';
import 'package:selfprivacy/utils/network_utils.dart';
class DnsProviderPicker extends StatefulWidget {
@ -130,18 +129,15 @@ class ProviderInputDataPage extends StatelessWidget {
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (final BuildContext context) => BrandBottomSheet(
isExpended: true,
child: Padding(
padding: paddingH15V0,
child: ListView(
padding: const EdgeInsets.symmetric(vertical: 16),
children: [
BrandMarkdown(
fileName: providerInfo.pathToHow,
),
],
),
builder: (final BuildContext context) => Padding(
padding: paddingH15V0,
child: ListView(
padding: const EdgeInsets.symmetric(vertical: 16),
children: [
BrandMarkdown(
fileName: providerInfo.pathToHow,
),
],
),
),
),

View File

@ -1,7 +1,7 @@
import 'package:auto_route/auto_route.dart';
import 'package:cubit_form/cubit_form.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:selfprivacy/config/brand_theme.dart';
import 'package:selfprivacy/logic/cubit/forms/setup/initializing/server_provider_form_cubit.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
import 'package:selfprivacy/logic/cubit/forms/factories/field_cubit_factory.dart';
@ -9,19 +9,21 @@ import 'package:selfprivacy/logic/cubit/forms/setup/initializing/backblaze_form_
import 'package:selfprivacy/logic/cubit/forms/setup/initializing/dns_provider_form_cubit.dart';
import 'package:selfprivacy/logic/cubit/forms/setup/initializing/domain_setup_cubit.dart';
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_md/brand_md.dart';
import 'package:selfprivacy/ui/components/brand_text/brand_text.dart';
import 'package:selfprivacy/logic/cubit/support_system/support_system_cubit.dart';
import 'package:selfprivacy/ui/components/buttons/brand_button.dart';
import 'package:selfprivacy/ui/components/brand_timer/brand_timer.dart';
import 'package:selfprivacy/ui/components/drawers/progress_drawer.dart';
import 'package:selfprivacy/ui/components/progress_bar/progress_bar.dart';
import 'package:selfprivacy/ui/pages/root_route.dart';
import 'package:selfprivacy/ui/components/drawers/support_drawer.dart';
import 'package:selfprivacy/ui/layouts/responsive_layout_with_infobox.dart';
import 'package:selfprivacy/ui/pages/setup/initializing/dns_provider_picker.dart';
import 'package:selfprivacy/ui/pages/setup/initializing/server_provider_picker.dart';
import 'package:selfprivacy/ui/pages/setup/initializing/server_type_picker.dart';
import 'package:selfprivacy/ui/pages/setup/recovering/recovery_routing.dart';
import 'package:selfprivacy/utils/route_transitions/basic.dart';
import 'package:selfprivacy/ui/router/router.dart';
import 'package:selfprivacy/utils/breakpoints.dart';
@RoutePage()
class InitializingPage extends StatelessWidget {
const InitializingPage({super.key});
@ -49,99 +51,155 @@ class InitializingPage extends StatelessWidget {
][cubit.state.progress.index]();
}
const steps = [
'initializing.steps.hosting',
'initializing.steps.server_type',
'initializing.steps.dns_provider',
'initializing.steps.backups_provider',
'initializing.steps.domain',
'initializing.steps.master_account',
'initializing.steps.server',
'initializing.steps.dns_setup',
'initializing.steps.nixos_installation',
'initializing.steps.server_reboot',
'initializing.steps.final_checks',
];
return BlocListener<ServerInstallationCubit, ServerInstallationState>(
listener: (final context, final state) {
if (cubit.state is ServerInstallationFinished) {
Navigator.of(context)
.pushReplacement(materialRoute(const RootPage()));
context.router.popUntilRoot();
}
},
child: Scaffold(
appBar: AppBar(
actions: [
if (cubit.state is ServerInstallationFinished)
IconButton(
icon: const Icon(Icons.check),
onPressed: () {
Navigator.of(context)
.pushReplacement(materialRoute(const RootPage()));
},
)
],
title: Text(
'more_page.configuration_wizard'.tr(),
),
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,
),
),
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: [
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,
);
},
),
endDrawer: const SupportDrawer(),
endDrawerEnableOpenDragGesture: false,
appBar: Breakpoints.large.isActive(context)
? null
: AppBar(
actions: [
if (cubit.state is ServerInstallationFinished)
IconButton(
icon: const Icon(Icons.check),
onPressed: () {
context.router.popUntilRoot();
},
),
if (cubit.state is ServerInstallationEmpty ||
cubit.state is ServerInstallationNotFinished)
Container(
alignment: Alignment.center,
child: BrandButton.text(
title: 'basis.connect_to_existing'.tr(),
const SizedBox.shrink(),
],
title: Text(
'more_page.configuration_wizard'.tr(),
),
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: LayoutBuilder(
builder: (final context, final constraints) => Row(
children: [
if (Breakpoints.large.isActive(context))
ProgressDrawer(
steps: steps,
currentStep: cubit.state.progress.index,
title: 'more_page.configuration_wizard'.tr(),
constraints: constraints,
trailing: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (cubit.state is ServerInstallationEmpty ||
cubit.state is ServerInstallationNotFinished)
Container(
alignment: Alignment.center,
child: BrandButton.filled(
text: 'basis.connect_to_existing'.tr(),
onPressed: () {
context.router.replace(const RecoveryRoute());
},
),
),
ConstrainedBox(
constraints: const BoxConstraints(
minWidth: double.infinity,
),
child: OutlinedButton(
child: Text(
cubit.state is ServerInstallationFinished
? 'basis.close'.tr()
: 'basis.later'.tr(),
),
onPressed: () {
Navigator.of(context).push(
materialRoute(
const RecoveryRouting(),
),
);
context.router.popUntilRoot();
},
),
)
],
),
],
),
),
SizedBox(
width: constraints.maxWidth -
(Breakpoints.large.isActive(context) ? 300 : 0),
height: constraints.maxHeight,
child: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Padding(
padding: Breakpoints.large.isActive(context)
? const EdgeInsets.all(16.0)
: const EdgeInsets.fromLTRB(16.0, 0, 16.0, 0.0),
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: actualInitializingPage,
),
),
if (!Breakpoints.large.isActive(context))
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
alignment: Alignment.center,
child: BrandButton.text(
title:
cubit.state is ServerInstallationFinished
? 'basis.close'.tr()
: 'basis.later'.tr(),
onPressed: () {
context.router.popUntilRoot();
},
),
),
if (cubit.state is ServerInstallationEmpty ||
cubit.state is ServerInstallationNotFinished)
Container(
alignment: Alignment.center,
child: BrandButton.text(
title: 'basis.connect_to_existing'.tr(),
onPressed: () {
context.router
.replace(const RecoveryRoute());
},
),
)
],
),
],
),
),
),
],
@ -182,15 +240,6 @@ class InitializingPage extends StatelessWidget {
),
);
void _showModal(final BuildContext context, final Widget widget) {
showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (final BuildContext context) => widget,
);
}
Widget _stepDnsProviderToken(
final ServerInstallationCubit initializingCubit,
) =>
@ -213,50 +262,57 @@ class InitializingPage extends StatelessWidget {
child: Builder(
builder: (final context) {
final formCubitState = context.watch<BackblazeFormCubit>().state;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${'initializing.connect_to_server_provider'.tr()}Backblaze',
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 32),
CubitFormTextField(
formFieldCubit: context.read<BackblazeFormCubit>().keyId,
textAlign: TextAlign.center,
scrollPadding: const EdgeInsets.only(bottom: 70),
decoration: const InputDecoration(
hintText: 'KeyID',
return ResponsiveLayoutWithInfobox(
topChild: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${'initializing.connect_to_server_provider'.tr()}Backblaze',
style: Theme.of(context).textTheme.headlineSmall,
),
),
const SizedBox(height: 16),
CubitFormTextField(
formFieldCubit:
context.read<BackblazeFormCubit>().applicationKey,
textAlign: TextAlign.center,
scrollPadding: const EdgeInsets.only(bottom: 70),
decoration: const InputDecoration(
hintText: 'Master Application Key',
),
),
const SizedBox(height: 32),
BrandButton.rised(
onPressed: formCubitState.isSubmitting
? null
: () => context.read<BackblazeFormCubit>().trySubmit(),
text: 'basis.connect'.tr(),
),
const SizedBox(height: 10),
BrandButton.text(
onPressed: () => _showModal(
context,
const _HowTo(
fileName: 'how_backblaze',
],
),
primaryColumn: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CubitFormTextField(
formFieldCubit: context.read<BackblazeFormCubit>().keyId,
textAlign: TextAlign.center,
scrollPadding: const EdgeInsets.only(bottom: 70),
decoration: const InputDecoration(
hintText: 'KeyID',
),
),
title: 'initializing.how'.tr(),
),
],
const SizedBox(height: 16),
CubitFormTextField(
formFieldCubit:
context.read<BackblazeFormCubit>().applicationKey,
textAlign: TextAlign.center,
scrollPadding: const EdgeInsets.only(bottom: 70),
decoration: const InputDecoration(
hintText: 'Master Application Key',
),
),
const SizedBox(height: 32),
BrandButton.rised(
onPressed: formCubitState.isSubmitting
? null
: () => context.read<BackblazeFormCubit>().trySubmit(),
text: 'basis.connect'.tr(),
),
const SizedBox(height: 10),
BrandButton.text(
onPressed: () {
context.read<SupportSystemCubit>().showArticle(
article: 'how_backblaze',
context: context,
);
Scaffold.of(context).openEndDrawer();
},
title: 'initializing.how'.tr(),
),
],
),
);
},
),
@ -269,9 +325,8 @@ class InitializingPage extends StatelessWidget {
builder: (final context) {
final DomainSetupState state =
context.watch<DomainSetupCubit>().state;
return SizedBox(
width: double.infinity,
child: Column(
return ResponsiveLayoutWithInfobox(
topChild: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
@ -283,7 +338,11 @@ class InitializingPage extends StatelessWidget {
'initializing.use_this_domain_text'.tr(),
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 32),
],
),
primaryColumn: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (state is Empty)
Text(
'initializing.no_connected_domains'.tr(),
@ -323,7 +382,7 @@ class InitializingPage extends StatelessWidget {
],
if (state is Empty) ...[
const SizedBox(height: 30),
BrandButton.rised(
BrandButton.filled(
onPressed: () => context.read<DomainSetupCubit>().load(),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
@ -333,14 +392,17 @@ class InitializingPage extends StatelessWidget {
color: Colors.white,
),
const SizedBox(width: 10),
BrandText.buttonTitleText('domain.update_list'.tr()),
Text(
'domain.update_list'.tr(),
style: Theme.of(context).textTheme.bodyLarge,
),
],
),
),
],
if (state is Loaded) ...[
const SizedBox(height: 32),
BrandButton.rised(
BrandButton.filled(
onPressed: () =>
context.read<DomainSetupCubit>().saveDomain(),
text: 'initializing.save_domain'.tr(),
@ -361,74 +423,83 @@ class InitializingPage extends StatelessWidget {
builder: (final context) {
final formCubitState = context.watch<RootUserFormCubit>().state;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'initializing.create_master_account'.tr(),
style: Theme.of(context).textTheme.headlineSmall,
),
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)
return ResponsiveLayoutWithInfobox(
topChild: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'users.username_rule'.tr(),
style: TextStyle(
color: Theme.of(context).colorScheme.error,
'initializing.create_master_account'.tr(),
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 16),
Text(
'initializing.enter_username_and_password'.tr(),
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
primaryColumn: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (formCubitState.isErrorShown) const SizedBox(height: 16),
if (formCubitState.isErrorShown)
Text(
'users.username_rule'.tr(),
style: TextStyle(
color: Theme.of(context).colorScheme.error,
),
),
const SizedBox(height: 32),
CubitFormTextField(
formFieldCubit: context.read<RootUserFormCubit>().userName,
textAlign: TextAlign.center,
scrollPadding: const EdgeInsets.only(bottom: 70),
decoration: InputDecoration(
hintText: 'basis.username'.tr(),
),
),
const SizedBox(height: 32),
CubitFormTextField(
formFieldCubit: context.read<RootUserFormCubit>().userName,
textAlign: TextAlign.center,
scrollPadding: const EdgeInsets.only(bottom: 70),
decoration: InputDecoration(
hintText: 'basis.username'.tr(),
),
),
const SizedBox(height: 16),
BlocBuilder<FieldCubit<bool>, FieldCubitState<bool>>(
bloc: context.read<RootUserFormCubit>().isVisible,
builder: (final context, final state) {
final bool isVisible = state.value;
return CubitFormTextField(
obscureText: !isVisible,
formFieldCubit:
context.read<RootUserFormCubit>().password,
textAlign: TextAlign.center,
scrollPadding: const EdgeInsets.only(bottom: 70),
decoration: InputDecoration(
hintText: 'basis.password'.tr(),
suffixIcon: IconButton(
icon: Icon(
isVisible ? Icons.visibility : Icons.visibility_off,
const SizedBox(height: 16),
BlocBuilder<FieldCubit<bool>, FieldCubitState<bool>>(
bloc: context.read<RootUserFormCubit>().isVisible,
builder: (final context, final state) {
final bool isVisible = state.value;
return CubitFormTextField(
obscureText: !isVisible,
formFieldCubit:
context.read<RootUserFormCubit>().password,
textAlign: TextAlign.center,
scrollPadding: const EdgeInsets.only(bottom: 70),
decoration: InputDecoration(
hintText: 'basis.password'.tr(),
suffixIcon: IconButton(
icon: Icon(
isVisible
? Icons.visibility
: Icons.visibility_off,
),
onPressed: () => context
.read<RootUserFormCubit>()
.isVisible
.setValue(!isVisible),
),
onPressed: () => context
.read<RootUserFormCubit>()
.isVisible
.setValue(!isVisible),
suffixIconConstraints:
const BoxConstraints(minWidth: 60),
prefixIconConstraints:
const BoxConstraints(maxWidth: 60),
prefixIcon: Container(),
),
suffixIconConstraints:
const BoxConstraints(minWidth: 60),
prefixIconConstraints:
const BoxConstraints(maxWidth: 60),
prefixIcon: Container(),
),
);
},
),
const SizedBox(height: 32),
BrandButton.rised(
onPressed: formCubitState.isSubmitting
? null
: () => context.read<RootUserFormCubit>().trySubmit(),
text: 'basis.connect'.tr(),
),
],
);
},
),
const SizedBox(height: 32),
BrandButton.filled(
onPressed: formCubitState.isSubmitting
? null
: () => context.read<RootUserFormCubit>().trySubmit(),
text: 'basis.connect'.tr(),
),
],
),
);
},
),
@ -438,27 +509,28 @@ class InitializingPage extends StatelessWidget {
final bool isLoading =
(appConfigCubit.state as ServerInstallationNotFinished).isLoading;
return Builder(
builder: (final context) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
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,
text: isLoading
? 'basis.loading'.tr()
: 'initializing.create_server'.tr(),
),
],
builder: (final context) => ResponsiveLayoutWithInfobox(
topChild: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
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,
),
],
),
primaryColumn: BrandButton.filled(
onPressed:
isLoading ? null : appConfigCubit.createServerAndSetDnsRecords,
text: isLoading
? 'basis.loading'.tr()
: 'initializing.create_server'.tr(),
),
),
);
}
@ -487,84 +559,67 @@ class InitializingPage extends StatelessWidget {
return Builder(
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: 16),
if (text != null)
child: ResponsiveLayoutWithInfobox(
topChild: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
text,
style: Theme.of(context).textTheme.bodyMedium,
'initializing.checks'.tr(args: [doneCount.toString(), '4']),
style: Theme.of(context).textTheme.headlineSmall,
),
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,
),
],
const SizedBox(height: 16),
if (text != null)
Text(
text,
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
primaryColumn: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
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,
),
],
),
),
),
);
}
}
class _HowTo extends StatelessWidget {
const _HowTo({
required this.fileName,
});
final String fileName;
@override
Widget build(final BuildContext context) => BrandBottomSheet(
isExpended: true,
child: Padding(
padding: paddingH15V0,
child: ListView(
padding: const EdgeInsets.symmetric(vertical: 16),
children: [
BrandMarkdown(
fileName: fileName,
),
],
),
),
);
}

View File

@ -2,16 +2,15 @@ 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/cubit/support_system/support_system_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/brand_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/buttons/brand_button.dart';
import 'package:selfprivacy/ui/components/buttons/outlined_button.dart';
import 'package:selfprivacy/ui/components/cards/outlined_card.dart';
import 'package:selfprivacy/ui/components/info_box/info_box.dart';
import 'package:selfprivacy/ui/layouts/responsive_layout_with_infobox.dart';
import 'package:selfprivacy/utils/launch_url.dart';
class ServerProviderPicker extends StatefulWidget {
@ -98,56 +97,49 @@ class ProviderInputDataPage extends StatelessWidget {
final ServerProviderFormCubit providerCubit;
@override
Widget build(final BuildContext context) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"${'initializing.connect_to_server_provider'.tr()}${providerInfo.providerType.displayName}",
style: Theme.of(context).textTheme.headlineSmall,
),
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,
scrollPadding: const EdgeInsets.only(bottom: 70),
decoration: const InputDecoration(
hintText: 'Provider API Token',
Widget build(final BuildContext context) => ResponsiveLayoutWithInfobox(
topChild: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"${'initializing.connect_to_server_provider'.tr()}${providerInfo.providerType.displayName}",
style: Theme.of(context).textTheme.headlineSmall,
),
),
const SizedBox(height: 32),
BrandButton.filled(
child: Text('basis.connect'.tr()),
onPressed: () => providerCubit.trySubmit(),
),
const SizedBox(height: 10),
BrandOutlinedButton(
child: Text('initializing.how'.tr()),
onPressed: () => showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (final BuildContext context) => BrandBottomSheet(
isExpended: true,
child: Padding(
padding: paddingH15V0,
child: ListView(
padding: const EdgeInsets.symmetric(vertical: 16),
children: [
BrandMarkdown(
fileName: providerInfo.pathToHow,
),
],
),
),
const SizedBox(height: 16),
Text(
'initializing.connect_to_server_provider_text'.tr(),
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
primaryColumn: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CubitFormTextField(
formFieldCubit: providerCubit.apiKey,
textAlign: TextAlign.center,
scrollPadding: const EdgeInsets.only(bottom: 70),
decoration: const InputDecoration(
hintText: 'Provider API Token',
),
),
),
],
const SizedBox(height: 32),
BrandButton.filled(
child: Text('basis.connect'.tr()),
onPressed: () => providerCubit.trySubmit(),
),
const SizedBox(height: 10),
BrandOutlinedButton(
child: Text('initializing.how'.tr()),
onPressed: () {
context.read<SupportSystemCubit>().showArticle(
article: providerInfo.pathToHow,
context: context,
);
},
),
],
),
);
}
@ -164,175 +156,183 @@ class ProviderSelectionPage extends StatelessWidget {
@override
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,
),
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: ResponsiveLayoutWithInfobox(
topChild: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'initializing.connect_to_server'.tr(),
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 10),
Text(
'initializing.select_provider'.tr(),
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
primaryColumn: Column(
children: [
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',
),
),
child: SvgPicture.asset(
'assets/images/logos/hetzner.svg',
const SizedBox(width: 16),
Text(
'Hetzner Cloud',
style: Theme.of(context).textTheme.titleMedium,
),
),
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),
BrandButton.filled(
child: Text('basis.select'.tr()),
onPressed: () {
serverInstallationCubit
.setServerProviderType(ServerProviderType.hetzner);
callback(ServerProviderType.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),
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),
BrandButton.filled(
child: Text('basis.select'.tr()),
onPressed: () {
serverInstallationCubit.setServerProviderType(
ServerProviderType.hetzner,
);
callback(ServerProviderType.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),
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',
),
),
child: SvgPicture.asset(
'assets/images/logos/digital_ocean.svg',
const SizedBox(width: 16),
Text(
'Digital Ocean',
style: Theme.of(context).textTheme.titleMedium,
),
),
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),
BrandButton.filled(
child: Text('basis.select'.tr()),
onPressed: () {
serverInstallationCubit.setServerProviderType(
ServerProviderType.digitalOcean);
callback(ServerProviderType.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),
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),
BrandButton.filled(
child: Text('basis.select'.tr()),
onPressed: () {
serverInstallationCubit.setServerProviderType(
ServerProviderType.digitalOcean,
);
callback(ServerProviderType.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()),
],
],
),
secondaryColumn:
InfoBox(text: 'initializing.select_provider_notice'.tr()),
),
);
}

View File

@ -5,8 +5,9 @@ import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_depe
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/buttons/brand_button.dart';
import 'package:selfprivacy/ui/components/info_box/info_box.dart';
import 'package:selfprivacy/ui/layouts/responsive_layout_with_infobox.dart';
class ServerTypePicker extends StatefulWidget {
const ServerTypePicker({
@ -70,50 +71,67 @@ class SelectLocationPage extends StatelessWidget {
if ((snapshot.data as List<ServerProviderLocation>).isEmpty) {
return Text('initializing.no_locations_found'.tr());
}
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) => 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,
return ResponsiveLayoutWithInfobox(
topChild: Column(
crossAxisAlignment: CrossAxisAlignment.start,
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,
),
],
),
primaryColumn: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
...(snapshot.data! as List<ServerProviderLocation>).map(
(final location) => Column(
children: [
SizedBox(
width: double.infinity,
child: Card(
clipBehavior: Clip.antiAlias,
child: InkResponse(
highlightShape: BoxShape.rectangle,
onTap: () {
callback(location);
},
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,
),
],
),
],
),
),
),
),
),
const SizedBox(height: 8),
],
),
),
),
const SizedBox(height: 24),
],
],
),
);
} else {
return const Center(child: CircularProgressIndicator());
@ -180,121 +198,145 @@ class SelectTypePage extends StatelessWidget {
],
);
}
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) => 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()],
return ResponsiveLayoutWithInfobox(
topChild: 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,
),
],
),
primaryColumn: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
...(snapshot.data! as List<ServerType>).map(
(final type) => Column(
children: [
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,
),
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}'
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,
),
],
),
style:
Theme.of(context).textTheme.bodyLarge,
),
],
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: 8),
],
),
),
),
const SizedBox(height: 16),
InfoBox(text: 'initializing.choose_server_type_notice'.tr()),
],
],
),
secondaryColumn:
InfoBox(text: 'initializing.choose_server_type_notice'.tr()),
);
} else {
return const Center(child: CircularProgressIndicator());

View File

@ -1,8 +1,8 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:selfprivacy/logic/cubit/forms/setup/recovering/recovery_device_form_cubit.dart';
import 'package:selfprivacy/ui/components/brand_button/brand_button.dart';
import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart';
import 'package:selfprivacy/ui/components/buttons/brand_button.dart';
import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart';
import 'package:selfprivacy/utils/route_transitions/basic.dart';
import 'package:cubit_form/cubit_form.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
@ -17,6 +17,7 @@ class RecoverByNewDeviceKeyInstruction extends StatelessWidget {
heroSubtitle: 'recovering.method_device_description'.tr(),
hasBackButton: true,
hasFlashButton: false,
ignoreBreakpoints: true,
onBackButtonPressed:
context.read<ServerInstallationCubit>().revertRecoveryStep,
children: [
@ -61,6 +62,7 @@ class RecoverByNewDeviceKeyInput extends StatelessWidget {
heroSubtitle: 'recovering.method_device_input_description'.tr(),
hasBackButton: true,
hasFlashButton: false,
ignoreBreakpoints: true,
children: [
CubitFormTextField(
formFieldCubit:

View File

@ -1,8 +1,8 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:selfprivacy/logic/cubit/forms/setup/recovering/recovery_device_form_cubit.dart';
import 'package:selfprivacy/ui/components/brand_button/brand_button.dart';
import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart';
import 'package:selfprivacy/ui/components/buttons/brand_button.dart';
import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart';
import 'package:selfprivacy/ui/components/brand_md/brand_md.dart';
import 'package:cubit_form/cubit_form.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
@ -28,6 +28,7 @@ class RecoverByOldTokenInstruction extends StatelessWidget {
heroTitle: 'recovering.recovery_main_header'.tr(),
hasBackButton: true,
hasFlashButton: false,
ignoreBreakpoints: true,
onBackButtonPressed:
context.read<ServerInstallationCubit>().revertRecoveryStep,
children: [
@ -72,6 +73,7 @@ class RecoverByOldToken extends StatelessWidget {
heroSubtitle: 'recovering.method_device_input_description'.tr(),
hasBackButton: true,
hasFlashButton: false,
ignoreBreakpoints: true,
children: [
CubitFormTextField(
formFieldCubit:

View File

@ -4,8 +4,8 @@ import 'package:flutter/material.dart';
import 'package:selfprivacy/logic/cubit/forms/factories/field_cubit_factory.dart';
import 'package:selfprivacy/logic/cubit/forms/setup/recovering/recovery_device_form_cubit.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
import 'package:selfprivacy/ui/components/brand_button/brand_button.dart';
import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart';
import 'package:selfprivacy/ui/components/buttons/brand_button.dart';
import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart';
class RecoverByRecoveryKey extends StatelessWidget {
const RecoverByRecoveryKey({super.key});
@ -31,6 +31,7 @@ class RecoverByRecoveryKey extends StatelessWidget {
heroSubtitle: 'recovering.method_recovery_input_description'.tr(),
hasBackButton: true,
hasFlashButton: false,
ignoreBreakpoints: true,
onBackButtonPressed:
context.read<ServerInstallationCubit>().revertRecoveryStep,
children: [

View File

@ -1,13 +1,11 @@
import 'package:cubit_form/cubit_form.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:selfprivacy/config/brand_theme.dart';
import 'package:selfprivacy/logic/cubit/forms/setup/initializing/backblaze_form_cubit.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_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_hero_screen/brand_hero_screen.dart';
import 'package:selfprivacy/ui/components/brand_md/brand_md.dart';
import 'package:selfprivacy/logic/cubit/support_system/support_system_cubit.dart';
import 'package:selfprivacy/ui/components/buttons/brand_button.dart';
import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart';
class RecoveryConfirmBackblaze extends StatelessWidget {
const RecoveryConfirmBackblaze({super.key});
@ -28,6 +26,8 @@ class RecoveryConfirmBackblaze extends StatelessWidget {
heroTitle: 'recovering.confirm_backblaze'.tr(),
heroSubtitle: 'recovering.confirm_backblaze_description'.tr(),
hasBackButton: true,
ignoreBreakpoints: true,
hasSupportDrawer: true,
onBackButtonPressed: () {
Navigator.of(context).popUntil((final route) => route.isFirst);
},
@ -57,27 +57,15 @@ class RecoveryConfirmBackblaze extends StatelessWidget {
text: 'basis.connect'.tr(),
),
const SizedBox(height: 16),
BrandButton.text(
onPressed: () => showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (final BuildContext context) => BrandBottomSheet(
isExpended: true,
child: Padding(
padding: paddingH15V0,
child: ListView(
padding: const EdgeInsets.symmetric(vertical: 16),
children: const [
BrandMarkdown(
fileName: 'how_backblaze',
Builder(
builder: (final context) => BrandButton.text(
onPressed: () =>
context.read<SupportSystemCubit>().showArticle(
article: 'how_backblaze',
context: context,
),
],
),
),
),
title: 'initializing.how'.tr(),
),
title: 'initializing.how'.tr(),
),
],
);

View File

@ -1,13 +1,11 @@
import 'package:cubit_form/cubit_form.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:selfprivacy/config/brand_theme.dart';
import 'package:selfprivacy/logic/cubit/forms/setup/initializing/dns_provider_form_cubit.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_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_hero_screen/brand_hero_screen.dart';
import 'package:selfprivacy/ui/components/brand_md/brand_md.dart';
import 'package:selfprivacy/logic/cubit/support_system/support_system_cubit.dart';
import 'package:selfprivacy/ui/components/buttons/brand_button.dart';
import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart';
class RecoveryConfirmCloudflare extends StatelessWidget {
const RecoveryConfirmCloudflare({super.key});
@ -31,6 +29,8 @@ class RecoveryConfirmCloudflare extends StatelessWidget {
),
hasBackButton: true,
hasFlashButton: false,
ignoreBreakpoints: true,
hasSupportDrawer: true,
onBackButtonPressed:
context.read<ServerInstallationCubit>().revertRecoveryStep,
children: [
@ -49,27 +49,15 @@ class RecoveryConfirmCloudflare extends StatelessWidget {
text: 'basis.connect'.tr(),
),
const SizedBox(height: 16),
BrandButton.text(
onPressed: () => showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (final BuildContext context) => BrandBottomSheet(
isExpended: true,
child: Padding(
padding: paddingH15V0,
child: ListView(
padding: const EdgeInsets.symmetric(vertical: 16),
children: const [
BrandMarkdown(
fileName: 'how_cloudflare',
Builder(
builder: (final context) => BrandButton.text(
onPressed: () =>
context.read<SupportSystemCubit>().showArticle(
article: 'how_cloudflare',
context: context,
),
],
),
),
),
title: 'initializing.how'.tr(),
),
title: 'initializing.how'.tr(),
),
],
);

View File

@ -2,9 +2,9 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart';
import 'package:selfprivacy/logic/models/server_basic_info.dart';
import 'package:selfprivacy/ui/components/brand_button/brand_button.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/components/buttons/brand_button.dart';
import 'package:selfprivacy/ui/components/cards/filled_card.dart';
import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart';
class RecoveryConfirmServer extends StatefulWidget {
const RecoveryConfirmServer({super.key});
@ -38,6 +38,7 @@ class _RecoveryConfirmServerState extends State<RecoveryConfirmServer> {
? 'recovering.choose_server_description'.tr()
: 'recovering.confirm_server_description'.tr(),
hasBackButton: true,
ignoreBreakpoints: true,
onBackButtonPressed: () {
Navigator.of(context).popUntil((final route) => route.isFirst);
},

View File

@ -2,9 +2,9 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
import 'package:selfprivacy/ui/components/brand_button/brand_button.dart';
import 'package:selfprivacy/ui/components/brand_cards/outlined_card.dart';
import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart';
import 'package:selfprivacy/ui/components/buttons/brand_button.dart';
import 'package:selfprivacy/ui/components/cards/outlined_card.dart';
import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart';
import 'package:selfprivacy/ui/pages/setup/recovering/recover_by_old_token.dart';
import 'package:selfprivacy/utils/route_transitions/basic.dart';
@ -17,6 +17,7 @@ class RecoveryMethodSelect extends StatelessWidget {
heroSubtitle: 'recovering.method_select_description'.tr(),
hasBackButton: true,
hasFlashButton: false,
ignoreBreakpoints: true,
onBackButtonPressed:
context.read<ServerInstallationCubit>().revertRecoveryStep,
children: [
@ -74,6 +75,7 @@ class RecoveryFallbackMethodSelect extends StatelessWidget {
heroSubtitle: 'recovering.fallback_select_description'.tr(),
hasBackButton: true,
hasFlashButton: false,
ignoreBreakpoints: true,
children: [
OutlinedCard(
child: ListTile(

View File

@ -1,11 +1,12 @@
import 'package:auto_route/auto_route.dart';
import 'package:cubit_form/cubit_form.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
import 'package:selfprivacy/logic/cubit/forms/factories/field_cubit_factory.dart';
import 'package:selfprivacy/logic/cubit/forms/setup/recovering/recovery_domain_form_cubit.dart';
import 'package:selfprivacy/ui/components/brand_button/brand_button.dart';
import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart';
import 'package:selfprivacy/ui/components/buttons/brand_button.dart';
import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart';
import 'package:selfprivacy/ui/pages/root_route.dart';
import 'package:selfprivacy/ui/pages/setup/recovering/recover_by_old_token.dart';
import 'package:selfprivacy/ui/pages/setup/recovering/recover_by_recovery_key.dart';
@ -17,6 +18,7 @@ import 'package:selfprivacy/ui/pages/setup/recovering/recovery_server_provider_c
import 'package:selfprivacy/ui/pages/setup/recovering/recovery_method_select.dart';
import 'package:selfprivacy/utils/route_transitions/basic.dart';
@RoutePage()
class RecoveryRouting extends StatelessWidget {
const RecoveryRouting({super.key});
@ -110,6 +112,7 @@ class SelectDomainToRecover extends StatelessWidget {
heroSubtitle: 'recovering.domain_recovery_description'.tr(),
hasBackButton: true,
hasFlashButton: false,
ignoreBreakpoints: true,
onBackButtonPressed: () {
Navigator.of(context).pushAndRemoveUntil(
materialRoute(const RootPage()),

View File

@ -1,13 +1,11 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:selfprivacy/config/brand_theme.dart';
import 'package:selfprivacy/logic/cubit/forms/setup/initializing/server_provider_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_hero_screen/brand_hero_screen.dart';
import 'package:selfprivacy/logic/cubit/support_system/support_system_cubit.dart';
import 'package:selfprivacy/ui/components/buttons/brand_button.dart';
import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart';
import 'package:cubit_form/cubit_form.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
import 'package:selfprivacy/ui/components/brand_md/brand_md.dart';
class RecoveryServerProviderConnected extends StatelessWidget {
const RecoveryServerProviderConnected({super.key});
@ -32,6 +30,8 @@ class RecoveryServerProviderConnected extends StatelessWidget {
),
hasBackButton: true,
hasFlashButton: false,
ignoreBreakpoints: true,
hasSupportDrawer: true,
onBackButtonPressed: () {
Navigator.of(context).popUntil((final route) => route.isFirst);
},
@ -52,26 +52,14 @@ class RecoveryServerProviderConnected extends StatelessWidget {
child: Text('basis.continue'.tr()),
),
const SizedBox(height: 16),
BrandButton.text(
title: 'initializing.how'.tr(),
onPressed: () => showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (final BuildContext context) => BrandBottomSheet(
isExpended: true,
child: Padding(
padding: paddingH15V0,
child: ListView(
padding: const EdgeInsets.symmetric(vertical: 16),
children: const [
BrandMarkdown(
fileName: 'how_hetzner',
Builder(
builder: (final context) => BrandButton.text(
title: 'initializing.how'.tr(),
onPressed: () =>
context.read<SupportSystemCubit>().showArticle(
article: 'how_hetzner',
context: context,
),
],
),
),
),
),
),
],

View File

@ -1,22 +0,0 @@
part of 'users.dart';
class AddUserFab extends StatelessWidget {
const AddUserFab({super.key});
@override
Widget build(final BuildContext context) => FloatingActionButton.small(
heroTag: 'new_user_fab',
onPressed: () {
showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (final BuildContext context) => Padding(
padding: MediaQuery.of(context).viewInsets,
child: const NewUser(),
),
);
},
child: const Icon(Icons.person_add_outlined),
);
}

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