Merge branch 'master' into remove_snackbar_style_notif

pull/457/head
def 2024-02-08 14:39:30 +02:00
commit 087deede3a
21 changed files with 317 additions and 325 deletions

View File

@ -1,6 +1,9 @@
name: Windows Builder
on: tag
on:
push:
tags:
- '*.*.*'
jobs:
build-windows:
@ -14,7 +17,7 @@ jobs:
# Install Flutter
- uses: subosito/flutter-action@v2
with:
flutter-version: '3.3.10'
flutter-version: '3.16.1'
channel: 'stable'
# Build Windows artifact

View File

@ -1 +0,0 @@

View File

@ -1,8 +1,7 @@
import 'package:graphql/client.dart';
import 'package:intl/intl.dart';
final DateFormat formatter = DateFormat('hh:mm');
/// TODO(misterfourtytwo): add equality override
class Message {
Message({this.text, this.severity = MessageSeverity.normal})
: time = DateTime.now();
@ -13,7 +12,9 @@ class Message {
final String? text;
final DateTime time;
final MessageSeverity severity;
String get timeString => formatter.format(time);
static final DateFormat _formatter = DateFormat('hh:mm');
String get timeString => _formatter.format(time);
}
enum MessageSeverity {

View File

@ -92,13 +92,15 @@ class SelfprivacyApp extends StatelessWidget {
? ThemeMode.dark
: ThemeMode.light,
builder: (final BuildContext context, final Widget? widget) {
Widget error = const Text('...rendering error...');
Widget error =
const Center(child: Text('...rendering error...'));
if (widget is Scaffold || widget is Navigator) {
error = Scaffold(body: Center(child: error));
error = Scaffold(body: error);
}
ErrorWidget.builder =
(final FlutterErrorDetails errorDetails) => error;
return widget!;
return widget ?? error;
},
);
},

View File

@ -14,6 +14,7 @@ class BrandHeader extends StatelessWidget {
@override
Widget build(final BuildContext context) => AppBar(
centerTitle: true,
title: Padding(
padding: const EdgeInsets.only(top: 4.0),
child: Text(title),
@ -25,8 +26,5 @@ class BrandHeader extends StatelessWidget {
onBackButtonPressed ?? () => Navigator.of(context).pop(),
)
: null,
actions: const [
SizedBox.shrink(),
],
);
}

View File

@ -7,8 +7,9 @@ class BrandButton {
final String? text,
final Widget? child,
}) {
assert(text == null || child == null, 'required title or child');
assert(text != null || child != null, 'required title or child');
assert((text ?? child) != null, 'either title or child must not be empty');
assert(text != null || child != null, 'title or child must be provided');
return ConstrainedBox(
constraints: const BoxConstraints(
minHeight: 48,
@ -28,8 +29,9 @@ class BrandButton {
final String? text,
final Widget? child,
}) {
assert(text == null || child == null, 'required title or child');
assert(text != null || child != null, 'required title or child');
assert((text ?? child) != null, 'either title or child must not be empty');
assert(text != null || child != null, 'title or child must be provided');
return ConstrainedBox(
constraints: const BoxConstraints(
minWidth: double.infinity,

View File

@ -0,0 +1,2 @@
export 'brand_button.dart';
export 'sp_brand_button.dart';

View File

@ -0,0 +1,28 @@
import 'package:flutter/material.dart';
class SPBrandButton extends StatelessWidget {
const SPBrandButton({
required this.child,
required this.onPressed,
super.key,
});
SPBrandButton.text({
required final String title,
required this.onPressed,
super.key,
}) : child = Text(title);
final Widget child;
final VoidCallback onPressed;
@override
Widget build(final BuildContext context) => FilledButton(
// TODO(misterfourtytwo): move button styles to theme configuration
style: const ButtonStyle(
minimumSize: MaterialStatePropertyAll(Size.fromHeight(48)),
),
onPressed: onPressed,
child: child,
);
}

View File

@ -1,5 +1,4 @@
import 'package:flutter/material.dart';
import 'package:selfprivacy/logic/cubit/app_settings/app_settings_cubit.dart';
class ProgressBar extends StatefulWidget {
const ProgressBar({
@ -21,41 +20,6 @@ class _ProgressBarState extends State<ProgressBar> {
Widget build(final BuildContext context) {
final double progress =
1 / widget.steps.length * (widget.activeIndex + 0.3);
final bool isDark = context.watch<AppSettingsCubit>().state.isDarkModeOn;
final TextStyle style =
isDark ? progressTextStyleDark : progressTextStyleLight;
final Iterable<Container> allSteps = widget.steps.asMap().map(
(final i, final step) {
final Container value = _stepTitle(index: i, style: style, step: step);
return MapEntry(i, value);
},
).values;
final List<Widget> odd = [];
final List<Widget> even = [];
int i = 0;
for (final Container step in allSteps) {
if (i.isEven) {
even.add(step);
} else {
odd.add(step);
}
i++;
}
odd.insert(
0,
const SizedBox(
width: 10,
),
);
odd.add(
const SizedBox(
width: 20,
),
);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
@ -91,39 +55,4 @@ class _ProgressBarState extends State<ProgressBar> {
],
);
}
Container _stepTitle({
required final int index,
TextStyle? style,
final String? step,
}) {
final bool isActive = index == widget.activeIndex;
style = isActive ? style!.copyWith(fontWeight: FontWeight.w700) : style;
return Container(
padding: const EdgeInsets.only(left: 10),
height: 20,
alignment: Alignment.center,
child: RichText(
textAlign: TextAlign.justify,
text: TextSpan(
style: progressTextStyleLight,
children: [
TextSpan(text: '${index + 1}.', style: style),
TextSpan(text: step, style: style),
],
),
),
);
}
}
const TextStyle progressTextStyleLight = TextStyle(
fontSize: 11,
color: Colors.black,
height: 1.7,
);
final TextStyle progressTextStyleDark = progressTextStyleLight.copyWith(
color: Colors.white,
);

View File

@ -1,8 +1,7 @@
import 'dart:collection';
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/models/message.dart';
import 'package:selfprivacy/ui/components/list_tiles/log_list_tile.dart';
@ -16,25 +15,36 @@ class ConsolePage extends StatefulWidget {
}
class _ConsolePageState extends State<ConsolePage> {
bool paused = false;
@override
void initState() {
getIt.get<ConsoleModel>().addListener(update);
super.initState();
getIt<ConsoleModel>().addListener(update);
}
@override
void dispose() {
getIt<ConsoleModel>().removeListener(update);
super.dispose();
}
bool paused = false;
void update() {
if (!paused) {
setState(() => {});
}
/// listener update could come at any time, like when widget is already
/// unmounted or during frame build, adding as postframe callback ensures
/// that element is marked for rebuild
WidgetsBinding.instance.addPostFrameCallback((final _) {
if (!paused && mounted) {
setState(() => {});
}
});
}
void togglePause() {
paused ^= true;
setState(() {});
}
@override
@ -51,7 +61,7 @@ class _ConsolePageState extends State<ConsolePage> {
icon: Icon(
paused ? Icons.play_arrow_outlined : Icons.pause_outlined,
),
onPressed: () => setState(() => paused = !paused),
onPressed: togglePause,
),
],
),
@ -69,12 +79,12 @@ class _ConsolePageState extends State<ConsolePage> {
reverse: true,
shrinkWrap: true,
children: [
const SizedBox(height: 20),
...UnmodifiableListView(
messages
.map((final message) => LogListItem(message: message))
.toList()
.reversed,
const Gap(20),
...messages.reversed.map(
(final message) => LogListItem(
key: ValueKey(message),
message: message,
),
),
],
);
@ -82,11 +92,10 @@ class _ConsolePageState extends State<ConsolePage> {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text('console_page.waiting'.tr()),
const SizedBox(
height: 16,
),
const Gap(16),
const CircularProgressIndicator(),
],
);

View File

@ -1,8 +1,7 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:selfprivacy/logic/cubit/app_settings/app_settings_cubit.dart';
import 'package:selfprivacy/ui/components/buttons/brand_button.dart';
import 'package:selfprivacy/ui/pages/onboarding/views/views.dart';
import 'package:selfprivacy/ui/router/router.dart';
@RoutePage()
@ -17,152 +16,35 @@ class _OnboardingPageState extends State<OnboardingPage> {
PageController pageController = PageController();
@override
void initState() {
super.initState();
void dispose() {
pageController.dispose();
super.dispose();
}
Future<void> scrollTo(final int targetView) => pageController.animateToPage(
targetView,
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOutCubicEmphasized,
);
@override
Widget build(final BuildContext context) => Scaffold(
body: PageView(
Widget build(final BuildContext context) => Material(
child: PageView(
controller: pageController,
children: [
_withPadding(firstPage()),
_withPadding(secondPage()),
],
),
);
Widget _withPadding(final Widget child) => Padding(
padding: const EdgeInsets.symmetric(
horizontal: 15,
),
child: child,
);
Widget firstPage() => ConstrainedBox(
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height,
),
child: Column(
children: [
Expanded(
child: ListView(
children: [
const SizedBox(height: 30),
Text(
'onboarding.page1_title'.tr(),
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 16),
Text(
'onboarding.page1_text'.tr(),
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 32),
Center(
child: Image.asset(
_fileName(
context: context,
path: 'assets/images/onboarding',
fileExtention: 'png',
fileName: 'onboarding1',
),
),
),
],
),
OnboardingFirstView(
onProceed: () => scrollTo(1),
),
BrandButton.rised(
onPressed: () {
pageController.animateToPage(
1,
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOutCubicEmphasized,
);
},
text: 'basis.next'.tr(),
),
const SizedBox(height: 30),
],
),
);
Widget secondPage() => ConstrainedBox(
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height,
),
child: Column(
children: [
Expanded(
child: ListView(
children: [
const SizedBox(height: 30),
Text(
'onboarding.page2_title'.tr(),
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 16),
Text(
'onboarding.page2_text'.tr(),
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 16),
Text(
'onboarding.page2_server_provider_title'.tr(),
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 16),
Text(
'onboarding.page2_server_provider_text'.tr(),
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 16),
Text(
'onboarding.page2_dns_provider_title'.tr(),
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 16),
Text(
'onboarding.page2_dns_provider_text'.tr(),
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 16),
Text(
'onboarding.page2_backup_provider_title'.tr(),
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 16),
Text(
'onboarding.page2_backup_provider_text'.tr(),
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 16),
],
),
),
BrandButton.rised(
onPressed: () {
OnboardingSecondView(
onProceed: () {
context.read<AppSettingsCubit>().turnOffOnboarding();
context.router.replaceAll([
const RootRoute(),
const InitializingRoute(),
]);
},
text: 'basis.got_it'.tr(),
),
const SizedBox(height: 30),
],
),
);
}
String _fileName({
required final BuildContext context,
required final String path,
required final String fileName,
required final String fileExtention,
}) {
final ThemeData theme = Theme.of(context);
final bool isDark = theme.brightness == Brightness.dark;
return '$path/$fileName${isDark ? '-dark' : '-light'}.$fileExtention';
}

View File

@ -0,0 +1,50 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:selfprivacy/ui/pages/onboarding/views/onboarding_view.dart';
class OnboardingFirstView extends StatelessWidget {
const OnboardingFirstView({
required this.onProceed,
super.key,
});
final VoidCallback onProceed;
String assetName({
required final BuildContext context,
required final String path,
required final String fileName,
required final String fileExtension,
}) {
final String suffix =
Theme.of(context).brightness == Brightness.dark ? '-dark' : '-light';
return '$path/$fileName$suffix.$fileExtension';
}
@override
Widget build(final BuildContext context) => OnboardingView(
onProceed: onProceed,
children: [
Text(
'onboarding.page1_title'.tr(),
style: Theme.of(context).textTheme.headlineSmall,
),
const Gap(15),
Text(
'onboarding.page1_text'.tr(),
style: Theme.of(context).textTheme.bodyMedium,
),
const Gap(30),
Image.asset(
assetName(
context: context,
path: 'assets/images/onboarding',
fileName: 'onboarding1',
fileExtension: 'png',
),
fit: BoxFit.fitWidth,
),
],
);
}

View File

@ -0,0 +1,60 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:selfprivacy/ui/pages/onboarding/views/onboarding_view.dart';
class OnboardingSecondView extends StatelessWidget {
const OnboardingSecondView({
required this.onProceed,
super.key,
});
final VoidCallback onProceed;
@override
Widget build(final BuildContext context) => OnboardingView(
buttonTitle: 'basis.got_it',
onProceed: onProceed,
children: [
Text(
'onboarding.page2_title'.tr(),
style: Theme.of(context).textTheme.headlineSmall,
),
const Gap(16),
Text(
'onboarding.page2_text'.tr(),
style: Theme.of(context).textTheme.bodyMedium,
),
const Gap(16),
Text(
'onboarding.page2_server_provider_title'.tr(),
style: Theme.of(context).textTheme.titleLarge,
),
const Gap(16),
Text(
'onboarding.page2_server_provider_text'.tr(),
style: Theme.of(context).textTheme.bodyMedium,
),
const Gap(16),
Text(
'onboarding.page2_dns_provider_title'.tr(),
style: Theme.of(context).textTheme.titleLarge,
),
const Gap(16),
Text(
'onboarding.page2_dns_provider_text'.tr(),
style: Theme.of(context).textTheme.bodyMedium,
),
const Gap(16),
Text(
'onboarding.page2_backup_provider_title'.tr(),
style: Theme.of(context).textTheme.titleLarge,
),
const Gap(16),
Text(
'onboarding.page2_backup_provider_text'.tr(),
style: Theme.of(context).textTheme.bodyMedium,
),
],
);
}

View File

@ -0,0 +1,53 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:selfprivacy/ui/components/buttons/buttons.dart';
// base widget for onboarding view
class OnboardingView extends StatelessWidget {
const OnboardingView({
required this.onProceed,
required this.children,
this.buttonTitle = 'basis.next',
super.key,
});
/// Proceed button title
final String buttonTitle;
/// Proceed button callback
final VoidCallback onProceed;
/// Current view content
final List<Widget> children;
@override
Widget build(final BuildContext context) => Scaffold(
body: Align(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 480),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
child: ListView(
primary: true,
shrinkWrap: true,
padding: const EdgeInsets.all(15) +
const EdgeInsets.only(top: 15),
children: children,
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 15) +
const EdgeInsets.only(bottom: 30),
child: SPBrandButton.text(
title: buttonTitle.tr(),
onPressed: onProceed,
),
),
],
),
),
),
);
}

View File

@ -0,0 +1,2 @@
export 'onboarding_first_view.dart';
export 'onboarding_second_view.dart';

View File

@ -5,7 +5,6 @@ import 'package:selfprivacy/logic/cubit/app_settings/app_settings_cubit.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
import 'package:selfprivacy/ui/layouts/root_scaffold_with_navigation.dart';
import 'package:selfprivacy/ui/router/root_destinations.dart';
import 'package:selfprivacy/ui/router/router.dart';
@RoutePage()
@ -150,8 +149,3 @@ class MainScreenNavigationDrawer extends StatelessWidget {
);
}
}
class ChangeTab {
ChangeTab(this.onPress);
final ValueChanged<int> onPress;
}

View File

@ -56,5 +56,3 @@ class _TempMessage extends StatelessWidget {
),
);
}
final DateFormat formatter = DateFormat('HH:mm:ss');

View File

@ -170,83 +170,54 @@ class ServiceStatusCard extends StatelessWidget {
@override
Widget build(final BuildContext context) {
late IconData icon;
late String buttonTitle;
switch (status) {
case ServiceStatus.active:
return FilledCard(
child: ListTile(
leading: const Icon(
Icons.check_circle_outline,
size: 24,
),
title: Text('service_page.status.active'.tr()),
),
);
icon = Icons.check_circle_outline;
buttonTitle = 'service_page.status.active';
break;
case ServiceStatus.inactive:
return FilledCard(
tertiary: true,
child: ListTile(
leading: const Icon(
Icons.stop_circle_outlined,
size: 24,
),
title: Text('service_page.status.inactive'.tr()),
),
);
icon = Icons.stop_circle_outlined;
buttonTitle = 'service_page.status.inactive';
break;
case ServiceStatus.failed:
return FilledCard(
error: true,
child: ListTile(
leading: const Icon(
Icons.error_outline,
size: 24,
),
title: Text('service_page.status.failed'.tr()),
),
);
icon = Icons.error_outline;
buttonTitle = 'service_page.status.failed';
break;
case ServiceStatus.off:
return FilledCard(
tertiary: true,
child: ListTile(
leading: const Icon(
Icons.power_settings_new,
size: 24,
),
title: Text('service_page.status.off'.tr()),
),
);
icon = Icons.power_settings_new;
buttonTitle = 'service_page.status.off';
break;
case ServiceStatus.activating:
return FilledCard(
tertiary: true,
child: ListTile(
leading: const Icon(
Icons.restart_alt_outlined,
size: 24,
),
title: Text('service_page.status.activating'.tr()),
),
);
icon = Icons.restart_alt_outlined;
buttonTitle = 'service_page.status.activating';
break;
case ServiceStatus.deactivating:
return FilledCard(
tertiary: true,
child: ListTile(
leading: const Icon(
Icons.restart_alt_outlined,
size: 24,
),
title: Text('service_page.status.deactivating'.tr()),
),
);
icon = Icons.restart_alt_outlined;
buttonTitle = 'service_page.status.deactivating';
break;
case ServiceStatus.reloading:
return FilledCard(
tertiary: true,
child: ListTile(
leading: const Icon(
Icons.restart_alt_outlined,
size: 24,
),
title: Text('service_page.status.reloading'.tr()),
),
);
icon = Icons.restart_alt_outlined;
buttonTitle = 'service_page.status.reloading';
}
return FilledCard(
tertiary: true,
child: ListTile(
leading: Icon(
icon,
size: 24,
),
title: Text(buttonTitle.tr()),
),
);
}
}

View File

@ -7,13 +7,13 @@ class UiHelpers {
static String getDomainName(final ServerInstallationState config) =>
config.isDomainSelected ? config.serverDomain!.domainName : 'example.com';
static final _formatter = NumberFormat()..minimumFractionDigits = 0;
static String formatWithPrecision(
final double value, {
final int fraction = 2,
}) {
final NumberFormat formatter = NumberFormat();
formatter.minimumFractionDigits = 0;
formatter.maximumFractionDigits = fraction;
return formatter.format(value);
_formatter.maximumFractionDigits = fraction;
return _formatter.format(value);
}
}

View File

@ -525,14 +525,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.2.0"
gap:
dependency: "direct main"
description:
name: gap
sha256: f19387d4e32f849394758b91377f9153a1b41d79513ef7668c088c77dbc6955d
url: "https://pub.dev"
source: hosted
version: "3.0.1"
get_it:
dependency: "direct main"
description:
name: get_it
sha256: f79870884de16d689cf9a7d15eedf31ed61d750e813c538a6efb92660fea83c3
sha256: e6017ce7fdeaf218dc51a100344d8cb70134b80e28b760f8bb23c242437bafd7
url: "https://pub.dev"
source: hosted
version: "7.6.4"
version: "7.6.7"
glob:
dependency: transitive
description:
@ -673,10 +681,10 @@ packages:
dependency: "direct main"
description:
name: http
sha256: d4872660c46d929f6b8a9ef4e7a7eff7e49bbf0c4ec3f385ee32df5119175139
sha256: a2bbf9d017fcced29139daa8ed2bba4ece450ab222871df93ca9eec6f80c34ba
url: "https://pub.dev"
source: hosted
version: "1.1.2"
version: "1.2.0"
http_multi_server:
dependency: transitive
description:

View File

@ -27,6 +27,7 @@ dependencies:
flutter_markdown: ^0.6.18+2
flutter_secure_storage: ^9.0.0
flutter_svg: ^2.0.9
gap: ^3.0.1
get_it: ^7.6.4
gql: ^1.0.0
graphql: ^5.1.3