mirror of
https://gitlab.com/veilid/veilidchat.git
synced 2024-10-01 06:55:46 -04:00
more refactor
This commit is contained in:
parent
ba4ef05a28
commit
b83aa3a64b
@ -1,3 +1,3 @@
|
||||
export 'cubit/cubit.dart';
|
||||
export 'repository/repository.dart';
|
||||
export 'view/view.dart';
|
||||
export 'views/views.dart';
|
||||
|
@ -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());
|
||||
|
@ -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());
|
||||
|
@ -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());
|
||||
|
22
lib/account_manager/models/account_info.dart
Normal file
22
lib/account_manager/models/account_info.dart
Normal 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;
|
||||
}
|
26
lib/account_manager/models/active_account_info.dart
Normal file
26
lib/account_manager/models/active_account_info.dart
Normal 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;
|
||||
}
|
@ -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';
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
@ -1 +1,2 @@
|
||||
export 'new_account_page/new_account_page.dart';
|
||||
export 'profile_widget.dart';
|
14
lib/app.dart
14
lib/app.dart
@ -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));
|
||||
}
|
||||
}
|
||||
|
@ -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!,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
@ -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';
|
||||
|
@ -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,
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
@ -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}');
|
||||
});
|
||||
|
@ -1,3 +0,0 @@
|
||||
export 'local_account.dart';
|
||||
export 'preferences.dart';
|
||||
export 'user_login.dart';
|
@ -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,
|
||||
);
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
|
1
lib/settings/models/models.dart
Normal file
1
lib/settings/models/models.dart
Normal file
@ -0,0 +1 @@
|
||||
export 'preferences.dart';
|
@ -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);
|
||||
}
|
@ -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
|
36
lib/settings/models/preferences.g.dart
Normal file
36
lib/settings/models/preferences.g.dart
Normal 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(),
|
||||
};
|
9
lib/settings/preferences_cubit.dart
Normal file
9
lib/settings/preferences_cubit.dart
Normal 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);
|
||||
}
|
34
lib/settings/preferences_repository.dart
Normal file
34
lib/settings/preferences_repository.dart
Normal 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();
|
||||
}
|
4
lib/settings/settings.dart
Normal file
4
lib/settings/settings.dart
Normal file
@ -0,0 +1,4 @@
|
||||
export 'models/models.dart';
|
||||
export 'preferences_cubit.dart';
|
||||
export 'preferences_repository.dart';
|
||||
export 'settings_page.dart';
|
129
lib/settings/settings_page.dart
Normal file
129
lib/settings/settings_page.dart
Normal 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),
|
||||
)));
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
export 'radix_generator.dart';
|
||||
export 'scale_color.dart';
|
||||
export 'scale_scheme.dart';
|
||||
export 'theme_preference.dart';
|
||||
export 'radix_generator.dart';
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -1,2 +1 @@
|
||||
export 'models/models.dart';
|
||||
export 'repository/theme_repository.dart';
|
||||
|
80
lib/tools/shared_preferences.dart
Normal file
80
lib/tools/shared_preferences.dart
Normal 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);
|
||||
}
|
@ -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';
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user