theme work

This commit is contained in:
Christien Rioux 2023-08-17 18:10:24 -04:00
parent e76e3cf0ba
commit 411705283d
17 changed files with 373 additions and 87 deletions

View File

@ -105,6 +105,21 @@
"groups": "Groups"
},
"themes": {
"vapor": "Vapor"
"vapor": "Vapor",
"scarlet": "Scarlet",
"babydoll": "Babydoll",
"gold": "Gold",
"garden": "Garden",
"forest": "Forest",
"arctic": "Arctic",
"lapis": "Lapis",
"eggplant": "Eggplant",
"lime": "Lime",
"grim": "Grim",
"contrast": "Contrast"
},
"settings_page": {
"titlebar": "Settings",
"color_theme": "Color Theme"
}
}

View File

@ -2,10 +2,11 @@ import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:flutter_translate/flutter_translate.dart';
import 'package:go_router/go_router.dart';
class DefaultAppBar extends AppBar {
DefaultAppBar(BuildContext context,
{required super.title, super.key, Widget? leading, List<Widget>? actions})
DefaultAppBar(
{required super.title, super.key, Widget? leading, super.actions})
: super(
leading: leading ??
Container(
@ -15,17 +16,5 @@ class DefaultAppBar extends AppBar {
shape: BoxShape.circle),
child:
SvgPicture.asset('assets/images/vlogo.svg', height: 32)
.paddingAll(4)),
actions: (actions ?? <Widget>[])
..add(
IconButton(
icon: const Icon(Icons.settings),
tooltip: translate('app_bar.settings_tooltip'),
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
content: Text(
'Accessibility and language options coming soon')));
},
),
));
.paddingAll(4)));
}

View File

@ -2,6 +2,8 @@ import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_translate/flutter_translate.dart';
import 'package:go_router/go_router.dart';
import '../tools/tools.dart';
@ -29,10 +31,18 @@ class ProfileWidget extends ConsumerWidget {
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: BorderSide(color: scale.primaryScale.border))),
child: Column(mainAxisSize: MainAxisSize.min, children: [
Text(name, style: textTheme.headlineSmall).paddingAll(8),
if (title != null && title!.isNotEmpty)
Text(title!, style: textTheme.bodyMedium).paddingLTRB(8, 0, 8, 8),
child: Row(children: [
Column(mainAxisSize: MainAxisSize.min, children: [
Text(name, style: textTheme.headlineSmall).paddingAll(8),
if (title != null && title!.isNotEmpty)
Text(title!, style: textTheme.bodyMedium).paddingLTRB(8, 0, 8, 8),
]).expanded(),
IconButton(
icon: const Icon(Icons.settings),
tooltip: translate('app_bar.settings_tooltip'),
onPressed: () async {
context.go('/home/settings');
})
])).paddingAll(8);
}

View File

@ -0,0 +1,63 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_translate/flutter_translate.dart';
import 'package:signal_strength_indicator/signal_strength_indicator.dart';
import '../providers/connection_state.dart';
import '../tools/tools.dart';
class SignalStrengthMeterWidget extends ConsumerWidget {
const SignalStrengthMeterWidget({super.key});
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context, WidgetRef ref) {
final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!;
const iconSize = 16.0;
final connState = ref.watch(globalConnectionStateProvider).asData?.value;
if (connState == null) {
return const Icon(Icons.signal_cellular_off, size: iconSize);
}
late final double value;
late final Color color;
late final Color inactiveColor;
switch (connState) {
case GlobalConnectionState.detached:
return Icon(Icons.signal_cellular_nodata,
size: iconSize, color: scale.grayScale.text);
case GlobalConnectionState.detaching:
return Icon(Icons.signal_cellular_off,
size: iconSize, color: scale.grayScale.text);
case GlobalConnectionState.attaching:
value = 0;
color = scale.primaryScale.text;
case GlobalConnectionState.attachedWeak:
value = 1;
color = scale.primaryScale.text;
case GlobalConnectionState.attachedStrong:
value = 2;
color = scale.primaryScale.text;
case GlobalConnectionState.attachedGood:
value = 3;
color = scale.primaryScale.text;
case GlobalConnectionState.fullyAttached:
value = 4;
color = scale.primaryScale.text;
case GlobalConnectionState.overAttached:
value = 4;
color = scale.secondaryScale.subtleText;
}
inactiveColor = scale.grayScale.subtleText;
return SignalStrengthIndicator.bars(
value: value,
activeColor: color,
inactiveColor: inactiveColor,
size: iconSize,
barCount: 4,
spacing: 1,
);
}
}

View File

@ -7,7 +7,6 @@ import 'home.dart';
class ChatOnlyPage extends ConsumerStatefulWidget {
const ChatOnlyPage({super.key});
static const path = '/chat';
@override
ChatOnlyPageState createState() => ChatOnlyPageState();

View File

@ -5,7 +5,6 @@ import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:split_view/split_view.dart';
import 'package:signal_strength_indicator/signal_strength_indicator.dart';
import '../../entities/proto.dart' as proto;
import '../components/chat_component.dart';
@ -21,7 +20,6 @@ import 'main_pager/main_pager.dart';
class HomePage extends ConsumerStatefulWidget {
const HomePage({super.key});
static const path = '/home';
@override
HomePageState createState() => HomePageState();

View File

@ -7,7 +7,6 @@ import '../providers/window_control.dart';
class IndexPage extends ConsumerWidget {
const IndexPage({super.key});
static const path = '/';
@override
Widget build(BuildContext context, WidgetRef ref) {

View File

@ -5,8 +5,10 @@ import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_translate/flutter_translate.dart';
import 'package:form_builder_validators/form_builder_validators.dart';
import 'package:go_router/go_router.dart';
import '../components/default_app_bar.dart';
import '../components/signal_strength_meter.dart';
import '../providers/local_accounts.dart';
import '../providers/logins.dart';
import '../providers/window_control.dart';
@ -15,7 +17,6 @@ import '../veilid_support/veilid_support.dart';
class NewAccountPage extends ConsumerStatefulWidget {
const NewAccountPage({super.key});
static const path = '/new_account';
@override
NewAccountPageState createState() => NewAccountPageState();
@ -136,8 +137,17 @@ class NewAccountPageState extends ConsumerState<NewAccountPage> {
return Scaffold(
// resizeToAvoidBottomInset: false,
appBar: DefaultAppBar(context,
title: Text(translate('new_account_page.titlebar'))),
appBar: DefaultAppBar(
title: Text(translate('new_account_page.titlebar')),
actions: [
const SignalStrengthMeterWidget(),
IconButton(
icon: const Icon(Icons.settings),
tooltip: translate('app_bar.settings_tooltip'),
onPressed: () async {
context.go('/new_account/settings');
})
]),
body: _newAccountForm(
context,
onSubmit: (formKey) async {

View File

@ -1,28 +1,174 @@
import 'package:animated_theme_switcher/animated_theme_switcher.dart';
import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_chat_ui/flutter_chat_ui.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_translate/flutter_translate.dart';
import 'package:form_builder_validators/form_builder_validators.dart';
class LoginPage extends ConsumerWidget {
const LoginPage({super.key});
static const path = '/settings';
import '../components/default_app_bar.dart';
import '../components/signal_strength_meter.dart';
import '../entities/preferences.dart';
import '../providers/local_accounts.dart';
import '../providers/logins.dart';
import '../providers/window_control.dart';
import '../tools/tools.dart';
import '../veilid_support/veilid_support.dart';
class SettingsPage extends ConsumerStatefulWidget {
const SettingsPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) => const Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
SettingsPageState createState() => SettingsPageState();
}
class SettingsPageState extends ConsumerState<SettingsPage> {
final _formKey = GlobalKey<FormBuilderState>();
late bool isInAsyncCall = false;
ThemeService? themeService;
ThemePreferences? themePreferences;
static const String formFieldTheme = 'theme';
// static const String formFieldTitle = 'title';
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) async {
await ref.read(windowControlProvider.notifier).changeWindowSetup(
TitleBarStyle.normal, OrientationCapability.normal);
final tsinst = await ThemeService.instance;
setState(() {
themeService = tsinst;
themePreferences = tsinst.load();
});
});
}
List<DropdownMenuItem<dynamic>> _getThemeDropdownItems() {
const colorPrefs = ColorPreference.values;
final colorNames = {
ColorPreference.scarlet: translate('themes.scarlet'),
ColorPreference.vapor: translate('themes.vapor'),
ColorPreference.babydoll: translate('themes.babydoll'),
ColorPreference.gold: translate('themes.gold'),
ColorPreference.garden: translate('themes.garden'),
ColorPreference.forest: translate('themes.forest'),
ColorPreference.arctic: translate('themes.arctic'),
ColorPreference.lapis: translate('themes.lapis'),
ColorPreference.eggplant: translate('themes.eggplant'),
ColorPreference.lime: translate('themes.lime'),
ColorPreference.grim: translate('themes.grim'),
ColorPreference.contrast: translate('themes.contrast')
};
return colorPrefs
.map((e) => DropdownMenuItem(value: e, child: Text(colorNames[e]!)))
.toList();
}
@override
Widget build(BuildContext context) {
ref.watch(windowControlProvider);
final themeService = ref.watch(themeServiceProvider).asData();
return ThemeSwitchingArea(
child: Scaffold(
// resizeToAvoidBottomInset: false,
appBar: DefaultAppBar(
title: Text(translate('settings_page.titlebar')),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => context.pop<void>(),
),
actions: const <Widget>[
SignalStrengthMeterWidget(),
]),
body: FormBuilder(
key: _formKey,
child: ListView(
children: [
Text('Settings Page'),
ThemeSwitcher.withTheme(
builder: (_, switcher, theme) => FormBuilderDropdown(
name: formFieldTheme,
decoration: InputDecoration(
label: Text(translate('settings_page.color_theme'))),
items: _getThemeDropdownItems(),
initialValue: themePreferences?.colorPreference,
onChanged: (value) async {
final tprefs = themePreferences;
if (tprefs != null) {
final newPrefs = tprefs.copyWith(
colorPreference: value as ColorPreference);
final tservice = themeService;
if (tservice != null) {
await tservice.save(newPrefs);
switcher.changeTheme(theme: tservice.get(newPrefs));
}
setState(() {
themePreferences = newPrefs;
});
}
}))
// Text(translate('settings_page.header'))
// .textStyle(context.headlineSmall)
// .paddingSymmetric(vertical: 16),
// FormBuilderTextField(
// autofocus: true,
// name: formFieldName,
// decoration:
// InputDecoration(hintText: translate('account.form_name')),
// maxLength: 64,
// // The validator receives the text that the user has entered.
// validator: FormBuilderValidators.compose([
// FormBuilderValidators.required(),
// ]),
// ),
// FormBuilderTextField(
// name: formFieldTitle,
// maxLength: 64,
// decoration:
// InputDecoration(hintText: translate('account.form_title')),
// ),
// Row(children: [
// const Spacer(),
// Text(translate('new_account_page.instructions'))
// .toCenter()
// .flexible(flex: 6),
// const Spacer(),
// ]).paddingSymmetric(vertical: 4),
// ElevatedButton(
// onPressed: () async {
// ref.watch(authNotifierProvider.notifier).login(
// "myEmail",
// "myPassword",
// );
// if (_formKey.currentState?.saveAndValidate() ?? false) {
// setState(() {
// isInAsyncCall = true;
// });
// try {
// await onSubmit(_formKey);
// } finally {
// if (mounted) {
// setState(() {
// isInAsyncCall = false;
// });
// }
// }
// }
// },
// child: const Text("Login"),
// ),
// child: Text(translate('new_account_page.create')),
// ).paddingSymmetric(vertical: 4).alignAtCenterRight(),
],
),
),
);
).paddingSymmetric(horizontal: 24, vertical: 8),
));
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<bool>('isInAsyncCall', isInAsyncCall));
}
}

View File

@ -2,7 +2,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../tools/tools.dart';
enum ConnectionState {
enum GlobalConnectionState {
detached,
detaching,
attaching,
@ -13,7 +13,7 @@ enum ConnectionState {
overAttached,
}
ExternalStreamState<ConnectionState> globalConnectionState =
ExternalStreamState<ConnectionState>(ConnectionState.detached);
AutoDisposeStreamProvider<ConnectionState> globalConnectionStateProvider =
ExternalStreamState<GlobalConnectionState> globalConnectionState =
ExternalStreamState<GlobalConnectionState>(GlobalConnectionState.detached);
AutoDisposeStreamProvider<GlobalConnectionState> globalConnectionStateProvider =
globalConnectionState.provider();

View File

@ -17,7 +17,7 @@ GoRouter router(RouterRef ref) {
navigatorKey: _key,
refreshListenable: notifier,
debugLogDiagnostics: true,
initialLocation: IndexPage.path,
initialLocation: '/',
routes: notifier.routes,
redirect: notifier.redirect,
);

View File

@ -6,7 +6,7 @@ part of 'router.dart';
// RiverpodGenerator
// **************************************************************************
String _$routerHash() => r'2273f69a347c52bbb53358ac9034ab0ea760ecce';
String _$routerHash() => r'86eecb1955be62ef8e6f6efcec0fa615289cb823';
/// This simple provider caches our GoRouter.
///

View File

@ -6,6 +6,7 @@ import '../pages/chat_only_page.dart';
import '../pages/home.dart';
import '../pages/index.dart';
import '../pages/new_account.dart';
import '../pages/settings.dart';
import '../providers/chat.dart';
import '../providers/local_accounts.dart';
import '../tools/responsive.dart';
@ -47,13 +48,13 @@ class RouterNotifier extends _$RouterNotifier implements Listenable {
// No matter where we are, if there's not
switch (state.location) {
case IndexPage.path:
return hasAnyAccount ? HomePage.path : NewAccountPage.path;
case NewAccountPage.path:
return hasAnyAccount ? HomePage.path : null;
case HomePage.path:
case '/':
return hasAnyAccount ? '/home' : '/new_account';
case '/new_account':
return hasAnyAccount ? '/home' : null;
case '/home':
if (!hasAnyAccount) {
return NewAccountPage.path;
return '/new_account';
}
if (responsiveVisibility(
context: context,
@ -61,13 +62,13 @@ class RouterNotifier extends _$RouterNotifier implements Listenable {
tabletLandscape: false,
desktop: false)) {
if (hasActiveChat) {
return ChatOnlyPage.path;
return '/home/chat';
}
}
return null;
case ChatOnlyPage.path:
case '/home/chat':
if (!hasAnyAccount) {
return NewAccountPage.path;
return '/new_account';
}
if (responsiveVisibility(
context: context,
@ -75,34 +76,49 @@ class RouterNotifier extends _$RouterNotifier implements Listenable {
tabletLandscape: false,
desktop: false)) {
if (!hasActiveChat) {
return HomePage.path;
return '/home';
}
} else {
return HomePage.path;
return '/home';
}
return null;
case '/home/settings':
case '/new_account/settings':
return null;
default:
return hasAnyAccount ? null : NewAccountPage.path;
return hasAnyAccount ? null : '/new_account';
}
}
/// Our application routes
List<GoRoute> get routes => [
GoRoute(
path: IndexPage.path,
path: '/',
builder: (context, state) => const IndexPage(),
),
GoRoute(
path: HomePage.path,
path: '/home',
builder: (context, state) => const HomePage(),
routes: [
GoRoute(
path: 'settings',
builder: (context, state) => const SettingsPage(),
),
GoRoute(
path: 'chat',
builder: (context, state) => const ChatOnlyPage(),
),
],
),
GoRoute(
path: NewAccountPage.path,
path: '/new_account',
builder: (context, state) => const NewAccountPage(),
),
GoRoute(
path: ChatOnlyPage.path,
builder: (context, state) => const ChatOnlyPage(),
routes: [
GoRoute(
path: 'settings',
builder: (context, state) => const SettingsPage(),
),
],
),
];

View File

@ -6,7 +6,7 @@ part of 'router_notifier.dart';
// RiverpodGenerator
// **************************************************************************
String _$routerNotifierHash() => r'6493fee7e11afead973a5bece304b3c94f7fa1c6';
String _$routerNotifierHash() => r'745bae688e8675c3046b95f29cbe0122bac3f189';
/// See also [RouterNotifier].
@ProviderFor(RouterNotifier)

View File

@ -4,11 +4,14 @@ import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_chat_ui/flutter_chat_ui.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../entities/preferences.dart';
import 'radix_generator.dart';
part 'theme_service.g.dart';
class ScaleColor {
ScaleColor({
required this.appBackground,
@ -191,17 +194,29 @@ class ThemeService {
Brightness.dark;
ThemeData get initial {
final themePreferencesJson = prefs.getString('themePreferences');
final themePreferences = themePreferencesJson != null
? ThemePreferences.fromJson(themePreferencesJson)
: const ThemePreferences(
colorPreference: ColorPreference.vapor,
brightnessPreference: BrightnessPreference.system,
displayScale: 1,
);
final themePreferences = load();
return get(themePreferences);
}
ThemePreferences load() {
final themePreferencesJson = prefs.getString('themePreferences');
ThemePreferences? themePreferences;
if (themePreferencesJson != null) {
try {
themePreferences = ThemePreferences.fromJson(themePreferencesJson);
// ignore: avoid_catches_without_on_clauses
} catch (_) {
// ignore
}
}
return themePreferences ??
const ThemePreferences(
colorPreference: ColorPreference.vapor,
brightnessPreference: BrightnessPreference.system,
displayScale: 1,
);
}
Future<void> save(ThemePreferences themePreferences) async {
await prefs.setString(
'themePreferences', jsonEncode(themePreferences.toJson()));
@ -256,3 +271,6 @@ class ThemeService {
return themeData;
}
}
@riverpod
Future<ThemeService> themeService() => ThemeService.instance;

View File

@ -0,0 +1,23 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'theme_service.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$themeServiceHash() => r'a319ae893d3b5cf5718a0c1a21c22bd073f5fd34';
/// See also [themeService].
@ProviderFor(themeService)
final themeServiceProvider = AutoDisposeFutureProvider<ThemeService>.internal(
themeService,
name: r'themeServiceProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$themeServiceHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef ThemeServiceRef = AutoDisposeFutureProviderRef<ThemeService>;
// ignore_for_file: unnecessary_raw_strings, subtype_of_sealed_class, invalid_use_of_internal_member, do_not_use_environment, prefer_const_constructors, public_member_api_docs, avoid_private_typedef_functions

View File

@ -61,42 +61,42 @@ class Processor {
//loggy.info("Attachment: ${updateAttachment.json}");
// Set connection meter and ui state for connection state
var cs = ConnectionState.detached;
var cs = GlobalConnectionState.detached;
var checkPublicInternet = false;
switch (updateAttachment.state) {
case AttachmentState.detached:
cs = ConnectionState.detached;
cs = GlobalConnectionState.detached;
break;
case AttachmentState.detaching:
cs = ConnectionState.detaching;
cs = GlobalConnectionState.detaching;
break;
case AttachmentState.attaching:
cs = ConnectionState.attaching;
cs = GlobalConnectionState.attaching;
break;
case AttachmentState.attachedWeak:
checkPublicInternet = true;
cs = ConnectionState.attachedWeak;
cs = GlobalConnectionState.attachedWeak;
break;
case AttachmentState.attachedGood:
checkPublicInternet = true;
cs = ConnectionState.attachedGood;
cs = GlobalConnectionState.attachedGood;
break;
case AttachmentState.attachedStrong:
checkPublicInternet = true;
cs = ConnectionState.attachedStrong;
cs = GlobalConnectionState.attachedStrong;
break;
case AttachmentState.fullyAttached:
checkPublicInternet = true;
cs = ConnectionState.fullyAttached;
cs = GlobalConnectionState.fullyAttached;
break;
case AttachmentState.overAttached:
checkPublicInternet = true;
cs = ConnectionState.overAttached;
cs = GlobalConnectionState.overAttached;
break;
}
if (checkPublicInternet) {
if (!updateAttachment.publicInternetReady) {
cs = ConnectionState.attaching;
cs = GlobalConnectionState.attaching;
}
}