feat: Introduce new router and adaptive layouts

pull/203/head
Inex Code 2023-02-23 17:49:14 +03:00 committed by Gitea
parent befdc0286e
commit 423efeeb20
44 changed files with 2352 additions and 981 deletions

View File

@ -333,7 +333,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.",

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,6 +63,8 @@ class MyApp extends StatelessWidget {
final ThemeData lightThemeData;
final ThemeData darkThemeData;
final _appRouter = RootRouter();
@override
Widget build(final BuildContext context) => Localization(
child: AnnotatedRegion<SystemUiOverlayStyle>(
@ -76,10 +75,11 @@ class MyApp extends StatelessWidget {
final BuildContext context,
final AppSettingsState appSettings,
) =>
MaterialApp(
MaterialApp.router(
routeInformationParser: _appRouter.defaultRouteParser(),
routerDelegate: _appRouter.delegate(),
scaffoldMessengerKey:
getIt.get<NavigationService>().scaffoldMessengerKey,
navigatorKey: getIt.get<NavigationService>().navigatorKey,
localizationsDelegates: context.localizationDelegates,
supportedLocales: context.supportedLocales,
locale: context.locale,

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/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,3 +1,4 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:ionicons/ionicons.dart';
@ -7,7 +8,12 @@ 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();
@ -64,20 +70,35 @@ class _BrandFabState extends State<BrandFab>
),
);
},
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,8 @@
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/helpers/widget_size.dart';
import 'package:selfprivacy/utils/breakpoints.dart';
class BrandHeroScreen extends StatelessWidget {
const BrandHeroScreen({
@ -13,6 +15,7 @@ class BrandHeroScreen extends StatelessWidget {
this.heroTitle = '',
this.heroSubtitle,
this.onBackButtonPressed,
this.bodyPadding = const EdgeInsets.all(16.0),
});
final List<Widget> children;
@ -23,6 +26,7 @@ class BrandHeroScreen extends StatelessWidget {
final String heroTitle;
final String? heroSubtitle;
final VoidCallback? onBackButtonPressed;
final EdgeInsetsGeometry bodyPadding;
@override
Widget build(final BuildContext context) {
@ -64,7 +68,7 @@ class BrandHeroScreen extends StatelessWidget {
),
),
SliverPadding(
padding: const EdgeInsets.all(16.0),
padding: bodyPadding,
sliver: SliverList(
delegate: SliverChildListDelegate(children),
),
@ -98,50 +102,50 @@ class HeroSliverAppBar extends StatefulWidget {
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,
),
Widget build(final BuildContext context) {
final isMobile = Breakpoints.small.isActive(context);
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(),
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,
],
),
),
);
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,276 @@
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/support_drawer/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,
),
Expanded(child: child),
],
),
bottomNavigationBar: BottomBar(
destinations: destinations,
hidden: !(Breakpoints.small.isActive(context) && showBottomBar),
key: const Key('bottomBar'),
),
);
}
}
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,
super.key,
});
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),
);
print(prevActiveIndex);
return AnimatedContainer(
duration: const Duration(milliseconds: 200),
height: hidden ? 0 : 80,
curve: Curves.easeInOut,
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,
super.key,
});
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

@ -6,7 +6,7 @@ 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/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';

View File

@ -5,7 +5,7 @@ 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';

View File

@ -4,7 +4,7 @@ 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/layouts/brand_hero_screen.dart';
class NewDeviceScreen extends StatelessWidget {
const NewDeviceScreen({super.key});

View File

@ -4,7 +4,7 @@ import 'package:selfprivacy/config/get_it_config.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/layouts/brand_hero_screen.dart';
import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart';
import 'package:selfprivacy/utils/network_utils.dart';

View File

@ -1,67 +1,70 @@
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/logic/cubit/server_installation/server_installation_cubit.dart';
import 'package:selfprivacy/ui/components/brand_md/brand_md.dart';
import 'package:selfprivacy/ui/components/brand_text/brand_text.dart';
import 'package:package_info/package_info.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart';
import 'package:url_launcher/url_launcher.dart';
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) => BrandText.body1(
'about_application_page.application_version_text'
.tr(args: [snapshot.data.toString()]),
),
),
if (isReady)
FutureBuilder(
future: _apiVersion(),
builder: (final context, final snapshot) => BrandText.body1(
'about_application_page.api_version_text'
.tr(args: [snapshot.data.toString()]),
),
),
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

@ -0,0 +1,83 @@
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';
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

@ -7,14 +7,14 @@ 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';
class Console extends StatefulWidget {
const Console({super.key});
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);

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';
@ -8,19 +9,8 @@ import 'package:selfprivacy/logic/cubit/services/services_cubit.dart';
import 'package:selfprivacy/ui/components/brand_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';
class MorePage extends StatelessWidget {
const MorePage({super.key});
@ -34,12 +24,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 +42,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 +69,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 +77,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 +128,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 +150,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,13 @@
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/router/router.dart';
import 'package:easy_localization/easy_localization.dart';
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 +22,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(
@ -142,10 +142,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

@ -13,6 +13,7 @@ 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/breakpoints.dart';
import 'package:selfprivacy/utils/route_transitions/basic.dart';
GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
@ -61,12 +62,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: [

View File

@ -1,4 +1,3 @@
import 'package:cubit_form/cubit_form.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@ -8,18 +7,18 @@ 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/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});
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();
@ -29,7 +28,7 @@ class _RecoveryKeyState extends State<RecoveryKey> {
@override
Widget build(final BuildContext context) {
final RecoveryKeyState keyStatus = context.watch<RecoveryKeyCubit>().state;
final List<Widget> widgets;
String? subtitle =
keyStatus.exists ? null : 'recovery_key.key_main_description'.tr();

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/layouts/brand_hero_screen.dart';
import 'package:selfprivacy/ui/components/info_box/info_box.dart';
class RecoveryKeyReceiving extends StatelessWidget {

View File

@ -1,89 +1,149 @@
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 {
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 routeName = getRouteTitle(context.router.current.name).tr();
return RootScaffoldWithNavigation(
title: routeName,
destinations: destinations,
showBottomBar: !(currentDestinationIndex == -1),
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

@ -12,7 +12,7 @@ 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/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';

View File

@ -3,7 +3,7 @@ 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/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

@ -6,7 +6,7 @@ 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/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';

View File

@ -5,7 +5,7 @@ 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/ui/components/brand_button/outlined_button.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/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';

View File

@ -7,7 +7,7 @@ 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/layouts/brand_hero_screen.dart';
import 'package:selfprivacy/ui/pages/server_storage/binds_migration/services_migration.dart';
import 'package:selfprivacy/utils/launch_url.dart';
import 'package:selfprivacy/utils/route_transitions/basic.dart';
@ -50,7 +50,11 @@ class _ServicePageState extends State<ServicePage> {
service.svgIcon,
width: 48.0,
height: 48.0,
color: Theme.of(context).colorScheme.onBackground,
colorFilter: ColorFilter.mode(
Theme.of(context).colorScheme.onBackground,
BlendMode.srcIn,
)
// color: Theme.of(context).colorScheme.onBackground,
),
heroTitle: service.displayName,
children: [

View File

@ -12,6 +12,7 @@ import 'package:selfprivacy/ui/components/icon_status_mask/icon_status_mask.dart
import 'package:selfprivacy/ui/components/not_ready_card/not_ready_card.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:selfprivacy/ui/pages/services/service_page.dart';
import 'package:selfprivacy/utils/breakpoints.dart';
import 'package:selfprivacy/utils/launch_url.dart';
import 'package:selfprivacy/utils/route_transitions/basic.dart';
import 'package:selfprivacy/utils/ui_helpers.dart';
@ -34,12 +35,14 @@ 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();

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/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,17 +9,19 @@ 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/logic/cubit/support_system/support_system_cubit.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_button/outlined_button.dart';
import 'package:selfprivacy/ui/components/brand_text/brand_text.dart';
import 'package:selfprivacy/ui/components/brand_timer/brand_timer.dart';
import 'package:selfprivacy/ui/components/progress_bar/progress_bar.dart';
import 'package:selfprivacy/ui/pages/root_route.dart';
import 'package:selfprivacy/ui/components/support_drawer/support_drawer.dart';
import 'package:selfprivacy/ui/layouts/responsive_layout_with_infobox.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';
class InitializingPage extends StatelessWidget {
const InitializingPage({super.key});
@ -48,99 +50,123 @@ 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(),
onPressed: () {
Navigator.of(context).push(
materialRoute(
const RecoveryRouting(),
),
);
},
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,
cubit: cubit,
constraints: constraints,
),
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());
},
),
)
],
),
],
),
),
),
],
@ -179,57 +205,55 @@ 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 _stepCloudflare(final ServerInstallationCubit initializingCubit) =>
BlocProvider(
create: (final context) => DnsProviderFormCubit(initializingCubit),
child: Builder(
builder: (final context) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${'initializing.connect_to_server_provider'.tr()}Cloudflare',
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 16),
Text(
'initializing.manage_domain_dns'.tr(),
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 32),
CubitFormTextField(
formFieldCubit: context.read<DnsProviderFormCubit>().apiKey,
textAlign: TextAlign.center,
scrollPadding: const EdgeInsets.only(bottom: 70),
decoration: InputDecoration(
hintText: 'initializing.cloudflare_api_token'.tr(),
builder: (final context) => ResponsiveLayoutWithInfobox(
topChild: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${'initializing.connect_to_server_provider'.tr()}Cloudflare',
style: Theme.of(context).textTheme.headlineSmall,
),
),
const SizedBox(height: 32),
BrandButton.rised(
onPressed: () =>
context.read<DnsProviderFormCubit>().trySubmit(),
text: 'basis.connect'.tr(),
),
const SizedBox(height: 10),
BrandButton.text(
onPressed: () => _showModal(
context,
const _HowTo(
fileName: 'how_cloudflare',
const SizedBox(height: 16),
Text(
'initializing.manage_domain_dns'.tr(),
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
primaryColumn: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CubitFormTextField(
formFieldCubit: context.read<DnsProviderFormCubit>().apiKey,
textAlign: TextAlign.center,
scrollPadding: const EdgeInsets.only(bottom: 70),
decoration: InputDecoration(
hintText: 'initializing.cloudflare_api_token'.tr(),
),
),
title: 'initializing.how'.tr(),
),
],
const SizedBox(height: 32),
BrandButton.filled(
onPressed: () =>
context.read<DnsProviderFormCubit>().trySubmit(),
text: 'basis.connect'.tr(),
),
const SizedBox(height: 10),
BrandOutlinedButton(
onPressed: () {
context.read<SupportSystemCubit>().showArticle(
article: 'how_cloudflare',
context: context,
);
Scaffold.of(context).openEndDrawer();
},
title: 'initializing.how'.tr(),
),
],
),
),
),
);
@ -240,50 +264,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(),
),
],
),
);
},
),
@ -296,9 +327,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(
@ -310,7 +340,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(),
@ -350,7 +384,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,
@ -367,7 +401,7 @@ class InitializingPage extends StatelessWidget {
],
if (state is Loaded) ...[
const SizedBox(height: 32),
BrandButton.rised(
BrandButton.filled(
onPressed: () =>
context.read<DomainSetupCubit>().saveDomain(),
text: 'initializing.save_domain'.tr(),
@ -388,74 +422,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(),
),
],
),
);
},
),
@ -465,27 +508,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(),
),
),
);
}
@ -514,84 +558,200 @@ 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,
class _ProgressDrawer extends StatelessWidget {
const _ProgressDrawer({
required this.steps,
required this.cubit,
required this.constraints,
});
final String fileName;
final List<String> steps;
final ServerInstallationCubit cubit;
final BoxConstraints constraints;
@override
Widget build(final BuildContext context) => BrandBottomSheet(
isExpended: true,
child: Padding(
padding: paddingH15V0,
child: ListView(
padding: const EdgeInsets.symmetric(vertical: 16),
Widget build(final BuildContext context) => SizedBox(
width: 300,
height: constraints.maxHeight,
child: Drawer(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
BrandMarkdown(
fileName: fileName,
Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
'more_page.configuration_wizard'.tr(),
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 == cubit.state.progress.index,
isCompleted: index < cubit.state.progress.index,
);
}).toList(),
],
),
),
),
// const Spacer(),
Padding(
padding: const EdgeInsets.all(16.0),
child: 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: () {
context.router.popUntilRoot();
},
),
),
],
),
),
],
),
),
);
}
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

@ -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/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/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 ProviderFormCubit 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,182 @@ 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(ServerProvider.hetzner);
callback(ServerProvider.hetzner);
},
),
// Outlined button that will open website
BrandOutlinedButton(
onPressed: () =>
launchURL('https://www.hetzner.com/cloud'),
title: 'initializing.select_provider_site_button'.tr(),
),
],
],
),
const SizedBox(height: 16),
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(ServerProvider.hetzner);
callback(ServerProvider.hetzner);
},
),
// Outlined button that will open website
BrandOutlinedButton(
onPressed: () =>
launchURL('https://www.hetzner.com/cloud'),
title: 'initializing.select_provider_site_button'.tr(),
),
],
),
),
),
),
const SizedBox(height: 16),
OutlinedCard(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 40,
height: 40,
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(40),
color: const Color(0xFF0080FF),
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(ServerProvider.digitalOcean);
callback(ServerProvider.digitalOcean);
},
),
// Outlined button that will open website
BrandOutlinedButton(
onPressed: () =>
launchURL('https://www.digitalocean.com'),
title: 'initializing.select_provider_site_button'.tr(),
),
],
],
),
const SizedBox(height: 16),
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(
ServerProvider.digitalOcean,
);
callback(ServerProvider.digitalOcean);
},
),
// Outlined button that will open website
BrandOutlinedButton(
onPressed: () =>
launchURL('https://www.digitalocean.com'),
title: 'initializing.select_provider_site_button'.tr(),
),
],
),
),
),
),
const SizedBox(height: 16),
InfoBox(text: 'initializing.select_provider_notice'.tr()),
],
],
),
secondaryColumn:
InfoBox(text: 'initializing.select_provider_notice'.tr()),
),
);
}

View File

@ -7,6 +7,7 @@ import 'package:selfprivacy/logic/models/server_provider_location.dart';
import 'package:selfprivacy/logic/models/server_type.dart';
import 'package:selfprivacy/ui/components/brand_button/brand_button.dart';
import 'package:selfprivacy/ui/components/info_box/info_box.dart';
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

@ -2,7 +2,7 @@ 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/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';

View File

@ -2,7 +2,7 @@ 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/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';

View File

@ -5,7 +5,7 @@ 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/layouts/brand_hero_screen.dart';
class RecoverByRecoveryKey extends StatelessWidget {
const RecoverByRecoveryKey({super.key});

View File

@ -6,7 +6,7 @@ import 'package:selfprivacy/logic/cubit/forms/setup/initializing/backblaze_form_
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/layouts/brand_hero_screen.dart';
import 'package:selfprivacy/ui/components/brand_md/brand_md.dart';
class RecoveryConfirmBackblaze extends StatelessWidget {

View File

@ -6,7 +6,7 @@ import 'package:selfprivacy/logic/cubit/forms/setup/initializing/dns_provider_fo
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/layouts/brand_hero_screen.dart';
import 'package:selfprivacy/ui/components/brand_md/brand_md.dart';
class RecoveryConfirmCloudflare extends StatelessWidget {

View File

@ -4,7 +4,7 @@ import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_depe
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/layouts/brand_hero_screen.dart';
class RecoveryConfirmServer extends StatefulWidget {
const RecoveryConfirmServer({super.key});

View File

@ -4,7 +4,7 @@ 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/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';

View File

@ -5,7 +5,7 @@ import 'package:selfprivacy/logic/cubit/server_installation/server_installation_
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/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';

View File

@ -4,7 +4,7 @@ import 'package:selfprivacy/config/brand_theme.dart';
import 'package:selfprivacy/logic/cubit/forms/setup/initializing/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/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';

View File

@ -12,7 +12,7 @@ class _User extends StatelessWidget {
Widget build(final BuildContext context) => InkWell(
onTap: () {
Navigator.of(context).push(
materialRoute(UserDetails(login: user.login)),
materialRoute(UserDetailsPage(login: user.login)),
);
},
child: Container(

View File

@ -1,7 +1,7 @@
part of 'users.dart';
class UserDetails extends StatelessWidget {
const UserDetails({
class UserDetailsPage extends StatelessWidget {
const UserDetailsPage({
required this.login,
super.key,
});

View File

@ -16,12 +16,13 @@ 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/filled_card.dart';
import 'package:selfprivacy/ui/components/brand_header/brand_header.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/brand_icons/brand_icons.dart';
import 'package:selfprivacy/ui/components/brand_text/brand_text.dart';
import 'package:selfprivacy/ui/components/info_box/info_box.dart';
import 'package:selfprivacy/ui/components/list_tiles/list_tile_on_surface_variant.dart';
import 'package:selfprivacy/ui/components/not_ready_card/not_ready_card.dart';
import 'package:selfprivacy/utils/breakpoints.dart';
import 'package:selfprivacy/utils/ui_helpers.dart';
import 'package:selfprivacy/utils/route_transitions/basic.dart';
@ -106,12 +107,14 @@ class UsersPage extends StatelessWidget {
}
return Scaffold(
appBar: PreferredSize(
preferredSize: const Size.fromHeight(52),
child: BrandHeader(
title: 'basis.users'.tr(),
),
),
appBar: Breakpoints.small.isActive(context)
? PreferredSize(
preferredSize: const Size.fromHeight(52),
child: BrandHeader(
title: 'basis.users'.tr(),
),
)
: null,
body: child,
);
}

View File

@ -0,0 +1,46 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart';
import 'package:selfprivacy/ui/router/router.dart';
class RouteDestination {
const RouteDestination({
required this.route,
required this.icon,
required this.label,
required this.title,
});
final PageRouteInfo route;
final IconData icon;
final String label;
final String title;
}
final rootDestinations = [
RouteDestination(
route: const ProvidersRoute(),
icon: BrandIcons.server,
label: 'basis.providers'.tr(),
title: 'basis.providers_title'.tr(),
),
RouteDestination(
route: const ServicesRoute(),
icon: BrandIcons.box,
label: 'basis.services'.tr(),
title: 'basis.services'.tr(),
),
RouteDestination(
route: const UsersRoute(),
icon: BrandIcons.users,
label: 'basis.users'.tr(),
title: 'basis.users'.tr(),
),
RouteDestination(
route: const MoreRoute(),
icon: Icons.menu_rounded,
label: 'basis.more'.tr(),
title: 'basis.more'.tr(),
),
];

122
lib/ui/router/router.dart Normal file
View File

@ -0,0 +1,122 @@
import 'package:animations/animations.dart';
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:selfprivacy/logic/models/disk_status.dart';
import 'package:selfprivacy/logic/models/service.dart';
import 'package:selfprivacy/ui/pages/devices/devices.dart';
import 'package:selfprivacy/ui/pages/more/about_application.dart';
import 'package:selfprivacy/ui/pages/more/app_settings/app_settings.dart';
import 'package:selfprivacy/ui/pages/more/app_settings/developer_settings.dart';
import 'package:selfprivacy/ui/pages/more/console.dart';
import 'package:selfprivacy/ui/pages/more/more.dart';
import 'package:selfprivacy/ui/pages/onboarding/onboarding.dart';
import 'package:selfprivacy/ui/pages/providers/providers.dart';
import 'package:selfprivacy/ui/pages/recovery_key/recovery_key.dart';
import 'package:selfprivacy/ui/pages/root_route.dart';
import 'package:selfprivacy/ui/pages/server_storage/binds_migration/services_migration.dart';
import 'package:selfprivacy/ui/pages/services/services.dart';
import 'package:selfprivacy/ui/pages/setup/initializing/initializing.dart';
import 'package:selfprivacy/ui/pages/setup/recovering/recovery_routing.dart';
import 'package:selfprivacy/ui/pages/users/users.dart';
part 'router.gr.dart';
Widget fadeThroughTransition(
final BuildContext context,
final Animation<double> animation,
final Animation<double> secondaryAnimation,
final Widget child,
) =>
SharedAxisTransition(
key: UniqueKey(),
animation: animation,
secondaryAnimation: secondaryAnimation,
transitionType: SharedAxisTransitionType.vertical,
child: child,
);
@MaterialAutoRouter(
// transitionsBuilder: fadeThroughTransition,
replaceInRouteName: 'Page|Screen|Routing,Route',
routes: <AutoRoute>[
AutoRoute(
page: OnboardingPage,
),
AutoRoute(page: InitializingPage),
AutoRoute(page: RecoveryRouting),
AutoRoute(
page: RootPage,
initial: true,
children: [
CustomRoute(
page: ProvidersPage,
usesPathAsKey: true,
initial: true,
transitionsBuilder: fadeThroughTransition,
durationInMilliseconds: 200,
),
CustomRoute(
page: ServicesPage,
usesPathAsKey: true,
transitionsBuilder: fadeThroughTransition,
durationInMilliseconds: 200,
),
CustomRoute(
page: UsersPage,
usesPathAsKey: true,
transitionsBuilder: fadeThroughTransition,
durationInMilliseconds: 200,
),
CustomRoute(
page: MorePage,
usesPathAsKey: true,
transitionsBuilder: fadeThroughTransition,
durationInMilliseconds: 200,
),
AutoRoute(page: AppSettingsPage),
AutoRoute(page: UserDetailsPage),
AutoRoute(page: RecoveryKeyPage),
AutoRoute(page: DevicesScreen),
AutoRoute(page: AboutApplicationPage),
AutoRoute(page: DeveloperSettingsPage),
],
),
AutoRoute(page: ServicesMigrationPage),
AutoRoute(page: ConsolePage),
],
)
class RootRouter extends _$RootRouter {
RootRouter();
}
// Function to map route names to route titles
String getRouteTitle(final String routeName) {
switch (routeName) {
case 'RootRoute':
return 'basis.app_name';
case 'ProvidersRoute':
return 'basis.providers_title';
case 'ServicesRoute':
return 'basis.services';
case 'UsersRoute':
return 'basis.users';
case 'MoreRoute':
return 'basis.more';
case 'AppSettingsRoute':
return 'application_settings.title';
case 'UserDetailsRoute':
return '[User Details]';
case 'RecoveryKeyRoute':
return 'recovery_key.key_main_header';
case 'DevicesRoute':
return 'devices.main_screen.header';
case 'AboutApplicationRoute':
return 'about_us_page.title';
case 'ConsoleRoute':
return '[Console]';
case 'DeveloperSettingsRoute':
return 'developer_settings.title';
default:
return routeName;
}
}

View File

@ -0,0 +1,474 @@
// **************************************************************************
// AutoRouteGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// **************************************************************************
// AutoRouteGenerator
// **************************************************************************
//
// ignore_for_file: type=lint
part of 'router.dart';
class _$RootRouter extends RootStackRouter {
_$RootRouter([GlobalKey<NavigatorState>? navigatorKey]) : super(navigatorKey);
@override
final Map<String, PageFactory> pagesMap = {
OnboardingRoute.name: (routeData) {
return MaterialPageX<dynamic>(
routeData: routeData,
child: const OnboardingPage(),
);
},
InitializingRoute.name: (routeData) {
return MaterialPageX<dynamic>(
routeData: routeData,
child: const InitializingPage(),
);
},
RecoveryRoute.name: (routeData) {
return MaterialPageX<dynamic>(
routeData: routeData,
child: const RecoveryRouting(),
);
},
RootRoute.name: (routeData) {
return MaterialPageX<dynamic>(
routeData: routeData,
child: WrappedRoute(child: const RootPage()),
);
},
ServicesMigrationRoute.name: (routeData) {
final args = routeData.argsAs<ServicesMigrationRouteArgs>();
return MaterialPageX<dynamic>(
routeData: routeData,
child: ServicesMigrationPage(
services: args.services,
diskStatus: args.diskStatus,
isMigration: args.isMigration,
key: args.key,
),
);
},
ConsoleRoute.name: (routeData) {
return MaterialPageX<dynamic>(
routeData: routeData,
child: const ConsolePage(),
);
},
ProvidersRoute.name: (routeData) {
return CustomPage<dynamic>(
routeData: routeData,
child: const ProvidersPage(),
transitionsBuilder: fadeThroughTransition,
durationInMilliseconds: 200,
opaque: true,
barrierDismissible: false,
);
},
ServicesRoute.name: (routeData) {
return CustomPage<dynamic>(
routeData: routeData,
child: const ServicesPage(),
transitionsBuilder: fadeThroughTransition,
durationInMilliseconds: 200,
opaque: true,
barrierDismissible: false,
);
},
UsersRoute.name: (routeData) {
return CustomPage<dynamic>(
routeData: routeData,
child: const UsersPage(),
transitionsBuilder: fadeThroughTransition,
durationInMilliseconds: 200,
opaque: true,
barrierDismissible: false,
);
},
MoreRoute.name: (routeData) {
return CustomPage<dynamic>(
routeData: routeData,
child: const MorePage(),
transitionsBuilder: fadeThroughTransition,
durationInMilliseconds: 200,
opaque: true,
barrierDismissible: false,
);
},
AppSettingsRoute.name: (routeData) {
return MaterialPageX<dynamic>(
routeData: routeData,
child: const AppSettingsPage(),
);
},
UserDetailsRoute.name: (routeData) {
final args = routeData.argsAs<UserDetailsRouteArgs>();
return MaterialPageX<dynamic>(
routeData: routeData,
child: UserDetailsPage(
login: args.login,
key: args.key,
),
);
},
RecoveryKeyRoute.name: (routeData) {
return MaterialPageX<dynamic>(
routeData: routeData,
child: const RecoveryKeyPage(),
);
},
DevicesRoute.name: (routeData) {
return MaterialPageX<dynamic>(
routeData: routeData,
child: const DevicesScreen(),
);
},
AboutApplicationRoute.name: (routeData) {
return MaterialPageX<dynamic>(
routeData: routeData,
child: const AboutApplicationPage(),
);
},
DeveloperSettingsRoute.name: (routeData) {
return MaterialPageX<dynamic>(
routeData: routeData,
child: const DeveloperSettingsPage(),
);
},
};
@override
List<RouteConfig> get routes => [
RouteConfig(
OnboardingRoute.name,
path: '/onboarding-page',
),
RouteConfig(
InitializingRoute.name,
path: '/initializing-page',
),
RouteConfig(
RecoveryRoute.name,
path: '/recovery-routing',
),
RouteConfig(
RootRoute.name,
path: '/',
children: [
RouteConfig(
ProvidersRoute.name,
path: '',
parent: RootRoute.name,
usesPathAsKey: true,
),
RouteConfig(
ServicesRoute.name,
path: 'services-page',
parent: RootRoute.name,
usesPathAsKey: true,
),
RouteConfig(
UsersRoute.name,
path: 'users-page',
parent: RootRoute.name,
usesPathAsKey: true,
),
RouteConfig(
MoreRoute.name,
path: 'more-page',
parent: RootRoute.name,
usesPathAsKey: true,
),
RouteConfig(
AppSettingsRoute.name,
path: 'app-settings-page',
parent: RootRoute.name,
),
RouteConfig(
UserDetailsRoute.name,
path: 'user-details-page',
parent: RootRoute.name,
),
RouteConfig(
RecoveryKeyRoute.name,
path: 'recovery-key-page',
parent: RootRoute.name,
),
RouteConfig(
DevicesRoute.name,
path: 'devices-screen',
parent: RootRoute.name,
),
RouteConfig(
AboutApplicationRoute.name,
path: 'about-application-page',
parent: RootRoute.name,
),
RouteConfig(
DeveloperSettingsRoute.name,
path: 'developer-settings-page',
parent: RootRoute.name,
),
],
),
RouteConfig(
ServicesMigrationRoute.name,
path: '/services-migration-page',
),
RouteConfig(
ConsoleRoute.name,
path: '/console-page',
),
];
}
/// generated route for
/// [OnboardingPage]
class OnboardingRoute extends PageRouteInfo<void> {
const OnboardingRoute()
: super(
OnboardingRoute.name,
path: '/onboarding-page',
);
static const String name = 'OnboardingRoute';
}
/// generated route for
/// [InitializingPage]
class InitializingRoute extends PageRouteInfo<void> {
const InitializingRoute()
: super(
InitializingRoute.name,
path: '/initializing-page',
);
static const String name = 'InitializingRoute';
}
/// generated route for
/// [RecoveryRouting]
class RecoveryRoute extends PageRouteInfo<void> {
const RecoveryRoute()
: super(
RecoveryRoute.name,
path: '/recovery-routing',
);
static const String name = 'RecoveryRoute';
}
/// generated route for
/// [RootPage]
class RootRoute extends PageRouteInfo<void> {
const RootRoute({List<PageRouteInfo>? children})
: super(
RootRoute.name,
path: '/',
initialChildren: children,
);
static const String name = 'RootRoute';
}
/// generated route for
/// [ServicesMigrationPage]
class ServicesMigrationRoute extends PageRouteInfo<ServicesMigrationRouteArgs> {
ServicesMigrationRoute({
required List<Service> services,
required DiskStatus diskStatus,
required bool isMigration,
Key? key,
}) : super(
ServicesMigrationRoute.name,
path: '/services-migration-page',
args: ServicesMigrationRouteArgs(
services: services,
diskStatus: diskStatus,
isMigration: isMigration,
key: key,
),
);
static const String name = 'ServicesMigrationRoute';
}
class ServicesMigrationRouteArgs {
const ServicesMigrationRouteArgs({
required this.services,
required this.diskStatus,
required this.isMigration,
this.key,
});
final List<Service> services;
final DiskStatus diskStatus;
final bool isMigration;
final Key? key;
@override
String toString() {
return 'ServicesMigrationRouteArgs{services: $services, diskStatus: $diskStatus, isMigration: $isMigration, key: $key}';
}
}
/// generated route for
/// [ConsolePage]
class ConsoleRoute extends PageRouteInfo<void> {
const ConsoleRoute()
: super(
ConsoleRoute.name,
path: '/console-page',
);
static const String name = 'ConsoleRoute';
}
/// generated route for
/// [ProvidersPage]
class ProvidersRoute extends PageRouteInfo<void> {
const ProvidersRoute()
: super(
ProvidersRoute.name,
path: '',
);
static const String name = 'ProvidersRoute';
}
/// generated route for
/// [ServicesPage]
class ServicesRoute extends PageRouteInfo<void> {
const ServicesRoute()
: super(
ServicesRoute.name,
path: 'services-page',
);
static const String name = 'ServicesRoute';
}
/// generated route for
/// [UsersPage]
class UsersRoute extends PageRouteInfo<void> {
const UsersRoute()
: super(
UsersRoute.name,
path: 'users-page',
);
static const String name = 'UsersRoute';
}
/// generated route for
/// [MorePage]
class MoreRoute extends PageRouteInfo<void> {
const MoreRoute()
: super(
MoreRoute.name,
path: 'more-page',
);
static const String name = 'MoreRoute';
}
/// generated route for
/// [AppSettingsPage]
class AppSettingsRoute extends PageRouteInfo<void> {
const AppSettingsRoute()
: super(
AppSettingsRoute.name,
path: 'app-settings-page',
);
static const String name = 'AppSettingsRoute';
}
/// generated route for
/// [UserDetailsPage]
class UserDetailsRoute extends PageRouteInfo<UserDetailsRouteArgs> {
UserDetailsRoute({
required String login,
Key? key,
}) : super(
UserDetailsRoute.name,
path: 'user-details-page',
args: UserDetailsRouteArgs(
login: login,
key: key,
),
);
static const String name = 'UserDetailsRoute';
}
class UserDetailsRouteArgs {
const UserDetailsRouteArgs({
required this.login,
this.key,
});
final String login;
final Key? key;
@override
String toString() {
return 'UserDetailsRouteArgs{login: $login, key: $key}';
}
}
/// generated route for
/// [RecoveryKeyPage]
class RecoveryKeyRoute extends PageRouteInfo<void> {
const RecoveryKeyRoute()
: super(
RecoveryKeyRoute.name,
path: 'recovery-key-page',
);
static const String name = 'RecoveryKeyRoute';
}
/// generated route for
/// [DevicesScreen]
class DevicesRoute extends PageRouteInfo<void> {
const DevicesRoute()
: super(
DevicesRoute.name,
path: 'devices-screen',
);
static const String name = 'DevicesRoute';
}
/// generated route for
/// [AboutApplicationPage]
class AboutApplicationRoute extends PageRouteInfo<void> {
const AboutApplicationRoute()
: super(
AboutApplicationRoute.name,
path: 'about-application-page',
);
static const String name = 'AboutApplicationRoute';
}
/// generated route for
/// [DeveloperSettingsPage]
class DeveloperSettingsRoute extends PageRouteInfo<void> {
const DeveloperSettingsRoute()
: super(
DeveloperSettingsRoute.name,
path: 'developer-settings-page',
);
static const String name = 'DeveloperSettingsRoute';
}