more refactor

This commit is contained in:
Christien Rioux 2024-01-08 21:37:08 -05:00
parent ba4ef05a28
commit b83aa3a64b
39 changed files with 722 additions and 514 deletions

View File

@ -1,3 +1,3 @@
export 'cubit/cubit.dart';
export 'repository/repository.dart';
export 'view/view.dart';
export 'views/views.dart';

View File

@ -16,8 +16,7 @@ class ActiveUserLoginCubit extends Cubit<ActiveUserLoginState> {
}
void _initAccountRepositorySubscription() {
_accountRepositorySubscription =
_accountRepository.changes().listen((change) {
_accountRepositorySubscription = _accountRepository.stream.listen((change) {
switch (change) {
case AccountRepositoryChange.activeUserLogin:
emit(_accountRepository.getActiveUserLogin());

View File

@ -18,8 +18,7 @@ class LocalAccountsCubit extends Cubit<LocalAccountsState> {
}
void _initAccountRepositorySubscription() {
_accountRepositorySubscription =
_accountRepository.changes().listen((change) {
_accountRepositorySubscription = _accountRepository.stream.listen((change) {
switch (change) {
case AccountRepositoryChange.localAccounts:
emit(_accountRepository.getLocalAccounts());

View File

@ -18,8 +18,7 @@ class UserLoginsCubit extends Cubit<UserLoginsState> {
}
void _initAccountRepositorySubscription() {
_accountRepositorySubscription =
_accountRepository.changes().listen((change) {
_accountRepositorySubscription = _accountRepository.stream.listen((change) {
switch (change) {
case AccountRepositoryChange.userLogins:
emit(_accountRepository.getUserLogins());

View File

@ -0,0 +1,22 @@
import 'package:meta/meta.dart';
import 'package:veilid_support/veilid_support.dart';
enum AccountInfoStatus {
noAccount,
accountInvalid,
accountLocked,
accountReady,
}
@immutable
class AccountInfo {
const AccountInfo({
required this.status,
required this.active,
this.accountRecord,
});
final AccountInfoStatus status;
final bool active;
final DHTRecord? accountRecord;
}

View File

@ -0,0 +1,26 @@
import 'package:meta/meta.dart';
import 'package:veilid_support/veilid_support.dart';
import 'local_account/local_account.dart';
import 'user_login/user_login.dart';
@immutable
class ActiveAccountInfo {
const ActiveAccountInfo({
required this.localAccount,
required this.userLogin,
required this.accountRecord,
});
//
KeyPair getConversationWriter() {
final identityKey = localAccount.identityMaster.identityPublicKey;
final identitySecret = userLogin.identitySecret;
return KeyPair(key: identityKey, secret: identitySecret.value);
}
//
final LocalAccount localAccount;
final UserLogin userLogin;
final DHTRecord accountRecord;
}

View File

@ -1,3 +1,5 @@
export 'account_info.dart';
export 'active_account_info.dart';
export 'encryption_key_type.dart';
export 'local_account/local_account.dart';
export 'new_profile_spec.dart';

View File

@ -1,7 +1,10 @@
import 'dart:async';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:veilid_support/veilid_support.dart';
import '../../../../proto/proto.dart' as proto;
import '../../../tools/tools.dart';
import '../../models/models.dart';
import 'active_logins.dart';
@ -12,7 +15,9 @@ enum AccountRepositoryChange { localAccounts, userLogins, activeUserLogin }
class AccountRepository {
AccountRepository._()
: _localAccounts = _initLocalAccounts(),
_activeLogins = _initActiveLogins();
_activeLogins = _initActiveLogins(),
_streamController =
StreamController<AccountRepositoryChange>.broadcast();
static TableDBValue<IList<LocalAccount>> _initLocalAccounts() => TableDBValue(
tableName: 'local_account_manager',
@ -33,6 +38,7 @@ class AccountRepository {
final TableDBValue<IList<LocalAccount>> _localAccounts;
final TableDBValue<ActiveLogins> _activeLogins;
final StreamController<AccountRepositoryChange> _streamController;
//////////////////////////////////////////////////////////////
/// Singleton initialization
@ -42,12 +48,13 @@ class AccountRepository {
Future<void> init() async {
await _localAccounts.load();
await _activeLogins.load();
await _openLoggedInDHTRecords();
}
//////////////////////////////////////////////////////////////
/// Streams
Stream<AccountRepositoryChange> changes() async* {}
Stream<AccountRepositoryChange> get stream => _streamController.stream;
//////////////////////////////////////////////////////////////
/// Selectors
@ -75,6 +82,84 @@ class AccountRepository {
return userLogins[idx];
}
AccountInfo getAccountInfo({required TypedKey accountMasterRecordKey}) {
// Get which local account we want to fetch the profile for
final localAccount =
fetchLocalAccount(accountMasterRecordKey: accountMasterRecordKey);
if (localAccount == null) {
// Local account does not exist
return const AccountInfo(
status: AccountInfoStatus.noAccount, active: false);
}
// See if we've logged into this account or if it is locked
final activeUserLogin = getActiveUserLogin();
final active = activeUserLogin == accountMasterRecordKey;
final login =
fetchUserLogin(accountMasterRecordKey: accountMasterRecordKey);
if (login == null) {
// Account was locked
return AccountInfo(
status: AccountInfoStatus.accountLocked, active: active);
}
// Pull the account DHT key, decode it and return it
final pool = DHTRecordPool.instance;
final accountRecord =
pool.getOpenedRecord(login.accountRecordInfo.accountRecord.recordKey);
if (accountRecord == null) {
// Account could not be read or decrypted from DHT
return AccountInfo(
status: AccountInfoStatus.accountInvalid, active: active);
}
// Got account, decrypted and decoded
return AccountInfo(
status: AccountInfoStatus.accountReady,
active: active,
accountRecord: accountRecord);
}
Future<ActiveAccountInfo?> fetchActiveAccountInfo() async {
// See if we've logged into this account or if it is locked
final activeUserLogin = getActiveUserLogin();
if (activeUserLogin == null) {
// No user logged in
return null;
}
// Get the user login
final userLogin = fetchUserLogin(accountMasterRecordKey: activeUserLogin);
if (userLogin == null) {
// Account was locked
return null;
}
// Get which local account we want to fetch the profile for
final localAccount =
fetchLocalAccount(accountMasterRecordKey: activeUserLogin);
if (localAccount == null) {
// Local account does not exist
return null;
}
// Pull the account DHT key, decode it and return it
final pool = DHTRecordPool.instance;
final accountRecord = pool
.getOpenedRecord(userLogin.accountRecordInfo.accountRecord.recordKey);
if (accountRecord == null) {
return null;
}
// Got account, decrypted and decoded
return ActiveAccountInfo(
localAccount: localAccount,
userLogin: userLogin,
accountRecord: accountRecord,
);
}
//////////////////////////////////////////////////////////////
/// Mutators
@ -86,6 +171,7 @@ class AccountRepository {
.removeAt(oldIndex, removedItem)
.insert(newIndex, removedItem.value!);
await _localAccounts.set(updated);
_streamController.add(AccountRepositoryChange.localAccounts);
}
/// Creates a new master identity, an account associated with the master
@ -172,6 +258,7 @@ class AccountRepository {
final newLocalAccounts = localAccounts.add(localAccount);
await _localAccounts.set(newLocalAccounts);
_streamController.add(AccountRepositoryChange.localAccounts);
// Return local account object
return localAccount;
@ -186,6 +273,7 @@ class AccountRepository {
(la) => la.identityMaster.masterRecordKey == accountMasterRecordKey);
await _localAccounts.set(newLocalAccounts);
_streamController.add(AccountRepositoryChange.localAccounts);
// TO DO: wipe messages
@ -201,6 +289,11 @@ class AccountRepository {
Future<void> switchToAccount(TypedKey? accountMasterRecordKey) async {
final activeLogins = await _activeLogins.get();
if (activeLogins.activeUserLogin == accountMasterRecordKey) {
// Nothing to do
return;
}
if (accountMasterRecordKey != null) {
// Assert the specified record key can be found, will throw if not
final _ = activeLogins.userLogins.firstWhere(
@ -209,6 +302,7 @@ class AccountRepository {
final newActiveLogins =
activeLogins.copyWith(activeUserLogin: accountMasterRecordKey);
await _activeLogins.set(newActiveLogins);
_streamController.add(AccountRepositoryChange.activeUserLogin);
}
Future<bool> _decryptedLogin(
@ -242,6 +336,12 @@ class AccountRepository {
addIfNotFound: true),
activeUserLogin: identityMaster.masterRecordKey);
await _activeLogins.set(newActiveLogins);
_streamController
..add(AccountRepositoryChange.activeUserLogin)
..add(AccountRepositoryChange.userLogins);
// Ensure all logins are opened
await _openLoggedInDHTRecords();
return true;
}
@ -273,11 +373,25 @@ class AccountRepository {
}
Future<void> logout(TypedKey? accountMasterRecordKey) async {
// Resolve which user to log out
final activeLogins = await _activeLogins.get();
final logoutUser = accountMasterRecordKey ?? activeLogins.activeUserLogin;
if (logoutUser == null) {
log.error('missing user in logout: $accountMasterRecordKey');
return;
}
final logoutUserLogin = fetchUserLogin(accountMasterRecordKey: logoutUser);
if (logoutUserLogin != null) {
// Close DHT records for this account
final pool = DHTRecordPool.instance;
final accountRecordKey =
logoutUserLogin.accountRecordInfo.accountRecord.recordKey;
final accountRecord = pool.getOpenedRecord(accountRecordKey);
await accountRecord?.close();
}
// Remove user from active logins list
final newActiveLogins = activeLogins.copyWith(
activeUserLogin: activeLogins.activeUserLogin == logoutUser
? null
@ -285,5 +399,48 @@ class AccountRepository {
userLogins: activeLogins.userLogins
.removeWhere((ul) => ul.accountMasterRecordKey == logoutUser));
await _activeLogins.set(newActiveLogins);
if (activeLogins.activeUserLogin == logoutUser) {
_streamController.add(AccountRepositoryChange.activeUserLogin);
}
_streamController.add(AccountRepositoryChange.userLogins);
}
Future<void> _openLoggedInDHTRecords() async {
final pool = DHTRecordPool.instance;
// For all user logins if they arent open yet
final activeLogins = await _activeLogins.get();
for (final userLogin in activeLogins.userLogins) {
final accountRecordKey =
userLogin.accountRecordInfo.accountRecord.recordKey;
final existingAccountRecord = pool.getOpenedRecord(accountRecordKey);
if (existingAccountRecord != null) {
continue;
}
final localAccount = fetchLocalAccount(
accountMasterRecordKey: userLogin.accountMasterRecordKey);
// Record not yet open, do it
final record = await pool.openOwned(
userLogin.accountRecordInfo.accountRecord,
parent: localAccount!.identityMaster.identityRecordKey);
// Watch the record's only (default) key
await record.watch();
// .scope(
// (accountRec) => accountRec.getProtobuf(proto.Account.fromBuffer));
}
}
Future<void> _closeLoggedInDHTRecords() async {
final pool = DHTRecordPool.instance;
final activeLogins = await _activeLogins.get();
for (final userLogin in activeLogins.userLogins) {
final accountRecordKey =
userLogin.accountRecordInfo.accountRecord.recordKey;
final accountRecord = pool.getOpenedRecord(accountRecordKey);
await accountRecord?.close();
}
}
}

View File

@ -1,11 +1,10 @@
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 '../tools/tools.dart';
import '../../theme/theme.dart';
class ProfileWidget extends ConsumerWidget {
class ProfileWidget extends StatelessWidget {
const ProfileWidget({
required this.name,
this.pronouns,
@ -17,7 +16,7 @@ class ProfileWidget extends ConsumerWidget {
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context, WidgetRef ref) {
Widget build(BuildContext context) {
final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!;
final textTheme = theme.textTheme;

View File

@ -1 +1,2 @@
export 'new_account_page/new_account_page.dart';
export 'profile_widget.dart';

View File

@ -8,24 +8,25 @@ import 'package:form_builder_validators/form_builder_validators.dart';
import 'account_manager/account_manager.dart';
import 'router/router.dart';
import 'settings/settings.dart';
import 'tick.dart';
class VeilidChatApp extends StatelessWidget {
const VeilidChatApp({
required this.themeData,
required this.initialThemeData,
super.key,
});
static const String name = 'VeilidChat';
final ThemeData themeData;
final ThemeData initialThemeData;
@override
Widget build(BuildContext context) {
final localizationDelegate = LocalizedApp.of(context).delegate;
return ThemeProvider(
initTheme: themeData,
initTheme: initialThemeData,
builder: (_, theme) => LocalizationProvider(
state: LocalizationProvider.of(context).state,
child: MultiBlocProvider(
@ -46,6 +47,10 @@ class VeilidChatApp extends StatelessWidget {
create: (context) =>
ActiveUserLoginCubit(AccountRepository.instance),
),
BlocProvider<PreferencesCubit>(
create: (context) =>
PreferencesCubit(PreferencesRepository.instance),
)
],
child: BackgroundTicker(
builder: (context) => MaterialApp.router(
@ -70,6 +75,7 @@ class VeilidChatApp extends StatelessWidget {
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<ThemeData>('themeData', themeData));
properties
.add(DiagnosticsProperty<ThemeData>('themeData', initialThemeData));
}
}

View File

@ -5,9 +5,11 @@ import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_translate/flutter_translate.dart';
import 'package:go_router/go_router.dart';
import 'package:veilid_support/veilid_support.dart';
import '../account_manager/account_manager.dart';
import '../../proto/proto.dart' as proto;
import '../account_manager/account_manager.dart';
import '../account_manager/models/models.dart';
import '../theme/theme.dart';
import '../tools/tools.dart';
import 'main_pager/main_pager.dart';
@ -92,36 +94,48 @@ class HomePageState extends State<HomePage> with TickerProviderStateMixin {
BuildContext context,
IList<LocalAccount> localAccounts,
TypedKey activeUserLogin,
proto.Account account) {
DHTRecord accountRecord) {
final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!;
return Column(children: <Widget>[
Row(children: [
IconButton(
icon: const Icon(Icons.settings),
color: scale.secondaryScale.text,
constraints: const BoxConstraints.expand(height: 64, width: 64),
style: ButtonStyle(
backgroundColor:
MaterialStateProperty.all(scale.secondaryScale.border),
shape: MaterialStateProperty.all(const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(16))))),
tooltip: translate('app_bar.settings_tooltip'),
onPressed: () async {
context.go('/home/settings');
}).paddingLTRB(0, 0, 8, 0),
ProfileWidget(
name: account.profile.name,
pronouns: account.profile.pronouns,
).expanded(),
]).paddingAll(8),
MainPager(
localAccounts: localAccounts,
activeUserLogin: activeUserLogin,
account: account)
.expanded()
]);
return BlocProvider(
create: (context) => DefaultDHTRecordCubit(
record: accountRecord, decodeState: proto.Account.fromBuffer),
child: Column(children: <Widget>[
Row(children: [
IconButton(
icon: const Icon(Icons.settings),
color: scale.secondaryScale.text,
constraints: const BoxConstraints.expand(height: 64, width: 64),
style: ButtonStyle(
backgroundColor:
MaterialStateProperty.all(scale.secondaryScale.border),
shape: MaterialStateProperty.all(
const RoundedRectangleBorder(
borderRadius:
BorderRadius.all(Radius.circular(16))))),
tooltip: translate('app_bar.settings_tooltip'),
onPressed: () async {
context.go('/home/settings');
}).paddingLTRB(0, 0, 8, 0),
context
.watch<DefaultDHTRecordCubit<proto.Account>>()
.state
.builder((context, account) => ProfileWidget(
name: account.profile.name,
pronouns: account.profile.pronouns,
))
.expanded(),
]).paddingAll(8),
context
.watch<DefaultDHTRecordCubit<proto.Account>>()
.state
.builder((context, account) => MainPager(
localAccounts: localAccounts,
activeUserLogin: activeUserLogin,
account: account))
.expanded()
]));
}
Widget buildUserPanel() => Builder(builder: (context) {
@ -133,12 +147,9 @@ class HomePageState extends State<HomePage> with TickerProviderStateMixin {
return waitingPage(context);
}
final accountV = ref.watch(
fetchAccountProvider(accountMasterRecordKey: activeUserLogin));
if (!accountV.hasValue) {
return waitingPage(context);
}
final account = accountV.requireValue;
final account = AccountRepository.instance
.getAccountInfo(accountMasterRecordKey: activeUserLogin);
switch (account.status) {
case AccountInfoStatus.noAccount:
Future.delayed(0.ms, () async {
@ -147,11 +158,10 @@ class HomePageState extends State<HomePage> with TickerProviderStateMixin {
translate('home.missing_account_title'),
translate('home.missing_account_text'));
// Delete account
await ref
.read(localAccountsProvider.notifier)
await AccountRepository.instance
.deleteLocalAccount(activeUserLogin);
// Switch to no active user login
await ref.read(loginsProvider.notifier).switchToAccount(null);
await AccountRepository.instance.switchToAccount(null);
});
return waitingPage(context);
case AccountInfoStatus.accountInvalid:
@ -161,11 +171,10 @@ class HomePageState extends State<HomePage> with TickerProviderStateMixin {
translate('home.invalid_account_title'),
translate('home.invalid_account_text'));
// Delete account
await ref
.read(localAccountsProvider.notifier)
await AccountRepository.instance
.deleteLocalAccount(activeUserLogin);
// Switch to no active user login
await ref.read(loginsProvider.notifier).switchToAccount(null);
await AccountRepository.instance.switchToAccount(null);
});
return waitingPage(context);
case AccountInfoStatus.accountLocked:
@ -176,7 +185,7 @@ class HomePageState extends State<HomePage> with TickerProviderStateMixin {
context,
localAccounts,
activeUserLogin,
account.account!,
account.accountRecord!,
);
}
});

View File

@ -1 +1,7 @@
export 'chat_only.dart';
export 'default_app_bar.dart';
export 'edit_account.dart';
export 'edit_contact.dart';
export 'home.dart';
export 'index.dart';
export 'main_pager/main_pager.dart';

View File

@ -24,7 +24,7 @@ import '../../../../packages/veilid_support/veilid_support.dart';
import 'account.dart';
import 'chats.dart';
class MainPager extends ConsumerStatefulWidget {
class MainPager extends StatefulWidget {
const MainPager(
{required this.localAccounts,
required this.activeUserLogin,

View File

@ -1,139 +0,0 @@
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_form_builder/flutter_form_builder.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_translate/flutter_translate.dart';
import '../../components/default_app_bar.dart';
import '../../components/signal_strength_meter.dart';
import '../../entities/preferences.dart';
import '../providers/window_control.dart';
import '../../tools/tools.dart';
class SettingsPage extends StatefulWidget {
const SettingsPage({super.key});
@override
SettingsPageState createState() => SettingsPageState();
}
class SettingsPageState extends ConsumerState<SettingsPage> {
final _formKey = GlobalKey<FormBuilderState>();
late bool isInAsyncCall = false;
// ThemePreferences? themePreferences;
static const String formFieldTheme = 'theme';
static const String formFieldBrightness = 'brightness';
// static const String formFieldTitle = 'title';
@override
void initState() {
super.initState();
}
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();
}
List<DropdownMenuItem<dynamic>> _getBrightnessDropdownItems() {
const brightnessPrefs = BrightnessPreference.values;
final brightnessNames = {
BrightnessPreference.system: translate('brightness.system'),
BrightnessPreference.light: translate('brightness.light'),
BrightnessPreference.dark: translate('brightness.dark')
};
return brightnessPrefs
.map(
(e) => DropdownMenuItem(value: e, child: Text(brightnessNames[e]!)))
.toList();
}
@override
Widget build(BuildContext context) {
ref.watch(windowControlProvider);
final themeService = ref.watch(themeServiceProvider).valueOrNull;
if (themeService == null) {
return waitingPage(context);
}
final themePreferences = themeService.load();
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: <Widget>[
const SignalStrengthMeterWidget().paddingLTRB(16, 0, 16, 0),
]),
body: FormBuilder(
key: _formKey,
child: ListView(
children: [
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 newPrefs = themePreferences.copyWith(
colorPreference: value as ColorPreference);
await themeService.save(newPrefs);
switcher.changeTheme(theme: themeService.get(newPrefs));
ref.invalidate(themeServiceProvider);
setState(() {});
})),
ThemeSwitcher.withTheme(
builder: (_, switcher, theme) => FormBuilderDropdown(
name: formFieldBrightness,
decoration: InputDecoration(
label:
Text(translate('settings_page.brightness_mode'))),
items: _getBrightnessDropdownItems(),
initialValue: themePreferences.brightnessPreference,
onChanged: (value) async {
final newPrefs = themePreferences.copyWith(
brightnessPreference: value as BrightnessPreference);
await themeService.save(newPrefs);
switcher.changeTheme(theme: themeService.get(newPrefs));
ref.invalidate(themeServiceProvider);
setState(() {});
})),
],
),
).paddingSymmetric(horizontal: 24, vertical: 8),
));
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<bool>('isInAsyncCall', isInAsyncCall));
}
}

View File

@ -9,6 +9,7 @@ import 'package:intl/date_symbol_data_local.dart';
import 'app.dart';
import 'init.dart';
import 'settings/preferences_repository.dart';
import 'theme/theme.dart';
import 'tools/tools.dart';
@ -31,10 +32,11 @@ void main() async {
// Logs
initLoggy();
// Prepare theme
// Prepare preferences from SharedPreferences and theme
WidgetsFlutterBinding.ensureInitialized();
final themeRepository = await ThemeRepository.instance;
final themeData = themeRepository.themeData();
await PreferencesRepository.instance.init();
final initialThemeData =
PreferencesRepository.instance.value.themePreferences.themeData();
// Manage window on desktop platforms
await initializeWindowControl();
@ -45,11 +47,12 @@ void main() async {
await initializeDateFormatting();
// Start up Veilid and Veilid processor in the background
unawaited(initializeVeilid());
unawaited(initializeVeilidChat());
// Run the app
// Hot reloads will only restart this part, not Veilid
runApp(LocalizedApp(delegate, VeilidChatApp(themeData: themeData)));
runApp(LocalizedApp(
delegate, VeilidChatApp(initialThemeData: initialThemeData)));
}, (error, stackTrace) {
log.error('Dart Runtime: {$error}\n{$stackTrace}');
});

View File

@ -1,3 +0,0 @@
export 'local_account.dart';
export 'preferences.dart';
export 'user_login.dart';

View File

@ -1,147 +0,0 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../../entities/local_account.dart';
import '../../entities/user_login.dart';
import '../../proto/proto.dart' as proto;
import '../../../packages/veilid_support/veilid_support.dart';
import '../../local_accounts/local_accounts.dart';
import 'logins.dart';
part 'account.g.dart';
enum AccountInfoStatus {
noAccount,
accountInvalid,
accountLocked,
accountReady,
}
@immutable
class AccountInfo {
const AccountInfo({
required this.status,
required this.active,
this.account,
});
final AccountInfoStatus status;
final bool active;
final proto.Account? account;
}
/// Get an account from the identity key and if it is logged in and we
/// have its secret available, return the account record contents
@riverpod
Future<AccountInfo> fetchAccountInfo(FetchAccountInfoRef ref,
{required TypedKey accountMasterRecordKey}) async {
// Get which local account we want to fetch the profile for
final localAccount = await ref.watch(
fetchLocalAccountProvider(accountMasterRecordKey: accountMasterRecordKey)
.future);
if (localAccount == null) {
// Local account does not exist
return const AccountInfo(
status: AccountInfoStatus.noAccount, active: false);
}
// See if we've logged into this account or if it is locked
final activeUserLogin = await ref.watch(loginsProvider.future
.select((value) async => (await value).activeUserLogin));
final active = activeUserLogin == accountMasterRecordKey;
final login = await ref.watch(
fetchLoginProvider(accountMasterRecordKey: accountMasterRecordKey)
.future);
if (login == null) {
// Account was locked
return AccountInfo(status: AccountInfoStatus.accountLocked, active: active);
}
xxx login should open this key and leave it open, logout should close it
// Pull the account DHT key, decode it and return it
final pool = await DHTRecordPool.instance();
final account = await (await pool.openOwned(
login.accountRecordInfo.accountRecord,
parent: localAccount.identityMaster.identityRecordKey))
.scope((accountRec) => accountRec.getProtobuf(proto.Account.fromBuffer));
if (account == null) {
// Account could not be read or decrypted from DHT
ref.invalidateSelf();
return AccountInfo(
status: AccountInfoStatus.accountInvalid, active: active);
}
// Got account, decrypted and decoded
return AccountInfo(
status: AccountInfoStatus.accountReady, active: active, account: account);
}
@immutable
class ActiveAccountInfo {
const ActiveAccountInfo({
required this.localAccount,
required this.userLogin,
required this.account,
});
//
KeyPair getConversationWriter() {
final identityKey = localAccount.identityMaster.identityPublicKey;
final identitySecret = userLogin.identitySecret;
return KeyPair(key: identityKey, secret: identitySecret.value);
}
//
final LocalAccount localAccount;
final UserLogin userLogin;
final proto.Account account;
}
/// Get the active account info
@riverpod
Future<ActiveAccountInfo?> fetchActiveAccountInfo(
FetchActiveAccountInfoRef ref) async {
// See if we've logged into this account or if it is locked
final activeUserLogin = await ref.watch(loginsProvider.future
.select((value) async => (await value).activeUserLogin));
if (activeUserLogin == null) {
return null;
}
// Get the user login
final userLogin = await ref.watch(
fetchLoginProvider(accountMasterRecordKey: activeUserLogin).future);
if (userLogin == null) {
// Account was locked
return null;
}
// Get which local account we want to fetch the profile for
final localAccount = await ref.watch(
fetchLocalAccountProvider(accountMasterRecordKey: activeUserLogin)
.future);
if (localAccount == null) {
// Local account does not exist
return null;
}
// Pull the account DHT key, decode it and return it
final pool = await DHTRecordPool.instance();
final account = await (await pool.openOwned(
userLogin.accountRecordInfo.accountRecord,
parent: localAccount.identityMaster.identityRecordKey))
.scope((accountRec) => accountRec.getProtobuf(proto.Account.fromBuffer));
if (account == null) {
ref.invalidateSelf();
return null;
}
// Got account, decrypted and decoded
return ActiveAccountInfo(
localAccount: localAccount,
userLogin: userLogin,
account: account,
);
}

View File

@ -7,10 +7,7 @@ import 'package:go_router/go_router.dart';
import '../../../account_manager/account_manager.dart';
import '../../init.dart';
import '../../old_to_refactor/pages/chat_only.dart';
import '../../old_to_refactor/pages/home.dart';
import '../../old_to_refactor/pages/index.dart';
import '../../old_to_refactor/pages/settings.dart';
import '../../layout/layout.dart';
import '../../tools/tools.dart';
import '../../veilid_processor/views/developer.dart';
@ -32,7 +29,7 @@ class RouterCubit extends Cubit<RouterState> {
});
// Subscribe to repository streams
_accountRepositorySubscription =
accountRepository.changes().listen((event) {
accountRepository.stream().listen((event) {
switch (event) {
case AccountRepositoryChange.localAccounts:
emit(state.copyWith(
@ -98,8 +95,8 @@ class RouterCubit extends Cubit<RouterState> {
switch (goRouterState.matchedLocation) {
case '/':
// Wait for veilid to be initialized
if (!eventualVeilid.isCompleted) {
// Wait for initialization to complete
if (!eventualInitialized.isCompleted) {
return null;
}

View File

@ -0,0 +1 @@
export 'preferences.dart';

View File

@ -1,6 +1,8 @@
import 'package:change_case/change_case.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import '../../theme/theme.dart';
part 'preferences.freezed.dart';
part 'preferences.g.dart';
@ -16,6 +18,12 @@ class LockPreference with _$LockPreference {
factory LockPreference.fromJson(dynamic json) =>
_$LockPreferenceFromJson(json as Map<String, dynamic>);
static const LockPreference defaults = LockPreference(
inactivityLockSecs: 0,
lockWhenSwitching: false,
lockWithSystemLock: false,
);
}
// Theme supports multiple translations
@ -25,6 +33,8 @@ enum LanguagePreference {
factory LanguagePreference.fromJson(dynamic j) =>
LanguagePreference.values.byName((j as String).toCamelCase());
String toJson() => name.toPascalCase();
static const LanguagePreference defaults = LanguagePreference.englishUS;
}
// Preferences are stored in a table locally and globally affect all
@ -39,4 +49,9 @@ class Preferences with _$Preferences {
factory Preferences.fromJson(dynamic json) =>
_$PreferencesFromJson(json as Map<String, dynamic>);
static const Preferences defaults = Preferences(
themePreferences: ThemePreferences.defaults,
language: LanguagePreference.defaults,
locking: LockPreference.defaults);
}

View File

@ -226,6 +226,7 @@ abstract class $PreferencesCopyWith<$Res> {
LanguagePreference language,
LockPreference locking});
$ThemePreferencesCopyWith<$Res> get themePreferences;
$LockPreferenceCopyWith<$Res> get locking;
}
@ -242,12 +243,12 @@ class _$PreferencesCopyWithImpl<$Res, $Val extends Preferences>
@pragma('vm:prefer-inline')
@override
$Res call({
Object? themePreferences = freezed,
Object? themePreferences = null,
Object? language = null,
Object? locking = null,
}) {
return _then(_value.copyWith(
themePreferences: freezed == themePreferences
themePreferences: null == themePreferences
? _value.themePreferences
: themePreferences // ignore: cast_nullable_to_non_nullable
as ThemePreferences,
@ -262,6 +263,14 @@ class _$PreferencesCopyWithImpl<$Res, $Val extends Preferences>
) as $Val);
}
@override
@pragma('vm:prefer-inline')
$ThemePreferencesCopyWith<$Res> get themePreferences {
return $ThemePreferencesCopyWith<$Res>(_value.themePreferences, (value) {
return _then(_value.copyWith(themePreferences: value) as $Val);
});
}
@override
@pragma('vm:prefer-inline')
$LockPreferenceCopyWith<$Res> get locking {
@ -284,6 +293,8 @@ abstract class _$$PreferencesImplCopyWith<$Res>
LanguagePreference language,
LockPreference locking});
@override
$ThemePreferencesCopyWith<$Res> get themePreferences;
@override
$LockPreferenceCopyWith<$Res> get locking;
}
@ -299,12 +310,12 @@ class __$$PreferencesImplCopyWithImpl<$Res>
@pragma('vm:prefer-inline')
@override
$Res call({
Object? themePreferences = freezed,
Object? themePreferences = null,
Object? language = null,
Object? locking = null,
}) {
return _then(_$PreferencesImpl(
themePreferences: freezed == themePreferences
themePreferences: null == themePreferences
? _value.themePreferences
: themePreferences // ignore: cast_nullable_to_non_nullable
as ThemePreferences,
@ -348,8 +359,8 @@ class _$PreferencesImpl implements _Preferences {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$PreferencesImpl &&
const DeepCollectionEquality()
.equals(other.themePreferences, themePreferences) &&
(identical(other.themePreferences, themePreferences) ||
other.themePreferences == themePreferences) &&
(identical(other.language, language) ||
other.language == language) &&
(identical(other.locking, locking) || other.locking == locking));
@ -357,8 +368,8 @@ class _$PreferencesImpl implements _Preferences {
@JsonKey(ignore: true)
@override
int get hashCode => Object.hash(runtimeType,
const DeepCollectionEquality().hash(themePreferences), language, locking);
int get hashCode =>
Object.hash(runtimeType, themePreferences, language, locking);
@JsonKey(ignore: true)
@override

View File

@ -0,0 +1,36 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'preferences.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$LockPreferenceImpl _$$LockPreferenceImplFromJson(Map<String, dynamic> json) =>
_$LockPreferenceImpl(
inactivityLockSecs: json['inactivity_lock_secs'] as int,
lockWhenSwitching: json['lock_when_switching'] as bool,
lockWithSystemLock: json['lock_with_system_lock'] as bool,
);
Map<String, dynamic> _$$LockPreferenceImplToJson(
_$LockPreferenceImpl instance) =>
<String, dynamic>{
'inactivity_lock_secs': instance.inactivityLockSecs,
'lock_when_switching': instance.lockWhenSwitching,
'lock_with_system_lock': instance.lockWithSystemLock,
};
_$PreferencesImpl _$$PreferencesImplFromJson(Map<String, dynamic> json) =>
_$PreferencesImpl(
themePreferences: ThemePreferences.fromJson(json['theme_preferences']),
language: LanguagePreference.fromJson(json['language']),
locking: LockPreference.fromJson(json['locking']),
);
Map<String, dynamic> _$$PreferencesImplToJson(_$PreferencesImpl instance) =>
<String, dynamic>{
'theme_preferences': instance.themePreferences.toJson(),
'language': instance.language.toJson(),
'locking': instance.locking.toJson(),
};

View File

@ -0,0 +1,9 @@
import '../tools/tools.dart';
import 'settings.dart';
xxx convert to non-asyncvalue based wrapper since there's always a default here
class PreferencesCubit extends StreamWrapperCubit<Preferences> {
PreferencesCubit(PreferencesRepository repository)
: super(repository.stream, defaultState: repository.value);
}

View File

@ -0,0 +1,34 @@
import 'dart:async';
import 'package:shared_preferences/shared_preferences.dart';
import '../tools/tools.dart';
import 'models/models.dart';
class PreferencesRepository {
PreferencesRepository._();
late final SharedPreferencesValue<Preferences> _data;
Preferences get value => _data.requireValue;
Stream<Preferences> get stream => _data.stream;
//////////////////////////////////////////////////////////////
/// Singleton initialization
static PreferencesRepository instance = PreferencesRepository._();
Future<void> init() async {
final sharedPreferences = await SharedPreferences.getInstance();
_data = SharedPreferencesValue<Preferences>(
sharedPreferences: sharedPreferences,
keyName: 'preferences',
valueFromJson: (obj) =>
obj != null ? Preferences.fromJson(obj) : Preferences.defaults,
valueToJson: (val) => val.toJson());
await _data.get();
}
Future<void> set(Preferences value) => _data.set(value);
Future<Preferences> get() => _data.get();
}

View File

@ -0,0 +1,4 @@
export 'models/models.dart';
export 'preferences_cubit.dart';
export 'preferences_repository.dart';
export 'settings_page.dart';

View File

@ -0,0 +1,129 @@
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_bloc/flutter_bloc.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:flutter_translate/flutter_translate.dart';
import 'package:veilid_support/veilid_support.dart';
import '../layout/default_app_bar.dart';
import '../theme/theme.dart';
import '../veilid_processor/veilid_processor.dart';
import 'preferences_cubit.dart';
import 'preferences_repository.dart';
import 'settings.dart';
class SettingsPage extends StatefulWidget {
const SettingsPage({super.key});
@override
SettingsPageState createState() => SettingsPageState();
}
class SettingsPageState extends State<SettingsPage> {
final _formKey = GlobalKey<FormBuilderState>();
static const String formFieldTheme = 'theme';
static const String formFieldBrightness = 'brightness';
@override
void initState() {
super.initState();
}
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();
}
List<DropdownMenuItem<dynamic>> _getBrightnessDropdownItems() {
const brightnessPrefs = BrightnessPreference.values;
final brightnessNames = {
BrightnessPreference.system: translate('brightness.system'),
BrightnessPreference.light: translate('brightness.light'),
BrightnessPreference.dark: translate('brightness.dark')
};
return brightnessPrefs
.map(
(e) => DropdownMenuItem(value: e, child: Text(brightnessNames[e]!)))
.toList();
}
@override
Widget build(BuildContext context) => BlocBuilder<PreferencesCubit,
AsyncValue<Preferences>>(
builder: (context, state) => 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: <Widget>[
const SignalStrengthMeterWidget().paddingLTRB(16, 0, 16, 0),
]),
body: FormBuilder(
key: _formKey,
child: ListView(
children: [
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 newPrefs = themePreferences.copyWith(
colorPreference: value as ColorPreference);
await themeService.save(newPrefs);
switcher.changeTheme(
theme: themeService.get(newPrefs));
ref.invalidate(themeServiceProvider);
setState(() {});
})),
ThemeSwitcher.withTheme(
builder: (_, switcher, theme) => FormBuilderDropdown(
name: formFieldBrightness,
decoration: InputDecoration(
label: Text(
translate('settings_page.brightness_mode'))),
items: _getBrightnessDropdownItems(),
initialValue: themePreferences.brightnessPreference,
onChanged: (value) async {
final newPrefs = themePreferences.copyWith(
brightnessPreference:
value as BrightnessPreference);
await themeService.save(newPrefs);
switcher.changeTheme(
theme: themeService.get(newPrefs));
ref.invalidate(themeServiceProvider);
setState(() {});
})),
],
),
).paddingSymmetric(horizontal: 24, vertical: 8),
)));
}

View File

@ -1,4 +1,4 @@
export 'radix_generator.dart';
export 'scale_color.dart';
export 'scale_scheme.dart';
export 'theme_preference.dart';
export 'radix_generator.dart';

View File

@ -1,6 +1,10 @@
import 'package:change_case/change_case.dart';
import 'package:flutter/material.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import '../../tools/tools.dart';
import 'radix_generator.dart';
part 'theme_preference.freezed.dart';
part 'theme_preference.g.dart';
@ -49,4 +53,62 @@ class ThemePreferences with _$ThemePreferences {
factory ThemePreferences.fromJson(dynamic json) =>
_$ThemePreferencesFromJson(json as Map<String, dynamic>);
static const ThemePreferences defaults = ThemePreferences(
colorPreference: ColorPreference.vapor,
brightnessPreference: BrightnessPreference.system,
displayScale: 1,
);
}
extension ThemePreferencesExt on ThemePreferences {
/// Get material 'ThemeData' for existinb
ThemeData themeData() {
late final Brightness brightness;
switch (brightnessPreference) {
case BrightnessPreference.system:
if (isPlatformDark) {
brightness = Brightness.dark;
} else {
brightness = Brightness.light;
}
case BrightnessPreference.light:
brightness = Brightness.light;
case BrightnessPreference.dark:
brightness = Brightness.dark;
}
late final ThemeData themeData;
switch (colorPreference) {
// Special cases
case ColorPreference.contrast:
// xxx do contrastGenerator
themeData = radixGenerator(brightness, RadixThemeColor.grim);
// Generate from Radix
case ColorPreference.scarlet:
themeData = radixGenerator(brightness, RadixThemeColor.scarlet);
case ColorPreference.babydoll:
themeData = radixGenerator(brightness, RadixThemeColor.babydoll);
case ColorPreference.vapor:
themeData = radixGenerator(brightness, RadixThemeColor.vapor);
case ColorPreference.gold:
themeData = radixGenerator(brightness, RadixThemeColor.gold);
case ColorPreference.garden:
themeData = radixGenerator(brightness, RadixThemeColor.garden);
case ColorPreference.forest:
themeData = radixGenerator(brightness, RadixThemeColor.forest);
case ColorPreference.arctic:
themeData = radixGenerator(brightness, RadixThemeColor.arctic);
case ColorPreference.lapis:
themeData = radixGenerator(brightness, RadixThemeColor.lapis);
case ColorPreference.eggplant:
themeData = radixGenerator(brightness, RadixThemeColor.eggplant);
case ColorPreference.lime:
themeData = radixGenerator(brightness, RadixThemeColor.lime);
case ColorPreference.grim:
themeData = radixGenerator(brightness, RadixThemeColor.grim);
}
return themeData;
}
}

View File

@ -1,131 +0,0 @@
// ignore_for_file: always_put_required_named_parameters_first
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../models/models.dart';
////////////////////////////////////////////////////////////////////////
class ThemeRepository {
ThemeRepository._({required SharedPreferences sharedPreferences})
: _sharedPreferences = sharedPreferences,
_themePreferences = defaultThemePreferences;
final SharedPreferences _sharedPreferences;
ThemePreferences _themePreferences;
ThemeData? _cachedThemeData;
/// Singleton instance of ThemeRepository
static ThemeRepository? _instance;
static Future<ThemeRepository> get instance async {
if (_instance == null) {
final sharedPreferences = await SharedPreferences.getInstance();
final instance = ThemeRepository._(sharedPreferences: sharedPreferences);
await instance.load();
_instance = instance;
}
return _instance!;
}
static bool get isPlatformDark =>
WidgetsBinding.instance.platformDispatcher.platformBrightness ==
Brightness.dark;
/// Defaults
static ThemePreferences get defaultThemePreferences => const ThemePreferences(
colorPreference: ColorPreference.vapor,
brightnessPreference: BrightnessPreference.system,
displayScale: 1,
);
/// Get theme preferences
ThemePreferences get themePreferences => _themePreferences;
/// Set theme preferences
void setThemePreferences(ThemePreferences themePreferences) {
_themePreferences = themePreferences;
_cachedThemeData = null;
}
/// Load theme preferences from storage
Future<void> load() async {
final themePreferencesJson =
_sharedPreferences.getString('themePreferences');
ThemePreferences? newThemePreferences;
if (themePreferencesJson != null) {
try {
newThemePreferences =
ThemePreferences.fromJson(jsonDecode(themePreferencesJson));
// ignore: avoid_catches_without_on_clauses
} catch (_) {
// ignore
}
}
setThemePreferences(newThemePreferences ?? defaultThemePreferences);
}
/// Save theme preferences to storage
Future<void> save() async {
await _sharedPreferences.setString(
'themePreferences', jsonEncode(_themePreferences.toJson()));
}
/// Get material 'ThemeData' for existinb
ThemeData themeData() {
final cachedThemeData = _cachedThemeData;
if (cachedThemeData != null) {
return cachedThemeData;
}
late final Brightness brightness;
switch (_themePreferences.brightnessPreference) {
case BrightnessPreference.system:
if (isPlatformDark) {
brightness = Brightness.dark;
} else {
brightness = Brightness.light;
}
case BrightnessPreference.light:
brightness = Brightness.light;
case BrightnessPreference.dark:
brightness = Brightness.dark;
}
late final ThemeData themeData;
switch (_themePreferences.colorPreference) {
// Special cases
case ColorPreference.contrast:
// xxx do contrastGenerator
themeData = radixGenerator(brightness, RadixThemeColor.grim);
// Generate from Radix
case ColorPreference.scarlet:
themeData = radixGenerator(brightness, RadixThemeColor.scarlet);
case ColorPreference.babydoll:
themeData = radixGenerator(brightness, RadixThemeColor.babydoll);
case ColorPreference.vapor:
themeData = radixGenerator(brightness, RadixThemeColor.vapor);
case ColorPreference.gold:
themeData = radixGenerator(brightness, RadixThemeColor.gold);
case ColorPreference.garden:
themeData = radixGenerator(brightness, RadixThemeColor.garden);
case ColorPreference.forest:
themeData = radixGenerator(brightness, RadixThemeColor.forest);
case ColorPreference.arctic:
themeData = radixGenerator(brightness, RadixThemeColor.arctic);
case ColorPreference.lapis:
themeData = radixGenerator(brightness, RadixThemeColor.lapis);
case ColorPreference.eggplant:
themeData = radixGenerator(brightness, RadixThemeColor.eggplant);
case ColorPreference.lime:
themeData = radixGenerator(brightness, RadixThemeColor.lime);
case ColorPreference.grim:
themeData = radixGenerator(brightness, RadixThemeColor.grim);
}
_cachedThemeData = themeData;
return themeData;
}
}

View File

@ -1,2 +1 @@
export 'models/models.dart';
export 'repository/theme_repository.dart';

View File

@ -0,0 +1,80 @@
import 'dart:async';
import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
abstract mixin class SharedPreferencesBacked<T> {
SharedPreferences get _sharedPreferences;
String keyName();
T valueFromJson(Object? obj);
Object? valueToJson(T val);
/// Load things from storage
Future<T> load() async {
final valueJsonStr = _sharedPreferences.getString(keyName());
final Object? valueJsonObj =
valueJsonStr != null ? jsonDecode(valueJsonStr) : null;
return valueFromJson(valueJsonObj);
}
/// Store things to storage
Future<T> store(T obj) async {
final valueJsonObj = valueToJson(obj);
if (valueJsonObj == null) {
await _sharedPreferences.remove(keyName());
} else {
await _sharedPreferences.setString(keyName(), jsonEncode(valueJsonObj));
}
return obj;
}
}
class SharedPreferencesValue<T> extends SharedPreferencesBacked<T> {
SharedPreferencesValue({
required SharedPreferences sharedPreferences,
required String keyName,
required T Function(Object? obj) valueFromJson,
required Object? Function(T obj) valueToJson,
}) : _sharedPreferencesInstance = sharedPreferences,
_valueFromJson = valueFromJson,
_valueToJson = valueToJson,
_keyName = keyName,
_streamController = StreamController<T>.broadcast();
@override
SharedPreferences get _sharedPreferences => _sharedPreferencesInstance;
T? get value => _value;
T get requireValue => _value!;
Stream<T> get stream => _streamController.stream;
Future<T> get() async {
final val = _value;
if (val != null) {
return val;
}
final loadedValue = await load();
return _value = loadedValue;
}
Future<void> set(T newVal) async {
_value = await store(newVal);
_streamController.add(newVal);
}
T? _value;
final SharedPreferences _sharedPreferencesInstance;
final String _keyName;
final T Function(Object? obj) _valueFromJson;
final Object? Function(T obj) _valueToJson;
final StreamController<T> _streamController;
//////////////////////////////////////////////////////////////
/// SharedPreferencesBacked
@override
String keyName() => _keyName;
@override
T valueFromJson(Object? obj) => _valueFromJson(obj);
@override
Object? valueToJson(T val) => _valueToJson(val);
}

View File

@ -3,6 +3,7 @@ export 'loggy.dart';
export 'phono_byte.dart';
export 'responsive.dart';
export 'scanner_error_widget.dart';
export 'shared_preferences.dart';
export 'state_logger.dart';
export 'stream_wrapper_cubit.dart';
export 'widget_helpers.dart';

View File

@ -5,6 +5,7 @@ import 'package:flutter_spinkit/flutter_spinkit.dart';
import 'package:flutter_translate/flutter_translate.dart';
import 'package:motion_toast/motion_toast.dart';
import 'package:quickalert/quickalert.dart';
import 'package:veilid_support/veilid_support.dart';
import '../theme/theme.dart';
@ -41,6 +42,24 @@ Widget waitingPage(BuildContext context) => ColoredBox(
color: Theme.of(context).scaffoldBackgroundColor,
child: Center(child: buildProgressIndicator(context)));
Widget errorPage(BuildContext context, Object err, StackTrace? st) =>
ColoredBox(
color: Theme.of(context).colorScheme.error,
child: Center(child: Text(err.toString())));
Widget asyncValueBuilder<T>(
AsyncValue<T> av, Widget Function(BuildContext, T) builder) =>
av.when(
loading: () => const Builder(builder: waitingPage),
error: (e, st) =>
Builder(builder: (context) => errorPage(context, e, st)),
data: (d) => Builder(builder: (context) => builder(context, d)));
extension AsyncValueBuilderExt<T> on AsyncValue<T> {
Widget builder(Widget Function(BuildContext, T) builder) =>
asyncValueBuilder<T>(this, builder);
}
Future<void> showErrorModal(
BuildContext context, String title, String text) async {
await QuickAlert.show(
@ -135,3 +154,7 @@ Future<T?> showStyledDialog<T>(
borderRadius: BorderRadius.circular(12))),
child: child.paddingAll(0)))));
}
bool get isPlatformDark =>
WidgetsBinding.instance.platformDispatcher.platformBrightness ==
Brightness.dark;

View File

@ -27,14 +27,15 @@ Future<void> initializeWindowControl() async {
skipTaskbar: false,
);
await windowManager.waitUntilReadyToShow(windowOptions, () async {
await _doWindowSetup(TitleBarStyle.hidden, OrientationCapability.normal);
await changeWindowSetup(
TitleBarStyle.hidden, OrientationCapability.normal);
await windowManager.show();
await windowManager.focus();
});
}
}
Future<void> _doWindowSetup(TitleBarStyle titleBarStyle,
Future<void> changeWindowSetup(TitleBarStyle titleBarStyle,
OrientationCapability orientationCapability) async {
if (isDesktop) {
await windowManager.setTitleBarStyle(titleBarStyle);
@ -58,8 +59,3 @@ Future<void> _doWindowSetup(TitleBarStyle titleBarStyle,
}
}
}
Future<void> changeWindowSetup(TitleBarStyle titleBarStyle,
OrientationCapability orientationCapability) async {
await _doWindowSetup(titleBarStyle, orientationCapability);
}

View File

@ -262,7 +262,6 @@ class DHTRecord {
eventualUpdateBytes(protobufUpdate(fromBuffer, update), subkey: subkey);
Future<void> watch(
Future<void> Function(VeilidUpdateValueChange update) onUpdate,
{List<ValueSubkeyRange>? subkeys,
Timestamp? expiration,
int? count}) async {

View File

@ -5,8 +5,8 @@ import 'package:bloc/bloc.dart';
import '../../veilid_support.dart';
class DhtRecordCubit<T> extends Cubit<AsyncValue<T>> {
DhtRecordCubit({
class DHTRecordCubit<T> extends Cubit<AsyncValue<T>> {
DHTRecordCubit({
required DHTRecord record,
required Future<T?> Function(DHTRecord) initialStateFunction,
required Future<T?> Function(DHTRecord, List<ValueSubkeyRange>, ValueData)
@ -48,7 +48,7 @@ class DhtRecordCubit<T> extends Cubit<AsyncValue<T>> {
}
// Cubit that watches the default subkey value of a dhtrecord
class DefaultDHTRecordCubit<T> extends DhtRecordCubit<T> {
class DefaultDHTRecordCubit<T> extends DHTRecordCubit<T> {
DefaultDHTRecordCubit({
required super.record,
required T Function(List<int> data) decodeState,

View File

@ -64,10 +64,12 @@ class TableDBValue<T> extends TableDBBacked<T> {
}) : _tableName = tableName,
_valueFromJson = valueFromJson,
_valueToJson = valueToJson,
_tableKeyName = tableKeyName;
_tableKeyName = tableKeyName,
_streamController = StreamController<T>.broadcast();
T? get value => _value;
T get requireValue => _value!;
Stream<T> get stream => _streamController.stream;
Future<T> get() async {
final val = _value;
@ -80,6 +82,7 @@ class TableDBValue<T> extends TableDBBacked<T> {
Future<void> set(T newVal) async {
_value = await store(newVal);
_streamController.add(newVal);
}
T? _value;
@ -87,6 +90,7 @@ class TableDBValue<T> extends TableDBBacked<T> {
final String _tableKeyName;
final T Function(Object? obj) _valueFromJson;
final Object? Function(T obj) _valueToJson;
final StreamController<T> _streamController;
//////////////////////////////////////////////////////////////
/// AsyncTableDBBacked