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", "create_master_account": "Create master account",
"enter_username_and_password": "Enter username and strong password", "enter_username_and_password": "Enter username and strong password",
"finish": "Everything is initialized", "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": { "recovering": {
"generic_error": "Operation failed, please try again.", "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/brand_colors.dart';
import 'package:selfprivacy/config/hive_config.dart'; import 'package:selfprivacy/config/hive_config.dart';
import 'package:selfprivacy/theming/factory/app_theme_factory.dart'; import 'package:selfprivacy/theming/factory/app_theme_factory.dart';
import 'package:selfprivacy/ui/pages/setup/initializing/initializing.dart'; import 'package:selfprivacy/ui/router/router.dart';
import 'package:selfprivacy/ui/pages/onboarding/onboarding.dart';
import 'package:selfprivacy/ui/pages/root_route.dart';
import 'package:wakelock/wakelock.dart'; import 'package:wakelock/wakelock.dart';
import 'package:timezone/data/latest.dart' as tz; 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 { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
await HiveConfig.init(); await HiveConfig.init();
await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); // await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
try { try {
/// Wakelock support for Linux /// Wakelock support for Linux
@ -43,21 +41,20 @@ void main() async {
fallbackColor: BrandColors.primary, fallbackColor: BrandColors.primary,
); );
BlocOverrides.runZoned( Bloc.observer = SimpleBlocObserver();
() => runApp(
Localization( runApp(
child: MyApp( Localization(
lightThemeData: lightThemeData, child: SelfprivacyApp(
darkThemeData: darkThemeData, lightThemeData: lightThemeData,
), darkThemeData: darkThemeData,
), ),
), ),
blocObserver: SimpleBlocObserver(),
); );
} }
class MyApp extends StatelessWidget { class SelfprivacyApp extends StatelessWidget {
const MyApp({ SelfprivacyApp({
required this.lightThemeData, required this.lightThemeData,
required this.darkThemeData, required this.darkThemeData,
super.key, super.key,
@ -66,6 +63,8 @@ class MyApp extends StatelessWidget {
final ThemeData lightThemeData; final ThemeData lightThemeData;
final ThemeData darkThemeData; final ThemeData darkThemeData;
final _appRouter = RootRouter();
@override @override
Widget build(final BuildContext context) => Localization( Widget build(final BuildContext context) => Localization(
child: AnnotatedRegion<SystemUiOverlayStyle>( child: AnnotatedRegion<SystemUiOverlayStyle>(
@ -76,10 +75,11 @@ class MyApp extends StatelessWidget {
final BuildContext context, final BuildContext context,
final AppSettingsState appSettings, final AppSettingsState appSettings,
) => ) =>
MaterialApp( MaterialApp.router(
routeInformationParser: _appRouter.defaultRouteParser(),
routerDelegate: _appRouter.delegate(),
scaffoldMessengerKey: scaffoldMessengerKey:
getIt.get<NavigationService>().scaffoldMessengerKey, getIt.get<NavigationService>().scaffoldMessengerKey,
navigatorKey: getIt.get<NavigationService>().navigatorKey,
localizationsDelegates: context.localizationDelegates, localizationsDelegates: context.localizationDelegates,
supportedLocales: context.supportedLocales, supportedLocales: context.supportedLocales,
locale: context.locale, locale: context.locale,

View File

@ -1,7 +1,7 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:selfprivacy/ui/components/brand_cards/filled_card.dart'; import 'package:selfprivacy/ui/components/brand_cards/filled_card.dart';
import 'package:selfprivacy/ui/pages/setup/initializing/initializing.dart'; import 'package:selfprivacy/ui/router/router.dart';
import 'package:selfprivacy/utils/route_transitions/basic.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
class NotReadyCard extends StatelessWidget { class NotReadyCard extends StatelessWidget {
@ -13,11 +13,7 @@ class NotReadyCard extends StatelessWidget {
child: ListTile( child: ListTile(
contentPadding: contentPadding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 8), const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
onTap: () => Navigator.of(context).push( onTap: () => context.pushRoute(const InitializingRoute()),
materialRoute(
const InitializingPage(),
),
),
title: Text( title: Text(
'not_ready_card.in_menu'.tr(), 'not_ready_card.in_menu'.tr(),
style: Theme.of(context).textTheme.titleSmall?.copyWith( 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/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:ionicons/ionicons.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'; import 'package:selfprivacy/ui/helpers/modals.dart';
class BrandFab extends StatefulWidget { class BrandFab extends StatefulWidget {
const BrandFab({super.key}); const BrandFab({
this.extended = false,
super.key,
});
final bool extended;
@override @override
State<BrandFab> createState() => _BrandFabState(); State<BrandFab> createState() => _BrandFabState();
@ -64,20 +70,35 @@ class _BrandFabState extends State<BrandFab>
), ),
); );
}, },
child: AnimatedBuilder( isExtended: widget.extended,
animation: _colorTween, child: Row(
builder: (final BuildContext context, final Widget? child) { crossAxisAlignment: CrossAxisAlignment.center,
final double v = _animationController.value; mainAxisAlignment: MainAxisAlignment.center,
final IconData icon = children: [
v > 0.5 ? Ionicons.flash : Ionicons.flash_outline; AnimatedBuilder(
return Transform.scale( animation: _colorTween,
scale: 1 + (v < 0.5 ? v : 1 - v) * 2, builder: (final BuildContext context, final Widget? child) {
child: Icon( final double v = _animationController.value;
icon, final IconData icon =
color: _colorTween.value, 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:flutter/material.dart';
import 'package:selfprivacy/ui/components/pre_styled_buttons/flash_fab.dart'; import 'package:selfprivacy/ui/components/pre_styled_buttons/flash_fab.dart';
import 'package:selfprivacy/ui/helpers/widget_size.dart'; import 'package:selfprivacy/ui/helpers/widget_size.dart';
import 'package:selfprivacy/utils/breakpoints.dart';
class BrandHeroScreen extends StatelessWidget { class BrandHeroScreen extends StatelessWidget {
const BrandHeroScreen({ const BrandHeroScreen({
@ -13,6 +15,7 @@ class BrandHeroScreen extends StatelessWidget {
this.heroTitle = '', this.heroTitle = '',
this.heroSubtitle, this.heroSubtitle,
this.onBackButtonPressed, this.onBackButtonPressed,
this.bodyPadding = const EdgeInsets.all(16.0),
}); });
final List<Widget> children; final List<Widget> children;
@ -23,6 +26,7 @@ class BrandHeroScreen extends StatelessWidget {
final String heroTitle; final String heroTitle;
final String? heroSubtitle; final String? heroSubtitle;
final VoidCallback? onBackButtonPressed; final VoidCallback? onBackButtonPressed;
final EdgeInsetsGeometry bodyPadding;
@override @override
Widget build(final BuildContext context) { Widget build(final BuildContext context) {
@ -64,7 +68,7 @@ class BrandHeroScreen extends StatelessWidget {
), ),
), ),
SliverPadding( SliverPadding(
padding: const EdgeInsets.all(16.0), padding: bodyPadding,
sliver: SliverList( sliver: SliverList(
delegate: SliverChildListDelegate(children), delegate: SliverChildListDelegate(children),
), ),
@ -98,50 +102,50 @@ class HeroSliverAppBar extends StatefulWidget {
class _HeroSliverAppBarState extends State<HeroSliverAppBar> { class _HeroSliverAppBarState extends State<HeroSliverAppBar> {
Size _size = Size.zero; Size _size = Size.zero;
@override @override
Widget build(final BuildContext context) => SliverAppBar( Widget build(final BuildContext context) {
expandedHeight: final isMobile = Breakpoints.small.isActive(context);
widget.hasHeroIcon ? 148.0 + _size.height : 72.0 + _size.height, return SliverAppBar(
primary: true, expandedHeight:
pinned: true, widget.hasHeroIcon ? 148.0 + _size.height : 72.0 + _size.height,
stretch: true, primary: true,
leading: widget.hasBackButton pinned: isMobile,
? IconButton( stretch: true,
icon: const Icon(Icons.arrow_back), surfaceTintColor: isMobile ? null : Colors.transparent,
onPressed: widget.onBackButtonPressed ?? leading: (widget.hasBackButton && isMobile)
() => Navigator.of(context).pop(), ? const AutoLeadingButton()
) : const SizedBox.shrink(),
: null, flexibleSpace: FlexibleSpaceBar(
flexibleSpace: FlexibleSpaceBar( title: LayoutBuilder(
title: LayoutBuilder( builder: (final context, final constraints) => SizedBox(
builder: (final context, final constraints) => SizedBox( width: constraints.maxWidth - 72.0,
width: constraints.maxWidth - 72.0, child: WidgetSize(
child: WidgetSize( onChange: (final Size size) => setState(() => _size = size),
onChange: (final Size size) => setState(() => _size = size), child: Text(
child: Text( widget.heroTitle,
widget.heroTitle, style: Theme.of(context).textTheme.titleLarge?.copyWith(
style: Theme.of(context).textTheme.titleLarge?.copyWith( color: Theme.of(context).colorScheme.onSurface,
color: Theme.of(context).colorScheme.onSurface, ),
), overflow: TextOverflow.fade,
overflow: TextOverflow.fade, textAlign: TextAlign.center,
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/logic/models/state_types.dart';
import 'package:selfprivacy/ui/components/brand_button/brand_button.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_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_icons/brand_icons.dart';
import 'package:selfprivacy/ui/components/brand_text/brand_text.dart'; import 'package:selfprivacy/ui/components/brand_text/brand_text.dart';
import 'package:selfprivacy/ui/helpers/modals.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/devices/devices_cubit.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
import 'package:selfprivacy/logic/models/json/api_token.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/components/info_box/info_box.dart';
import 'package:selfprivacy/ui/pages/devices/new_device.dart'; import 'package:selfprivacy/ui/pages/devices/new_device.dart';
import 'package:selfprivacy/utils/route_transitions/basic.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/devices/devices_cubit.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
import 'package:selfprivacy/ui/components/brand_button/brand_button.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 { class NewDeviceScreen extends StatelessWidget {
const NewDeviceScreen({super.key}); 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/server_installation/server_installation_cubit.dart';
import 'package:selfprivacy/logic/cubit/dns_records/dns_records_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_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_icons/brand_icons.dart';
import 'package:selfprivacy/utils/network_utils.dart'; import 'package:selfprivacy/utils/network_utils.dart';

View File

@ -1,67 +1,70 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:selfprivacy/config/brand_theme.dart';
import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server_api.dart'; import 'package:selfprivacy/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:selfprivacy/ui/components/brand_text/brand_text.dart';
import 'package:package_info/package_info.dart'; import 'package:package_info/package_info.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
class AboutApplicationPage extends StatelessWidget { class AboutApplicationPage extends StatelessWidget {
const AboutApplicationPage({super.key}); const AboutApplicationPage({super.key});
@override @override
Widget build(final BuildContext context) => SafeArea( Widget build(final BuildContext context) {
child: Scaffold( final bool isReady = context.watch<ServerInstallationCubit>().state
appBar: PreferredSize( is ServerInstallationFinished;
preferredSize: const Size.fromHeight(52),
child: BrandHeader( return BrandHeroScreen(
title: 'about_application_page.title'.tr(), hasBackButton: true,
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( const SizedBox(height: 10),
padding: paddingH15V0, // Button to call showAboutDialog
TextButton(
onPressed: () => showAboutDialog(
context: context,
applicationName: 'SelfPrivacy',
applicationLegalese: '© 2022 SelfPrivacy',
// Link to privacy policy
children: [ 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( TextButton(
onPressed: () => showAboutDialog( onPressed: () => launchUrl(
context: context, Uri.parse('https://selfprivacy.ru/privacy-policy'),
applicationName: 'SelfPrivacy', mode: LaunchMode.externalApplication,
applicationLegalese: '© 2022 SelfPrivacy',
// Link to privacy policy
children: [
TextButton(
onPressed: () => launchUrl(
Uri.parse('https://selfprivacy.ru/privacy-policy'),
mode: LaunchMode.externalApplication,
),
child: Text('about_application_page.privacy_policy'.tr()),
),
],
), ),
child: const Text('Show about dialog'), 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 { Future<String> _packageVersion() async {
String packageVersion = 'unknown'; 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/logic/models/message.dart';
import 'package:selfprivacy/ui/components/brand_header/brand_header.dart'; import 'package:selfprivacy/ui/components/brand_header/brand_header.dart';
class Console extends StatefulWidget { class ConsolePage extends StatefulWidget {
const Console({super.key}); const ConsolePage({super.key});
@override @override
State<Console> createState() => _ConsoleState(); State<ConsolePage> createState() => _ConsolePageState();
} }
class _ConsoleState extends State<Console> { class _ConsolePageState extends State<ConsolePage> {
@override @override
void initState() { void initState() {
getIt.get<ConsoleModel>().addListener(update); 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:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:ionicons/ionicons.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_cards/filled_card.dart';
import 'package:selfprivacy/ui/components/brand_header/brand_header.dart'; import 'package:selfprivacy/ui/components/brand_header/brand_header.dart';
import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart'; import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart';
import 'package:selfprivacy/ui/pages/devices/devices.dart'; import 'package:selfprivacy/utils/breakpoints.dart';
import 'package:selfprivacy/ui/pages/recovery_key/recovery_key.dart'; import 'package:selfprivacy/ui/router/router.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';
class MorePage extends StatelessWidget { class MorePage extends StatelessWidget {
const MorePage({super.key}); const MorePage({super.key});
@ -34,12 +24,14 @@ class MorePage extends StatelessWidget {
context.watch<ApiServerVolumeCubit>().state.usesBinds; context.watch<ApiServerVolumeCubit>().state.usesBinds;
return Scaffold( return Scaffold(
appBar: PreferredSize( appBar: Breakpoints.small.isActive(context)
preferredSize: const Size.fromHeight(52), ? PreferredSize(
child: BrandHeader( preferredSize: const Size.fromHeight(52),
title: 'basis.more'.tr(), child: BrandHeader(
), title: 'basis.more'.tr(),
), ),
)
: null,
body: ListView( body: ListView(
children: [ children: [
Padding( Padding(
@ -50,7 +42,7 @@ class MorePage extends StatelessWidget {
_MoreMenuItem( _MoreMenuItem(
title: 'storage.start_migration_button'.tr(), title: 'storage.start_migration_button'.tr(),
iconData: Icons.drive_file_move_outline, iconData: Icons.drive_file_move_outline,
goTo: ServicesMigrationPage( goTo: () => ServicesMigrationRoute(
diskStatus: context diskStatus: context
.watch<ApiServerVolumeCubit>() .watch<ApiServerVolumeCubit>()
.state .state
@ -77,7 +69,7 @@ class MorePage extends StatelessWidget {
_MoreMenuItem( _MoreMenuItem(
title: 'more_page.configuration_wizard'.tr(), title: 'more_page.configuration_wizard'.tr(),
iconData: Icons.change_history_outlined, iconData: Icons.change_history_outlined,
goTo: const InitializingPage(), goTo: () => const InitializingRoute(),
subtitle: 'not_ready_card.in_menu'.tr(), subtitle: 'not_ready_card.in_menu'.tr(),
accent: true, accent: true,
), ),
@ -85,47 +77,43 @@ class MorePage extends StatelessWidget {
_MoreMenuItem( _MoreMenuItem(
title: 'more_page.create_ssh_key'.tr(), title: 'more_page.create_ssh_key'.tr(),
iconData: Ionicons.key_outline, iconData: Ionicons.key_outline,
goTo: const UserDetails( goTo: () => UserDetailsRoute(
login: 'root', login: 'root',
), ),
), ),
if (isReady) if (isReady)
_MoreMenuItem( _MoreMenuItem(
iconData: Icons.password_outlined, iconData: Icons.password_outlined,
goTo: const RecoveryKey(), goTo: () => const RecoveryKeyRoute(),
title: 'recovery_key.key_main_header'.tr(), title: 'recovery_key.key_main_header'.tr(),
), ),
if (isReady) if (isReady)
_MoreMenuItem( _MoreMenuItem(
iconData: Icons.devices_outlined, iconData: Icons.devices_outlined,
goTo: const DevicesScreen(), goTo: () => const DevicesRoute(),
title: 'devices.main_screen.header'.tr(), title: 'devices.main_screen.header'.tr(),
), ),
_MoreMenuItem( _MoreMenuItem(
title: 'more_page.application_settings'.tr(), title: 'more_page.application_settings'.tr(),
iconData: Icons.settings_outlined, iconData: Icons.settings_outlined,
goTo: const AppSettingsPage(), goTo: () => const AppSettingsRoute(),
),
_MoreMenuItem(
title: 'more_page.about_project'.tr(),
iconData: BrandIcons.engineer,
goTo: const AboutUsPage(),
), ),
_MoreMenuItem( _MoreMenuItem(
title: 'more_page.about_application'.tr(), title: 'more_page.about_application'.tr(),
iconData: BrandIcons.fire, iconData: BrandIcons.fire,
goTo: const AboutApplicationPage(), goTo: () => const AboutApplicationRoute(),
longGoTo: const DeveloperSettingsRoute(),
), ),
if (!isReady) if (!isReady)
_MoreMenuItem( _MoreMenuItem(
title: 'more_page.onboarding'.tr(), title: 'more_page.onboarding'.tr(),
iconData: BrandIcons.start, iconData: BrandIcons.start,
goTo: const OnboardingPage(nextPage: RootPage()), goTo: () => const OnboardingRoute(),
), ),
_MoreMenuItem( _MoreMenuItem(
title: 'more_page.console'.tr(), title: 'more_page.console'.tr(),
iconData: BrandIcons.terminal, iconData: BrandIcons.terminal,
goTo: const Console(), goTo: () => const ConsoleRoute(),
), ),
], ],
), ),
@ -140,14 +128,16 @@ class _MoreMenuItem extends StatelessWidget {
const _MoreMenuItem({ const _MoreMenuItem({
required this.iconData, required this.iconData,
required this.title, required this.title,
required this.goTo,
this.subtitle, this.subtitle,
this.goTo, this.longGoTo,
this.accent = false, this.accent = false,
}); });
final IconData iconData; final IconData iconData;
final String title; final String title;
final Widget? goTo; final PageRouteInfo Function() goTo;
final PageRouteInfo? longGoTo;
final String? subtitle; final String? subtitle;
final bool accent; final bool accent;
@ -160,9 +150,9 @@ class _MoreMenuItem extends StatelessWidget {
tertiary: accent, tertiary: accent,
child: ListTile( child: ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
onTap: goTo != null onTap: () => context.pushRoute(goTo()),
? () => Navigator.of(context).push(materialRoute(goTo!)) onLongPress:
: null, longGoTo != null ? () => context.pushRoute(longGoTo!) : null,
leading: Icon( leading: Icon(
iconData, iconData,
size: 24, size: 24,

View File

@ -1,13 +1,13 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:selfprivacy/logic/cubit/app_settings/app_settings_cubit.dart'; import 'package:selfprivacy/logic/cubit/app_settings/app_settings_cubit.dart';
import 'package:selfprivacy/ui/components/brand_button/brand_button.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'; import 'package:easy_localization/easy_localization.dart';
class OnboardingPage extends StatefulWidget { class OnboardingPage extends StatefulWidget {
const OnboardingPage({required this.nextPage, super.key}); const OnboardingPage({super.key});
final Widget nextPage;
@override @override
State<OnboardingPage> createState() => _OnboardingPageState(); State<OnboardingPage> createState() => _OnboardingPageState();
} }
@ -22,14 +22,14 @@ class _OnboardingPageState extends State<OnboardingPage> {
@override @override
Widget build(final BuildContext context) => Scaffold( Widget build(final BuildContext context) => Scaffold(
body: PageView( body: PageView(
controller: pageController, controller: pageController,
children: [ children: [
_withPadding(firstPage()), _withPadding(firstPage()),
_withPadding(secondPage()), _withPadding(secondPage()),
], ],
), ),
); );
Widget _withPadding(final Widget child) => Padding( Widget _withPadding(final Widget child) => Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
@ -142,10 +142,10 @@ class _OnboardingPageState extends State<OnboardingPage> {
BrandButton.rised( BrandButton.rised(
onPressed: () { onPressed: () {
context.read<AppSettingsCubit>().turnOffOnboarding(); context.read<AppSettingsCubit>().turnOffOnboarding();
Navigator.of(context).pushAndRemoveUntil( context.router.replaceAll([
materialRoute(widget.nextPage), const RootRoute(),
(final route) => false, const InitializingRoute(),
); ]);
}, },
text: 'basis.got_it'.tr(), 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/backup_details/backup_details.dart';
import 'package:selfprivacy/ui/pages/dns_details/dns_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/ui/pages/server_details/server_details_screen.dart';
import 'package:selfprivacy/utils/breakpoints.dart';
import 'package:selfprivacy/utils/route_transitions/basic.dart'; import 'package:selfprivacy/utils/route_transitions/basic.dart';
GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>(); GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
@ -61,12 +62,14 @@ class _ProvidersPageState extends State<ProvidersPage> {
} }
return Scaffold( return Scaffold(
appBar: PreferredSize( appBar: Breakpoints.small.isActive(context)
preferredSize: const Size.fromHeight(52), ? PreferredSize(
child: BrandHeader( preferredSize: const Size.fromHeight(52),
title: 'basis.providers_title'.tr(), child: BrandHeader(
), title: 'basis.providers_title'.tr(),
), ),
)
: null,
body: ListView( body: ListView(
padding: paddingH15V0, padding: paddingH15V0,
children: [ children: [

View File

@ -1,4 +1,3 @@
import 'package:cubit_form/cubit_form.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.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/logic/cubit/server_installation/server_installation_cubit.dart';
import 'package:selfprivacy/ui/components/brand_button/brand_button.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_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/ui/pages/recovery_key/recovery_key_receiving.dart';
import 'package:selfprivacy/utils/route_transitions/basic.dart'; import 'package:selfprivacy/utils/route_transitions/basic.dart';
class RecoveryKey extends StatefulWidget { class RecoveryKeyPage extends StatefulWidget {
const RecoveryKey({super.key}); const RecoveryKeyPage({super.key});
@override @override
State<RecoveryKey> createState() => _RecoveryKeyState(); State<RecoveryKeyPage> createState() => _RecoveryKeyPageState();
} }
class _RecoveryKeyState extends State<RecoveryKey> { class _RecoveryKeyPageState extends State<RecoveryKeyPage> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@ -29,7 +28,7 @@ class _RecoveryKeyState extends State<RecoveryKey> {
@override @override
Widget build(final BuildContext context) { Widget build(final BuildContext context) {
final RecoveryKeyState keyStatus = context.watch<RecoveryKeyCubit>().state; final RecoveryKeyState keyStatus = context.watch<RecoveryKeyCubit>().state;
final List<Widget> widgets; final List<Widget> widgets;
String? subtitle = String? subtitle =
keyStatus.exists ? null : 'recovery_key.key_main_description'.tr(); keyStatus.exists ? null : 'recovery_key.key_main_description'.tr();

View File

@ -1,7 +1,7 @@
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:selfprivacy/ui/components/brand_button/brand_button.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'; import 'package:selfprivacy/ui/components/info_box/info_box.dart';
class RecoveryKeyReceiving extends StatelessWidget { 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: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/logic/cubit/server_installation/server_installation_cubit.dart';
import 'package:selfprivacy/ui/components/brand_tab_bar/brand_tab_bar.dart'; import 'package:selfprivacy/ui/layouts/root_scaffold_with_navigation.dart';
import 'package:selfprivacy/ui/pages/more/more.dart'; import 'package:selfprivacy/ui/router/root_destinations.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/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}); const RootPage({super.key});
@override @override
State<RootPage> createState() => _RootPageState(); State<RootPage> createState() => _RootPageState();
@override
Widget wrappedRoute(final BuildContext context) => this;
} }
class _RootPageState extends State<RootPage> with TickerProviderStateMixin { class _RootPageState extends State<RootPage> with TickerProviderStateMixin {
late TabController tabController; bool shouldUseSplitView() => false;
late final AnimationController _controller = AnimationController( final destinations = rootDestinations;
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();
}
@override @override
Widget build(final BuildContext context) { Widget build(final BuildContext context) {
final bool isReady = context.watch<ServerInstallationCubit>().state final bool isReady = context.watch<ServerInstallationCubit>().state
is ServerInstallationFinished; is ServerInstallationFinished;
return Provider<ChangeTab>( if (context.read<AppSettingsCubit>().state.isOnboardingShowing) {
create: (final _) => ChangeTab(tabController.animateTo), context.router.replace(const OnboardingRoute());
child: Scaffold( }
body: TabBarView(
controller: tabController, return AutoRouter(
children: const [ builder: (final context, final child) {
ProvidersPage(), final currentDestinationIndex = destinations.indexWhere(
ServicesPage(), (final destination) =>
UsersPage(), context.router.isRouteActive(destination.route.routeName),
MorePage(), );
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/logic/models/job.dart';
import 'package:selfprivacy/ui/components/brand_button/segmented_buttons.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_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_icons/brand_icons.dart';
import 'package:selfprivacy/ui/components/brand_loader/brand_loader.dart'; import 'package:selfprivacy/ui/components/brand_loader/brand_loader.dart';
import 'package:selfprivacy/ui/components/brand_text/brand_text.dart'; import 'package:selfprivacy/ui/components/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/cubit/server_jobs/server_jobs_cubit.dart';
import 'package:selfprivacy/logic/models/json/server_job.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_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/components/brand_linear_indicator/brand_linear_indicator.dart';
import 'package:selfprivacy/ui/pages/root_route.dart'; import 'package:selfprivacy/ui/pages/root_route.dart';
import 'package:selfprivacy/utils/route_transitions/basic.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/disk_size.dart';
import 'package:selfprivacy/logic/models/price.dart'; import 'package:selfprivacy/logic/models/price.dart';
import 'package:selfprivacy/ui/components/brand_button/brand_button.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/logic/models/disk_status.dart';
import 'package:selfprivacy/ui/pages/root_route.dart'; import 'package:selfprivacy/ui/pages/root_route.dart';
import 'package:selfprivacy/utils/route_transitions/basic.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/cubit/services/services_cubit.dart';
import 'package:selfprivacy/logic/models/service.dart'; import 'package:selfprivacy/logic/models/service.dart';
import 'package:selfprivacy/ui/components/brand_button/outlined_button.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/logic/models/disk_status.dart';
import 'package:selfprivacy/ui/pages/server_storage/extending_volume.dart'; import 'package:selfprivacy/ui/pages/server_storage/extending_volume.dart';
import 'package:selfprivacy/ui/components/storage_list_items/server_storage_list_item.dart'; import 'package:selfprivacy/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/job.dart';
import 'package:selfprivacy/logic/models/service.dart'; import 'package:selfprivacy/logic/models/service.dart';
import 'package:selfprivacy/ui/components/brand_cards/filled_card.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/ui/pages/server_storage/binds_migration/services_migration.dart';
import 'package:selfprivacy/utils/launch_url.dart'; import 'package:selfprivacy/utils/launch_url.dart';
import 'package:selfprivacy/utils/route_transitions/basic.dart'; import 'package:selfprivacy/utils/route_transitions/basic.dart';
@ -50,7 +50,11 @@ class _ServicePageState extends State<ServicePage> {
service.svgIcon, service.svgIcon,
width: 48.0, width: 48.0,
height: 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, heroTitle: service.displayName,
children: [ 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:selfprivacy/ui/components/not_ready_card/not_ready_card.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:selfprivacy/ui/pages/services/service_page.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/launch_url.dart';
import 'package:selfprivacy/utils/route_transitions/basic.dart'; import 'package:selfprivacy/utils/route_transitions/basic.dart';
import 'package:selfprivacy/utils/ui_helpers.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)); .sort((final a, final b) => a.status.index.compareTo(b.status.index));
return Scaffold( return Scaffold(
appBar: PreferredSize( appBar: Breakpoints.small.isActive(context)
preferredSize: const Size.fromHeight(52), ? PreferredSize(
child: BrandHeader( preferredSize: const Size.fromHeight(52),
title: 'basis.services'.tr(), child: BrandHeader(
), title: 'basis.services'.tr(),
), ),
)
: null,
body: RefreshIndicator( body: RefreshIndicator(
onRefresh: () async { onRefresh: () async {
context.read<ServicesCubit>().reload(); 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:cubit_form/cubit_form.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:selfprivacy/config/brand_theme.dart';
import 'package:selfprivacy/logic/cubit/forms/setup/initializing/provider_form_cubit.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/server_installation/server_installation_cubit.dart';
import 'package:selfprivacy/logic/cubit/forms/factories/field_cubit_factory.dart'; import 'package:selfprivacy/logic/cubit/forms/factories/field_cubit_factory.dart';
@ -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/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/domain_setup_cubit.dart';
import 'package:selfprivacy/logic/cubit/forms/setup/initializing/root_user_form_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_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_text/brand_text.dart';
import 'package:selfprivacy/ui/components/brand_timer/brand_timer.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/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_provider_picker.dart';
import 'package:selfprivacy/ui/pages/setup/initializing/server_type_picker.dart'; import 'package:selfprivacy/ui/pages/setup/initializing/server_type_picker.dart';
import 'package:selfprivacy/ui/pages/setup/recovering/recovery_routing.dart'; import 'package:selfprivacy/ui/pages/setup/recovering/recovery_routing.dart';
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 { class InitializingPage extends StatelessWidget {
const InitializingPage({super.key}); const InitializingPage({super.key});
@ -48,99 +50,123 @@ class InitializingPage extends StatelessWidget {
][cubit.state.progress.index](); ][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>( return BlocListener<ServerInstallationCubit, ServerInstallationState>(
listener: (final context, final state) { listener: (final context, final state) {
if (cubit.state is ServerInstallationFinished) { if (cubit.state is ServerInstallationFinished) {
Navigator.of(context) context.router.popUntilRoot();
.pushReplacement(materialRoute(const RootPage()));
} }
}, },
child: Scaffold( child: Scaffold(
appBar: AppBar( endDrawer: const SupportDrawer(),
actions: [ endDrawerEnableOpenDragGesture: false,
if (cubit.state is ServerInstallationFinished) appBar: Breakpoints.large.isActive(context)
IconButton( ? null
icon: const Icon(Icons.check), : AppBar(
onPressed: () { actions: [
Navigator.of(context) if (cubit.state is ServerInstallationFinished)
.pushReplacement(materialRoute(const RootPage())); IconButton(
}, icon: const Icon(Icons.check),
) onPressed: () {
], context.router.popUntilRoot();
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,
);
},
),
), ),
if (cubit.state is ServerInstallationEmpty || const SizedBox.shrink(),
cubit.state is ServerInstallationNotFinished) ],
Container( title: Text(
alignment: Alignment.center, 'more_page.configuration_wizard'.tr(),
child: BrandButton.text( ),
title: 'basis.connect_to_existing'.tr(), bottom: PreferredSize(
onPressed: () { preferredSize: const Size.fromHeight(28),
Navigator.of(context).push( child: Padding(
materialRoute( padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
const RecoveryRouting(), 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) => Widget _stepCloudflare(final ServerInstallationCubit initializingCubit) =>
BlocProvider( BlocProvider(
create: (final context) => DnsProviderFormCubit(initializingCubit), create: (final context) => DnsProviderFormCubit(initializingCubit),
child: Builder( child: Builder(
builder: (final context) => Column( builder: (final context) => ResponsiveLayoutWithInfobox(
crossAxisAlignment: CrossAxisAlignment.start, topChild: Column(
children: [ crossAxisAlignment: CrossAxisAlignment.start,
Text( children: [
'${'initializing.connect_to_server_provider'.tr()}Cloudflare', Text(
style: Theme.of(context).textTheme.headlineSmall, '${'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(),
), ),
), const SizedBox(height: 16),
const SizedBox(height: 32), Text(
BrandButton.rised( 'initializing.manage_domain_dns'.tr(),
onPressed: () => style: Theme.of(context).textTheme.bodyMedium,
context.read<DnsProviderFormCubit>().trySubmit(), ),
text: 'basis.connect'.tr(), ],
), ),
const SizedBox(height: 10), primaryColumn: Column(
BrandButton.text( crossAxisAlignment: CrossAxisAlignment.start,
onPressed: () => _showModal( children: [
context, CubitFormTextField(
const _HowTo( formFieldCubit: context.read<DnsProviderFormCubit>().apiKey,
fileName: 'how_cloudflare', 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( child: Builder(
builder: (final context) { builder: (final context) {
final formCubitState = context.watch<BackblazeFormCubit>().state; final formCubitState = context.watch<BackblazeFormCubit>().state;
return Column( return ResponsiveLayoutWithInfobox(
crossAxisAlignment: CrossAxisAlignment.start, topChild: Column(
children: [ crossAxisAlignment: CrossAxisAlignment.start,
Text( children: [
'${'initializing.connect_to_server_provider'.tr()}Backblaze', Text(
style: Theme.of(context).textTheme.headlineSmall, '${'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',
), ),
), ],
const SizedBox(height: 16), ),
CubitFormTextField( primaryColumn: Column(
formFieldCubit: crossAxisAlignment: CrossAxisAlignment.start,
context.read<BackblazeFormCubit>().applicationKey, children: [
textAlign: TextAlign.center, CubitFormTextField(
scrollPadding: const EdgeInsets.only(bottom: 70), formFieldCubit: context.read<BackblazeFormCubit>().keyId,
decoration: const InputDecoration( textAlign: TextAlign.center,
hintText: 'Master Application Key', scrollPadding: const EdgeInsets.only(bottom: 70),
), decoration: const InputDecoration(
), hintText: 'KeyID',
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',
), ),
), ),
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) { builder: (final context) {
final DomainSetupState state = final DomainSetupState state =
context.watch<DomainSetupCubit>().state; context.watch<DomainSetupCubit>().state;
return SizedBox( return ResponsiveLayoutWithInfobox(
width: double.infinity, topChild: Column(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
@ -310,7 +340,11 @@ class InitializingPage extends StatelessWidget {
'initializing.use_this_domain_text'.tr(), 'initializing.use_this_domain_text'.tr(),
style: Theme.of(context).textTheme.bodyMedium, style: Theme.of(context).textTheme.bodyMedium,
), ),
const SizedBox(height: 32), ],
),
primaryColumn: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (state is Empty) if (state is Empty)
Text( Text(
'initializing.no_connected_domains'.tr(), 'initializing.no_connected_domains'.tr(),
@ -350,7 +384,7 @@ class InitializingPage extends StatelessWidget {
], ],
if (state is Empty) ...[ if (state is Empty) ...[
const SizedBox(height: 30), const SizedBox(height: 30),
BrandButton.rised( BrandButton.filled(
onPressed: () => context.read<DomainSetupCubit>().load(), onPressed: () => context.read<DomainSetupCubit>().load(),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
@ -367,7 +401,7 @@ class InitializingPage extends StatelessWidget {
], ],
if (state is Loaded) ...[ if (state is Loaded) ...[
const SizedBox(height: 32), const SizedBox(height: 32),
BrandButton.rised( BrandButton.filled(
onPressed: () => onPressed: () =>
context.read<DomainSetupCubit>().saveDomain(), context.read<DomainSetupCubit>().saveDomain(),
text: 'initializing.save_domain'.tr(), text: 'initializing.save_domain'.tr(),
@ -388,74 +422,83 @@ class InitializingPage extends StatelessWidget {
builder: (final context) { builder: (final context) {
final formCubitState = context.watch<RootUserFormCubit>().state; final formCubitState = context.watch<RootUserFormCubit>().state;
return Column( return ResponsiveLayoutWithInfobox(
crossAxisAlignment: CrossAxisAlignment.start, topChild: Column(
children: [ crossAxisAlignment: CrossAxisAlignment.start,
Text( children: [
'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)
Text( Text(
'users.username_rule'.tr(), 'initializing.create_master_account'.tr(),
style: TextStyle( style: Theme.of(context).textTheme.headlineSmall,
color: Theme.of(context).colorScheme.error, ),
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), const SizedBox(height: 16),
CubitFormTextField( BlocBuilder<FieldCubit<bool>, FieldCubitState<bool>>(
formFieldCubit: context.read<RootUserFormCubit>().userName, bloc: context.read<RootUserFormCubit>().isVisible,
textAlign: TextAlign.center, builder: (final context, final state) {
scrollPadding: const EdgeInsets.only(bottom: 70), final bool isVisible = state.value;
decoration: InputDecoration( return CubitFormTextField(
hintText: 'basis.username'.tr(), obscureText: !isVisible,
), formFieldCubit:
), context.read<RootUserFormCubit>().password,
const SizedBox(height: 16), textAlign: TextAlign.center,
BlocBuilder<FieldCubit<bool>, FieldCubitState<bool>>( scrollPadding: const EdgeInsets.only(bottom: 70),
bloc: context.read<RootUserFormCubit>().isVisible, decoration: InputDecoration(
builder: (final context, final state) { hintText: 'basis.password'.tr(),
final bool isVisible = state.value; suffixIcon: IconButton(
return CubitFormTextField( icon: Icon(
obscureText: !isVisible, isVisible
formFieldCubit: ? Icons.visibility
context.read<RootUserFormCubit>().password, : Icons.visibility_off,
textAlign: TextAlign.center, ),
scrollPadding: const EdgeInsets.only(bottom: 70), onPressed: () => context
decoration: InputDecoration( .read<RootUserFormCubit>()
hintText: 'basis.password'.tr(), .isVisible
suffixIcon: IconButton( .setValue(!isVisible),
icon: Icon(
isVisible ? Icons.visibility : Icons.visibility_off,
), ),
onPressed: () => context suffixIconConstraints:
.read<RootUserFormCubit>() const BoxConstraints(minWidth: 60),
.isVisible prefixIconConstraints:
.setValue(!isVisible), const BoxConstraints(maxWidth: 60),
prefixIcon: Container(),
), ),
suffixIconConstraints: );
const BoxConstraints(minWidth: 60), },
prefixIconConstraints: ),
const BoxConstraints(maxWidth: 60), const SizedBox(height: 32),
prefixIcon: Container(), BrandButton.filled(
), onPressed: formCubitState.isSubmitting
); ? null
}, : () => context.read<RootUserFormCubit>().trySubmit(),
), text: 'basis.connect'.tr(),
const SizedBox(height: 32), ),
BrandButton.rised( ],
onPressed: formCubitState.isSubmitting ),
? null
: () => context.read<RootUserFormCubit>().trySubmit(),
text: 'basis.connect'.tr(),
),
],
); );
}, },
), ),
@ -465,27 +508,28 @@ class InitializingPage extends StatelessWidget {
final bool isLoading = final bool isLoading =
(appConfigCubit.state as ServerInstallationNotFinished).isLoading; (appConfigCubit.state as ServerInstallationNotFinished).isLoading;
return Builder( return Builder(
builder: (final context) => Column( builder: (final context) => ResponsiveLayoutWithInfobox(
crossAxisAlignment: CrossAxisAlignment.start, topChild: Column(
children: [ crossAxisAlignment: CrossAxisAlignment.start,
Text( children: [
'initializing.final'.tr(), Text(
style: Theme.of(context).textTheme.headlineSmall, 'initializing.final'.tr(),
), style: Theme.of(context).textTheme.headlineSmall,
const SizedBox(height: 16), ),
Text( const SizedBox(height: 16),
'initializing.create_server'.tr(), Text(
style: Theme.of(context).textTheme.bodyMedium, 'initializing.create_server'.tr(),
), style: Theme.of(context).textTheme.bodyMedium,
const SizedBox(height: 128), ),
BrandButton.rised( ],
onPressed: ),
isLoading ? null : appConfigCubit.createServerAndSetDnsRecords, primaryColumn: BrandButton.filled(
text: isLoading onPressed:
? 'basis.loading'.tr() isLoading ? null : appConfigCubit.createServerAndSetDnsRecords,
: 'initializing.create_server'.tr(), text: isLoading
), ? 'basis.loading'.tr()
], : 'initializing.create_server'.tr(),
),
), ),
); );
} }
@ -514,84 +558,200 @@ class InitializingPage extends StatelessWidget {
return Builder( return Builder(
builder: (final context) => SizedBox( builder: (final context) => SizedBox(
width: double.infinity, width: double.infinity,
child: Column( child: ResponsiveLayoutWithInfobox(
crossAxisAlignment: CrossAxisAlignment.start, topChild: Column(
children: [ crossAxisAlignment: CrossAxisAlignment.start,
Text( children: [
'initializing.checks'.tr(args: [doneCount.toString(), '4']),
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 16),
if (text != null)
Text( Text(
text, 'initializing.checks'.tr(args: [doneCount.toString(), '4']),
style: Theme.of(context).textTheme.bodyMedium, style: Theme.of(context).textTheme.headlineSmall,
), ),
const SizedBox(height: 128), const SizedBox(height: 16),
const SizedBox(height: 10), if (text != null)
if (doneCount == 0 && state.dnsMatches != null) Text(
Column( text,
children: state.dnsMatches!.entries.map((final entry) { style: Theme.of(context).textTheme.bodyMedium,
final String domain = entry.key; ),
final bool isCorrect = entry.value; ],
return Row( ),
children: [ primaryColumn: Column(
if (isCorrect) crossAxisAlignment: CrossAxisAlignment.start,
const Icon(Icons.check, color: Colors.green), children: [
if (!isCorrect) const SizedBox(height: 128),
const Icon(Icons.schedule, color: Colors.amber), const SizedBox(height: 10),
const SizedBox(width: 10), if (doneCount == 0 && state.dnsMatches != null)
Text(domain), Column(
], children: state.dnsMatches!.entries.map((final entry) {
); final String domain = entry.key;
}).toList(), final bool isCorrect = entry.value;
), return Row(
const SizedBox(height: 10), children: [
if (!state.isLoading) if (isCorrect)
Row( const Icon(Icons.check, color: Colors.green),
children: [ if (!isCorrect)
Text( const Icon(Icons.schedule, color: Colors.amber),
'initializing.until_the_next_check'.tr(), const SizedBox(width: 10),
style: Theme.of(context).textTheme.bodyMedium, Text(domain),
), ],
BrandTimer( );
startDateTime: state.timerStart!, }).toList(),
duration: state.duration!, ),
) const SizedBox(height: 10),
], if (!state.isLoading)
), Row(
if (state.isLoading) children: [
Text( Text(
'initializing.check'.tr(), 'initializing.until_the_next_check'.tr(),
style: Theme.of(context).textTheme.bodyMedium, 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 { class _ProgressDrawer extends StatelessWidget {
const _HowTo({ const _ProgressDrawer({
required this.fileName, required this.steps,
required this.cubit,
required this.constraints,
}); });
final String fileName; final List<String> steps;
final ServerInstallationCubit cubit;
final BoxConstraints constraints;
@override @override
Widget build(final BuildContext context) => BrandBottomSheet( Widget build(final BuildContext context) => SizedBox(
isExpended: true, width: 300,
child: Padding( height: constraints.maxHeight,
padding: paddingH15V0, child: Drawer(
child: ListView( child: Column(
padding: const EdgeInsets.symmetric(vertical: 16), crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
BrandMarkdown( Padding(
fileName: fileName, 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:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:selfprivacy/config/brand_theme.dart';
import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart'; import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart';
import 'package:selfprivacy/logic/cubit/forms/setup/initializing/provider_form_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/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/brand_button.dart';
import 'package:selfprivacy/ui/components/brand_button/outlined_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_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/components/info_box/info_box.dart';
import 'package:selfprivacy/ui/layouts/responsive_layout_with_infobox.dart';
import 'package:selfprivacy/utils/launch_url.dart'; import 'package:selfprivacy/utils/launch_url.dart';
class ServerProviderPicker extends StatefulWidget { class ServerProviderPicker extends StatefulWidget {
@ -98,56 +97,49 @@ class ProviderInputDataPage extends StatelessWidget {
final ProviderFormCubit providerCubit; final ProviderFormCubit providerCubit;
@override @override
Widget build(final BuildContext context) => Column( Widget build(final BuildContext context) => ResponsiveLayoutWithInfobox(
crossAxisAlignment: CrossAxisAlignment.start, topChild: Column(
children: [ crossAxisAlignment: CrossAxisAlignment.start,
Text( children: [
"${'initializing.connect_to_server_provider'.tr()}${providerInfo.providerType.displayName}", Text(
style: Theme.of(context).textTheme.headlineSmall, "${'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',
), ),
), const SizedBox(height: 16),
const SizedBox(height: 32), Text(
BrandButton.filled( 'initializing.connect_to_server_provider_text'.tr(),
child: Text('basis.connect'.tr()), style: Theme.of(context).textTheme.bodyMedium,
onPressed: () => providerCubit.trySubmit(), ),
), ],
const SizedBox(height: 10), ),
BrandOutlinedButton( primaryColumn: Column(
child: Text('initializing.how'.tr()), crossAxisAlignment: CrossAxisAlignment.start,
onPressed: () => showModalBottomSheet<void>( children: [
context: context, CubitFormTextField(
isScrollControlled: true, formFieldCubit: providerCubit.apiKey,
backgroundColor: Colors.transparent, textAlign: TextAlign.center,
builder: (final BuildContext context) => BrandBottomSheet( scrollPadding: const EdgeInsets.only(bottom: 70),
isExpended: true, decoration: const InputDecoration(
child: Padding( hintText: 'Provider API Token',
padding: paddingH15V0,
child: ListView(
padding: const EdgeInsets.symmetric(vertical: 16),
children: [
BrandMarkdown(
fileName: providerInfo.pathToHow,
),
],
),
),
), ),
), ),
), 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 @override
Widget build(final BuildContext context) => SizedBox( Widget build(final BuildContext context) => SizedBox(
width: double.infinity, width: double.infinity,
child: Column( child: ResponsiveLayoutWithInfobox(
crossAxisAlignment: CrossAxisAlignment.start, topChild: Column(
children: [ crossAxisAlignment: CrossAxisAlignment.start,
Text( children: [
'initializing.connect_to_server'.tr(), Text(
style: Theme.of(context).textTheme.headlineSmall, 'initializing.connect_to_server'.tr(),
), style: Theme.of(context).textTheme.headlineSmall,
const SizedBox(height: 10), ),
Text( const SizedBox(height: 10),
'initializing.select_provider'.tr(), Text(
style: Theme.of(context).textTheme.bodyMedium, 'initializing.select_provider'.tr(),
), style: Theme.of(context).textTheme.bodyMedium,
const SizedBox(height: 10), ),
OutlinedCard( ],
child: Padding( ),
padding: const EdgeInsets.all(16.0), primaryColumn: Column(
child: Column( children: [
crossAxisAlignment: CrossAxisAlignment.start, OutlinedCard(
children: [ child: Padding(
Row( padding: const EdgeInsets.all(16.0),
children: [ child: Column(
Container( crossAxisAlignment: CrossAxisAlignment.start,
width: 40, children: [
height: 40, Row(
padding: const EdgeInsets.all(10), children: [
decoration: BoxDecoration( Container(
borderRadius: BorderRadius.circular(40), width: 40,
color: const Color(0xFFD50C2D), 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( const SizedBox(width: 16),
'assets/images/logos/hetzner.svg', Text(
'Hetzner Cloud',
style: Theme.of(context).textTheme.titleMedium,
), ),
), ],
const SizedBox(width: 16), ),
Text( const SizedBox(height: 16),
'Hetzner Cloud', Text(
style: Theme.of(context).textTheme.titleMedium, 'initializing.select_provider_countries_title'.tr(),
), style: Theme.of(context).textTheme.bodyLarge,
], ),
), Text(
const SizedBox(height: 16), 'initializing.select_provider_countries_text_hetzner'
Text( .tr(),
'initializing.select_provider_countries_title'.tr(), style: Theme.of(context).textTheme.bodySmall,
style: Theme.of(context).textTheme.bodyLarge, ),
), const SizedBox(height: 16),
Text( Text(
'initializing.select_provider_countries_text_hetzner' 'initializing.select_provider_price_title'.tr(),
.tr(), style: Theme.of(context).textTheme.bodyLarge,
style: Theme.of(context).textTheme.bodySmall, ),
), Text(
const SizedBox(height: 16), 'initializing.select_provider_price_text_hetzner'.tr(),
Text( style: Theme.of(context).textTheme.bodySmall,
'initializing.select_provider_price_title'.tr(), ),
style: Theme.of(context).textTheme.bodyLarge, const SizedBox(height: 16),
), Text(
Text( 'initializing.select_provider_payment_title'.tr(),
'initializing.select_provider_price_text_hetzner'.tr(), style: Theme.of(context).textTheme.bodyLarge,
style: Theme.of(context).textTheme.bodySmall, ),
), Text(
const SizedBox(height: 16), 'initializing.select_provider_payment_text_hetzner'
Text( .tr(),
'initializing.select_provider_payment_title'.tr(), style: Theme.of(context).textTheme.bodySmall,
style: Theme.of(context).textTheme.bodyLarge, ),
), const SizedBox(height: 16),
Text( Text(
'initializing.select_provider_payment_text_hetzner'.tr(), 'initializing.select_provider_email_notice'.tr(),
style: Theme.of(context).textTheme.bodySmall, style: Theme.of(context).textTheme.bodySmall,
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
Text( BrandButton.filled(
'initializing.select_provider_email_notice'.tr(), child: Text('basis.select'.tr()),
style: Theme.of(context).textTheme.bodySmall, onPressed: () {
), serverInstallationCubit
const SizedBox(height: 16), .setServerProviderType(ServerProvider.hetzner);
BrandButton.filled( callback(ServerProvider.hetzner);
child: Text('basis.select'.tr()), },
onPressed: () { ),
serverInstallationCubit // Outlined button that will open website
.setServerProviderType(ServerProvider.hetzner); BrandOutlinedButton(
callback(ServerProvider.hetzner); onPressed: () =>
}, launchURL('https://www.hetzner.com/cloud'),
), title: 'initializing.select_provider_site_button'.tr(),
// 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),
const SizedBox(height: 16), OutlinedCard(
OutlinedCard( child: Padding(
child: Padding( padding: const EdgeInsets.all(16.0),
padding: const EdgeInsets.all(16.0), child: Column(
child: Column( crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, children: [
children: [ Row(
Row( children: [
children: [ Container(
Container( width: 40,
width: 40, height: 40,
height: 40, padding: const EdgeInsets.all(10),
padding: const EdgeInsets.all(10), decoration: BoxDecoration(
decoration: BoxDecoration( borderRadius: BorderRadius.circular(40),
borderRadius: BorderRadius.circular(40), color: const Color(0xFF0080FF),
color: const Color(0xFF0080FF), ),
child: SvgPicture.asset(
'assets/images/logos/digital_ocean.svg',
),
), ),
child: SvgPicture.asset( const SizedBox(width: 16),
'assets/images/logos/digital_ocean.svg', Text(
'Digital Ocean',
style: Theme.of(context).textTheme.titleMedium,
), ),
), ],
const SizedBox(width: 16), ),
Text( const SizedBox(height: 16),
'Digital Ocean', Text(
style: Theme.of(context).textTheme.titleMedium, 'initializing.select_provider_countries_title'.tr(),
), style: Theme.of(context).textTheme.bodyLarge,
], ),
), Text(
const SizedBox(height: 16), 'initializing.select_provider_countries_text_do'.tr(),
Text( style: Theme.of(context).textTheme.bodySmall,
'initializing.select_provider_countries_title'.tr(), ),
style: Theme.of(context).textTheme.bodyLarge, const SizedBox(height: 16),
), Text(
Text( 'initializing.select_provider_price_title'.tr(),
'initializing.select_provider_countries_text_do'.tr(), style: Theme.of(context).textTheme.bodyLarge,
style: Theme.of(context).textTheme.bodySmall, ),
), Text(
const SizedBox(height: 16), 'initializing.select_provider_price_text_do'.tr(),
Text( style: Theme.of(context).textTheme.bodySmall,
'initializing.select_provider_price_title'.tr(), ),
style: Theme.of(context).textTheme.bodyLarge, const SizedBox(height: 16),
), Text(
Text( 'initializing.select_provider_payment_title'.tr(),
'initializing.select_provider_price_text_do'.tr(), style: Theme.of(context).textTheme.bodyLarge,
style: Theme.of(context).textTheme.bodySmall, ),
), Text(
const SizedBox(height: 16), 'initializing.select_provider_payment_text_do'.tr(),
Text( style: Theme.of(context).textTheme.bodySmall,
'initializing.select_provider_payment_title'.tr(), ),
style: Theme.of(context).textTheme.bodyLarge, const SizedBox(height: 16),
), BrandButton.filled(
Text( child: Text('basis.select'.tr()),
'initializing.select_provider_payment_text_do'.tr(), onPressed: () {
style: Theme.of(context).textTheme.bodySmall, serverInstallationCubit.setServerProviderType(
), ServerProvider.digitalOcean,
const SizedBox(height: 16), );
BrandButton.filled( callback(ServerProvider.digitalOcean);
child: Text('basis.select'.tr()), },
onPressed: () { ),
serverInstallationCubit // Outlined button that will open website
.setServerProviderType(ServerProvider.digitalOcean); BrandOutlinedButton(
callback(ServerProvider.digitalOcean); onPressed: () =>
}, launchURL('https://www.digitalocean.com'),
), title: 'initializing.select_provider_site_button'.tr(),
// 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/logic/models/server_type.dart';
import 'package:selfprivacy/ui/components/brand_button/brand_button.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/components/info_box/info_box.dart';
import 'package:selfprivacy/ui/layouts/responsive_layout_with_infobox.dart';
class ServerTypePicker extends StatefulWidget { class ServerTypePicker extends StatefulWidget {
const ServerTypePicker({ const ServerTypePicker({
@ -70,50 +71,67 @@ class SelectLocationPage extends StatelessWidget {
if ((snapshot.data as List<ServerProviderLocation>).isEmpty) { if ((snapshot.data as List<ServerProviderLocation>).isEmpty) {
return Text('initializing.no_locations_found'.tr()); return Text('initializing.no_locations_found'.tr());
} }
return Column( return ResponsiveLayoutWithInfobox(
children: [ topChild: Column(
Text( crossAxisAlignment: CrossAxisAlignment.start,
'initializing.choose_location_type'.tr(), children: [
style: Theme.of(context).textTheme.headlineSmall, Text(
), 'initializing.choose_location_type'.tr(),
const SizedBox(height: 16), style: Theme.of(context).textTheme.headlineSmall,
Text( ),
'initializing.choose_location_type_text'.tr(), const SizedBox(height: 16),
style: Theme.of(context).textTheme.bodyMedium, Text(
), 'initializing.choose_location_type_text'.tr(),
const SizedBox(height: 16), style: Theme.of(context).textTheme.bodyMedium,
...(snapshot.data! as List<ServerProviderLocation>).map( ),
(final location) => SizedBox( ],
width: double.infinity, ),
child: InkWell( primaryColumn: Column(
onTap: () { crossAxisAlignment: CrossAxisAlignment.start,
callback(location); children: [
}, ...(snapshot.data! as List<ServerProviderLocation>).map(
child: Card( (final location) => Column(
child: Padding( children: [
padding: const EdgeInsets.all(16.0), SizedBox(
child: Column( width: double.infinity,
crossAxisAlignment: CrossAxisAlignment.start, child: Card(
children: [ clipBehavior: Clip.antiAlias,
Text( child: InkResponse(
'${location.flag ?? ''} ${location.title}', highlightShape: BoxShape.rectangle,
style: Theme.of(context).textTheme.titleMedium, onTap: () {
), callback(location);
const SizedBox(height: 8), },
if (location.description != null) child: Padding(
Text( padding: const EdgeInsets.all(16.0),
location.description!, child: Column(
style: Theme.of(context).textTheme.bodyMedium, 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 { } else {
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());
@ -180,121 +198,145 @@ class SelectTypePage extends StatelessWidget {
], ],
); );
} }
return Column( return ResponsiveLayoutWithInfobox(
crossAxisAlignment: CrossAxisAlignment.start, topChild: Column(
children: [ crossAxisAlignment: CrossAxisAlignment.start,
Text( children: [
'initializing.choose_server_type'.tr(), Text(
style: Theme.of(context).textTheme.headlineSmall, 'initializing.choose_server_type'.tr(),
), style: Theme.of(context).textTheme.headlineSmall,
const SizedBox(height: 16), ),
Text( const SizedBox(height: 16),
'initializing.choose_server_type_text'.tr(), Text(
style: Theme.of(context).textTheme.bodyMedium, '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, primaryColumn: Column(
child: InkWell( crossAxisAlignment: CrossAxisAlignment.start,
onTap: () { children: [
serverInstallationCubit.setServerType(type); ...(snapshot.data! as List<ServerType>).map(
}, (final type) => Column(
child: Card( children: [
child: Padding( SizedBox(
padding: const EdgeInsets.all(16.0), width: double.infinity,
child: Column( child: InkWell(
crossAxisAlignment: CrossAxisAlignment.start, onTap: () {
children: [ serverInstallationCubit.setServerType(type);
Text( },
type.title, child: Card(
style: Theme.of(context).textTheme.titleMedium, child: Padding(
), padding: const EdgeInsets.all(16.0),
const SizedBox(height: 8), child: Column(
Row( crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Icon( Text(
Icons.memory_outlined, type.title,
color: style: Theme.of(context)
Theme.of(context).colorScheme.onSurface, .textTheme
), .titleMedium,
const SizedBox(width: 8),
Text(
'server.core_count'.plural(type.cores),
style:
Theme.of(context).textTheme.bodyMedium,
),
],
),
const SizedBox(height: 8),
Row(
children: [
Icon(
Icons.memory_outlined,
color:
Theme.of(context).colorScheme.onSurface,
),
const SizedBox(width: 8),
Text(
'initializing.choose_server_type_ram'
.tr(args: [type.ram.toString()]),
style:
Theme.of(context).textTheme.bodyMedium,
),
],
),
const SizedBox(height: 8),
Row(
children: [
Icon(
Icons.sd_card_outlined,
color:
Theme.of(context).colorScheme.onSurface,
),
const SizedBox(width: 8),
Text(
'initializing.choose_server_type_storage'
.tr(
args: [type.disk.gibibyte.toString()],
), ),
style: const SizedBox(height: 8),
Theme.of(context).textTheme.bodyMedium, Row(
), children: [
], Icon(
), Icons.memory_outlined,
const SizedBox(height: 8), color: Theme.of(context)
const Divider(height: 8), .colorScheme
const SizedBox(height: 8), .onSurface,
Row( ),
children: [ const SizedBox(width: 8),
Icon( Text(
Icons.payments_outlined, 'server.core_count'
color: .plural(type.cores),
Theme.of(context).colorScheme.onSurface, style: Theme.of(context)
), .textTheme
const SizedBox(width: 8), .bodyMedium,
Text( ),
'initializing.choose_server_type_payment_per_month'
.tr(
args: [
'${type.price.value.toString()} ${type.price.currency}'
], ],
), ),
style: const SizedBox(height: 8),
Theme.of(context).textTheme.bodyLarge, 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 { } else {
return const Center(child: CircularProgressIndicator()); 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:flutter/material.dart';
import 'package:selfprivacy/logic/cubit/forms/setup/recovering/recovery_device_form_cubit.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_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:selfprivacy/utils/route_transitions/basic.dart';
import 'package:cubit_form/cubit_form.dart'; import 'package:cubit_form/cubit_form.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.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:flutter/material.dart';
import 'package:selfprivacy/logic/cubit/forms/setup/recovering/recovery_device_form_cubit.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_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:selfprivacy/ui/components/brand_md/brand_md.dart';
import 'package:cubit_form/cubit_form.dart'; import 'package:cubit_form/cubit_form.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.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/forms/setup/recovering/recovery_device_form_cubit.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
import 'package:selfprivacy/ui/components/brand_button/brand_button.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 { class RecoverByRecoveryKey extends StatelessWidget {
const RecoverByRecoveryKey({super.key}); 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/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_bottom_sheet/brand_bottom_sheet.dart';
import 'package:selfprivacy/ui/components/brand_button/brand_button.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:selfprivacy/ui/components/brand_md/brand_md.dart';
class RecoveryConfirmBackblaze extends StatelessWidget { 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/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_bottom_sheet/brand_bottom_sheet.dart';
import 'package:selfprivacy/ui/components/brand_button/brand_button.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:selfprivacy/ui/components/brand_md/brand_md.dart';
class RecoveryConfirmCloudflare extends StatelessWidget { 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/logic/models/server_basic_info.dart';
import 'package:selfprivacy/ui/components/brand_button/brand_button.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_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 { class RecoveryConfirmServer extends StatefulWidget {
const RecoveryConfirmServer({super.key}); 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/logic/cubit/server_installation/server_installation_cubit.dart';
import 'package:selfprivacy/ui/components/brand_button/brand_button.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_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/ui/pages/setup/recovering/recover_by_old_token.dart';
import 'package:selfprivacy/utils/route_transitions/basic.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/factories/field_cubit_factory.dart';
import 'package:selfprivacy/logic/cubit/forms/setup/recovering/recovery_domain_form_cubit.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_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/root_route.dart';
import 'package:selfprivacy/ui/pages/setup/recovering/recover_by_old_token.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'; 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/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_bottom_sheet/brand_bottom_sheet.dart';
import 'package:selfprivacy/ui/components/brand_button/brand_button.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:cubit_form/cubit_form.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
import 'package:selfprivacy/ui/components/brand_md/brand_md.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( Widget build(final BuildContext context) => InkWell(
onTap: () { onTap: () {
Navigator.of(context).push( Navigator.of(context).push(
materialRoute(UserDetails(login: user.login)), materialRoute(UserDetailsPage(login: user.login)),
); );
}, },
child: Container( child: Container(

View File

@ -1,7 +1,7 @@
part of 'users.dart'; part of 'users.dart';
class UserDetails extends StatelessWidget { class UserDetailsPage extends StatelessWidget {
const UserDetails({ const UserDetailsPage({
required this.login, required this.login,
super.key, 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_button/outlined_button.dart';
import 'package:selfprivacy/ui/components/brand_cards/filled_card.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_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_icons/brand_icons.dart';
import 'package:selfprivacy/ui/components/brand_text/brand_text.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/info_box/info_box.dart';
import 'package:selfprivacy/ui/components/list_tiles/list_tile_on_surface_variant.dart'; import 'package:selfprivacy/ui/components/list_tiles/list_tile_on_surface_variant.dart';
import 'package:selfprivacy/ui/components/not_ready_card/not_ready_card.dart'; import 'package:selfprivacy/ui/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/ui_helpers.dart';
import 'package:selfprivacy/utils/route_transitions/basic.dart'; import 'package:selfprivacy/utils/route_transitions/basic.dart';
@ -106,12 +107,14 @@ class UsersPage extends StatelessWidget {
} }
return Scaffold( return Scaffold(
appBar: PreferredSize( appBar: Breakpoints.small.isActive(context)
preferredSize: const Size.fromHeight(52), ? PreferredSize(
child: BrandHeader( preferredSize: const Size.fromHeight(52),
title: 'basis.users'.tr(), child: BrandHeader(
), title: 'basis.users'.tr(),
), ),
)
: null,
body: child, 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';
}