mirror of
https://gitlab.com/veilid/veilidchat.git
synced 2025-06-08 14:42:39 -04:00
more refactor
This commit is contained in:
parent
ba4ef05a28
commit
b83aa3a64b
39 changed files with 722 additions and 514 deletions
|
@ -1,3 +1,3 @@
|
||||||
export 'cubit/cubit.dart';
|
export 'cubit/cubit.dart';
|
||||||
export 'repository/repository.dart';
|
export 'repository/repository.dart';
|
||||||
export 'view/view.dart';
|
export 'views/views.dart';
|
||||||
|
|
|
@ -16,8 +16,7 @@ class ActiveUserLoginCubit extends Cubit<ActiveUserLoginState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
void _initAccountRepositorySubscription() {
|
void _initAccountRepositorySubscription() {
|
||||||
_accountRepositorySubscription =
|
_accountRepositorySubscription = _accountRepository.stream.listen((change) {
|
||||||
_accountRepository.changes().listen((change) {
|
|
||||||
switch (change) {
|
switch (change) {
|
||||||
case AccountRepositoryChange.activeUserLogin:
|
case AccountRepositoryChange.activeUserLogin:
|
||||||
emit(_accountRepository.getActiveUserLogin());
|
emit(_accountRepository.getActiveUserLogin());
|
||||||
|
|
|
@ -18,8 +18,7 @@ class LocalAccountsCubit extends Cubit<LocalAccountsState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
void _initAccountRepositorySubscription() {
|
void _initAccountRepositorySubscription() {
|
||||||
_accountRepositorySubscription =
|
_accountRepositorySubscription = _accountRepository.stream.listen((change) {
|
||||||
_accountRepository.changes().listen((change) {
|
|
||||||
switch (change) {
|
switch (change) {
|
||||||
case AccountRepositoryChange.localAccounts:
|
case AccountRepositoryChange.localAccounts:
|
||||||
emit(_accountRepository.getLocalAccounts());
|
emit(_accountRepository.getLocalAccounts());
|
||||||
|
|
|
@ -18,8 +18,7 @@ class UserLoginsCubit extends Cubit<UserLoginsState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
void _initAccountRepositorySubscription() {
|
void _initAccountRepositorySubscription() {
|
||||||
_accountRepositorySubscription =
|
_accountRepositorySubscription = _accountRepository.stream.listen((change) {
|
||||||
_accountRepository.changes().listen((change) {
|
|
||||||
switch (change) {
|
switch (change) {
|
||||||
case AccountRepositoryChange.userLogins:
|
case AccountRepositoryChange.userLogins:
|
||||||
emit(_accountRepository.getUserLogins());
|
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 'encryption_key_type.dart';
|
||||||
export 'local_account/local_account.dart';
|
export 'local_account/local_account.dart';
|
||||||
export 'new_profile_spec.dart';
|
export 'new_profile_spec.dart';
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
||||||
import 'package:veilid_support/veilid_support.dart';
|
import 'package:veilid_support/veilid_support.dart';
|
||||||
|
|
||||||
import '../../../../proto/proto.dart' as proto;
|
import '../../../../proto/proto.dart' as proto;
|
||||||
|
import '../../../tools/tools.dart';
|
||||||
import '../../models/models.dart';
|
import '../../models/models.dart';
|
||||||
import 'active_logins.dart';
|
import 'active_logins.dart';
|
||||||
|
|
||||||
|
@ -12,7 +15,9 @@ enum AccountRepositoryChange { localAccounts, userLogins, activeUserLogin }
|
||||||
class AccountRepository {
|
class AccountRepository {
|
||||||
AccountRepository._()
|
AccountRepository._()
|
||||||
: _localAccounts = _initLocalAccounts(),
|
: _localAccounts = _initLocalAccounts(),
|
||||||
_activeLogins = _initActiveLogins();
|
_activeLogins = _initActiveLogins(),
|
||||||
|
_streamController =
|
||||||
|
StreamController<AccountRepositoryChange>.broadcast();
|
||||||
|
|
||||||
static TableDBValue<IList<LocalAccount>> _initLocalAccounts() => TableDBValue(
|
static TableDBValue<IList<LocalAccount>> _initLocalAccounts() => TableDBValue(
|
||||||
tableName: 'local_account_manager',
|
tableName: 'local_account_manager',
|
||||||
|
@ -33,6 +38,7 @@ class AccountRepository {
|
||||||
|
|
||||||
final TableDBValue<IList<LocalAccount>> _localAccounts;
|
final TableDBValue<IList<LocalAccount>> _localAccounts;
|
||||||
final TableDBValue<ActiveLogins> _activeLogins;
|
final TableDBValue<ActiveLogins> _activeLogins;
|
||||||
|
final StreamController<AccountRepositoryChange> _streamController;
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////
|
||||||
/// Singleton initialization
|
/// Singleton initialization
|
||||||
|
@ -42,12 +48,13 @@ class AccountRepository {
|
||||||
Future<void> init() async {
|
Future<void> init() async {
|
||||||
await _localAccounts.load();
|
await _localAccounts.load();
|
||||||
await _activeLogins.load();
|
await _activeLogins.load();
|
||||||
|
await _openLoggedInDHTRecords();
|
||||||
}
|
}
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////
|
||||||
/// Streams
|
/// Streams
|
||||||
|
|
||||||
Stream<AccountRepositoryChange> changes() async* {}
|
Stream<AccountRepositoryChange> get stream => _streamController.stream;
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////
|
||||||
/// Selectors
|
/// Selectors
|
||||||
|
@ -75,6 +82,84 @@ class AccountRepository {
|
||||||
return userLogins[idx];
|
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
|
/// Mutators
|
||||||
|
|
||||||
|
@ -86,6 +171,7 @@ class AccountRepository {
|
||||||
.removeAt(oldIndex, removedItem)
|
.removeAt(oldIndex, removedItem)
|
||||||
.insert(newIndex, removedItem.value!);
|
.insert(newIndex, removedItem.value!);
|
||||||
await _localAccounts.set(updated);
|
await _localAccounts.set(updated);
|
||||||
|
_streamController.add(AccountRepositoryChange.localAccounts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a new master identity, an account associated with the master
|
/// Creates a new master identity, an account associated with the master
|
||||||
|
@ -172,6 +258,7 @@ class AccountRepository {
|
||||||
final newLocalAccounts = localAccounts.add(localAccount);
|
final newLocalAccounts = localAccounts.add(localAccount);
|
||||||
|
|
||||||
await _localAccounts.set(newLocalAccounts);
|
await _localAccounts.set(newLocalAccounts);
|
||||||
|
_streamController.add(AccountRepositoryChange.localAccounts);
|
||||||
|
|
||||||
// Return local account object
|
// Return local account object
|
||||||
return localAccount;
|
return localAccount;
|
||||||
|
@ -186,6 +273,7 @@ class AccountRepository {
|
||||||
(la) => la.identityMaster.masterRecordKey == accountMasterRecordKey);
|
(la) => la.identityMaster.masterRecordKey == accountMasterRecordKey);
|
||||||
|
|
||||||
await _localAccounts.set(newLocalAccounts);
|
await _localAccounts.set(newLocalAccounts);
|
||||||
|
_streamController.add(AccountRepositoryChange.localAccounts);
|
||||||
|
|
||||||
// TO DO: wipe messages
|
// TO DO: wipe messages
|
||||||
|
|
||||||
|
@ -201,6 +289,11 @@ class AccountRepository {
|
||||||
Future<void> switchToAccount(TypedKey? accountMasterRecordKey) async {
|
Future<void> switchToAccount(TypedKey? accountMasterRecordKey) async {
|
||||||
final activeLogins = await _activeLogins.get();
|
final activeLogins = await _activeLogins.get();
|
||||||
|
|
||||||
|
if (activeLogins.activeUserLogin == accountMasterRecordKey) {
|
||||||
|
// Nothing to do
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (accountMasterRecordKey != null) {
|
if (accountMasterRecordKey != null) {
|
||||||
// Assert the specified record key can be found, will throw if not
|
// Assert the specified record key can be found, will throw if not
|
||||||
final _ = activeLogins.userLogins.firstWhere(
|
final _ = activeLogins.userLogins.firstWhere(
|
||||||
|
@ -209,6 +302,7 @@ class AccountRepository {
|
||||||
final newActiveLogins =
|
final newActiveLogins =
|
||||||
activeLogins.copyWith(activeUserLogin: accountMasterRecordKey);
|
activeLogins.copyWith(activeUserLogin: accountMasterRecordKey);
|
||||||
await _activeLogins.set(newActiveLogins);
|
await _activeLogins.set(newActiveLogins);
|
||||||
|
_streamController.add(AccountRepositoryChange.activeUserLogin);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> _decryptedLogin(
|
Future<bool> _decryptedLogin(
|
||||||
|
@ -242,6 +336,12 @@ class AccountRepository {
|
||||||
addIfNotFound: true),
|
addIfNotFound: true),
|
||||||
activeUserLogin: identityMaster.masterRecordKey);
|
activeUserLogin: identityMaster.masterRecordKey);
|
||||||
await _activeLogins.set(newActiveLogins);
|
await _activeLogins.set(newActiveLogins);
|
||||||
|
_streamController
|
||||||
|
..add(AccountRepositoryChange.activeUserLogin)
|
||||||
|
..add(AccountRepositoryChange.userLogins);
|
||||||
|
|
||||||
|
// Ensure all logins are opened
|
||||||
|
await _openLoggedInDHTRecords();
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -273,11 +373,25 @@ class AccountRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> logout(TypedKey? accountMasterRecordKey) async {
|
Future<void> logout(TypedKey? accountMasterRecordKey) async {
|
||||||
|
// Resolve which user to log out
|
||||||
final activeLogins = await _activeLogins.get();
|
final activeLogins = await _activeLogins.get();
|
||||||
final logoutUser = accountMasterRecordKey ?? activeLogins.activeUserLogin;
|
final logoutUser = accountMasterRecordKey ?? activeLogins.activeUserLogin;
|
||||||
if (logoutUser == null) {
|
if (logoutUser == null) {
|
||||||
|
log.error('missing user in logout: $accountMasterRecordKey');
|
||||||
return;
|
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(
|
final newActiveLogins = activeLogins.copyWith(
|
||||||
activeUserLogin: activeLogins.activeUserLogin == logoutUser
|
activeUserLogin: activeLogins.activeUserLogin == logoutUser
|
||||||
? null
|
? null
|
||||||
|
@ -285,5 +399,48 @@ class AccountRepository {
|
||||||
userLogins: activeLogins.userLogins
|
userLogins: activeLogins.userLogins
|
||||||
.removeWhere((ul) => ul.accountMasterRecordKey == logoutUser));
|
.removeWhere((ul) => ul.accountMasterRecordKey == logoutUser));
|
||||||
await _activeLogins.set(newActiveLogins);
|
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:awesome_extensions/awesome_extensions.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.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({
|
const ProfileWidget({
|
||||||
required this.name,
|
required this.name,
|
||||||
this.pronouns,
|
this.pronouns,
|
||||||
|
@ -17,7 +16,7 @@ class ProfileWidget extends ConsumerWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
// ignore: prefer_expression_function_bodies
|
// ignore: prefer_expression_function_bodies
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final scale = theme.extension<ScaleScheme>()!;
|
final scale = theme.extension<ScaleScheme>()!;
|
||||||
final textTheme = theme.textTheme;
|
final textTheme = theme.textTheme;
|
|
@ -1 +1,2 @@
|
||||||
export 'new_account_page/new_account_page.dart';
|
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 'account_manager/account_manager.dart';
|
||||||
import 'router/router.dart';
|
import 'router/router.dart';
|
||||||
|
import 'settings/settings.dart';
|
||||||
import 'tick.dart';
|
import 'tick.dart';
|
||||||
|
|
||||||
class VeilidChatApp extends StatelessWidget {
|
class VeilidChatApp extends StatelessWidget {
|
||||||
const VeilidChatApp({
|
const VeilidChatApp({
|
||||||
required this.themeData,
|
required this.initialThemeData,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
static const String name = 'VeilidChat';
|
static const String name = 'VeilidChat';
|
||||||
|
|
||||||
final ThemeData themeData;
|
final ThemeData initialThemeData;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final localizationDelegate = LocalizedApp.of(context).delegate;
|
final localizationDelegate = LocalizedApp.of(context).delegate;
|
||||||
|
|
||||||
return ThemeProvider(
|
return ThemeProvider(
|
||||||
initTheme: themeData,
|
initTheme: initialThemeData,
|
||||||
builder: (_, theme) => LocalizationProvider(
|
builder: (_, theme) => LocalizationProvider(
|
||||||
state: LocalizationProvider.of(context).state,
|
state: LocalizationProvider.of(context).state,
|
||||||
child: MultiBlocProvider(
|
child: MultiBlocProvider(
|
||||||
|
@ -46,6 +47,10 @@ class VeilidChatApp extends StatelessWidget {
|
||||||
create: (context) =>
|
create: (context) =>
|
||||||
ActiveUserLoginCubit(AccountRepository.instance),
|
ActiveUserLoginCubit(AccountRepository.instance),
|
||||||
),
|
),
|
||||||
|
BlocProvider<PreferencesCubit>(
|
||||||
|
create: (context) =>
|
||||||
|
PreferencesCubit(PreferencesRepository.instance),
|
||||||
|
)
|
||||||
],
|
],
|
||||||
child: BackgroundTicker(
|
child: BackgroundTicker(
|
||||||
builder: (context) => MaterialApp.router(
|
builder: (context) => MaterialApp.router(
|
||||||
|
@ -70,6 +75,7 @@ class VeilidChatApp extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||||
super.debugFillProperties(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_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_translate/flutter_translate.dart';
|
import 'package:flutter_translate/flutter_translate.dart';
|
||||||
import 'package:go_router/go_router.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 '../../proto/proto.dart' as proto;
|
||||||
|
import '../account_manager/account_manager.dart';
|
||||||
|
import '../account_manager/models/models.dart';
|
||||||
import '../theme/theme.dart';
|
import '../theme/theme.dart';
|
||||||
import '../tools/tools.dart';
|
import '../tools/tools.dart';
|
||||||
import 'main_pager/main_pager.dart';
|
import 'main_pager/main_pager.dart';
|
||||||
|
@ -92,36 +94,48 @@ class HomePageState extends State<HomePage> with TickerProviderStateMixin {
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
IList<LocalAccount> localAccounts,
|
IList<LocalAccount> localAccounts,
|
||||||
TypedKey activeUserLogin,
|
TypedKey activeUserLogin,
|
||||||
proto.Account account) {
|
DHTRecord accountRecord) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final scale = theme.extension<ScaleScheme>()!;
|
final scale = theme.extension<ScaleScheme>()!;
|
||||||
|
|
||||||
return Column(children: <Widget>[
|
return BlocProvider(
|
||||||
Row(children: [
|
create: (context) => DefaultDHTRecordCubit(
|
||||||
IconButton(
|
record: accountRecord, decodeState: proto.Account.fromBuffer),
|
||||||
icon: const Icon(Icons.settings),
|
child: Column(children: <Widget>[
|
||||||
color: scale.secondaryScale.text,
|
Row(children: [
|
||||||
constraints: const BoxConstraints.expand(height: 64, width: 64),
|
IconButton(
|
||||||
style: ButtonStyle(
|
icon: const Icon(Icons.settings),
|
||||||
backgroundColor:
|
color: scale.secondaryScale.text,
|
||||||
MaterialStateProperty.all(scale.secondaryScale.border),
|
constraints: const BoxConstraints.expand(height: 64, width: 64),
|
||||||
shape: MaterialStateProperty.all(const RoundedRectangleBorder(
|
style: ButtonStyle(
|
||||||
borderRadius: BorderRadius.all(Radius.circular(16))))),
|
backgroundColor:
|
||||||
tooltip: translate('app_bar.settings_tooltip'),
|
MaterialStateProperty.all(scale.secondaryScale.border),
|
||||||
onPressed: () async {
|
shape: MaterialStateProperty.all(
|
||||||
context.go('/home/settings');
|
const RoundedRectangleBorder(
|
||||||
}).paddingLTRB(0, 0, 8, 0),
|
borderRadius:
|
||||||
ProfileWidget(
|
BorderRadius.all(Radius.circular(16))))),
|
||||||
name: account.profile.name,
|
tooltip: translate('app_bar.settings_tooltip'),
|
||||||
pronouns: account.profile.pronouns,
|
onPressed: () async {
|
||||||
).expanded(),
|
context.go('/home/settings');
|
||||||
]).paddingAll(8),
|
}).paddingLTRB(0, 0, 8, 0),
|
||||||
MainPager(
|
context
|
||||||
localAccounts: localAccounts,
|
.watch<DefaultDHTRecordCubit<proto.Account>>()
|
||||||
activeUserLogin: activeUserLogin,
|
.state
|
||||||
account: account)
|
.builder((context, account) => ProfileWidget(
|
||||||
.expanded()
|
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) {
|
Widget buildUserPanel() => Builder(builder: (context) {
|
||||||
|
@ -133,12 +147,9 @@ class HomePageState extends State<HomePage> with TickerProviderStateMixin {
|
||||||
return waitingPage(context);
|
return waitingPage(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
final accountV = ref.watch(
|
final account = AccountRepository.instance
|
||||||
fetchAccountProvider(accountMasterRecordKey: activeUserLogin));
|
.getAccountInfo(accountMasterRecordKey: activeUserLogin);
|
||||||
if (!accountV.hasValue) {
|
|
||||||
return waitingPage(context);
|
|
||||||
}
|
|
||||||
final account = accountV.requireValue;
|
|
||||||
switch (account.status) {
|
switch (account.status) {
|
||||||
case AccountInfoStatus.noAccount:
|
case AccountInfoStatus.noAccount:
|
||||||
Future.delayed(0.ms, () async {
|
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_title'),
|
||||||
translate('home.missing_account_text'));
|
translate('home.missing_account_text'));
|
||||||
// Delete account
|
// Delete account
|
||||||
await ref
|
await AccountRepository.instance
|
||||||
.read(localAccountsProvider.notifier)
|
|
||||||
.deleteLocalAccount(activeUserLogin);
|
.deleteLocalAccount(activeUserLogin);
|
||||||
// Switch to no active user login
|
// Switch to no active user login
|
||||||
await ref.read(loginsProvider.notifier).switchToAccount(null);
|
await AccountRepository.instance.switchToAccount(null);
|
||||||
});
|
});
|
||||||
return waitingPage(context);
|
return waitingPage(context);
|
||||||
case AccountInfoStatus.accountInvalid:
|
case AccountInfoStatus.accountInvalid:
|
||||||
|
@ -161,11 +171,10 @@ class HomePageState extends State<HomePage> with TickerProviderStateMixin {
|
||||||
translate('home.invalid_account_title'),
|
translate('home.invalid_account_title'),
|
||||||
translate('home.invalid_account_text'));
|
translate('home.invalid_account_text'));
|
||||||
// Delete account
|
// Delete account
|
||||||
await ref
|
await AccountRepository.instance
|
||||||
.read(localAccountsProvider.notifier)
|
|
||||||
.deleteLocalAccount(activeUserLogin);
|
.deleteLocalAccount(activeUserLogin);
|
||||||
// Switch to no active user login
|
// Switch to no active user login
|
||||||
await ref.read(loginsProvider.notifier).switchToAccount(null);
|
await AccountRepository.instance.switchToAccount(null);
|
||||||
});
|
});
|
||||||
return waitingPage(context);
|
return waitingPage(context);
|
||||||
case AccountInfoStatus.accountLocked:
|
case AccountInfoStatus.accountLocked:
|
||||||
|
@ -176,7 +185,7 @@ class HomePageState extends State<HomePage> with TickerProviderStateMixin {
|
||||||
context,
|
context,
|
||||||
localAccounts,
|
localAccounts,
|
||||||
activeUserLogin,
|
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 'account.dart';
|
||||||
import 'chats.dart';
|
import 'chats.dart';
|
||||||
|
|
||||||
class MainPager extends ConsumerStatefulWidget {
|
class MainPager extends StatefulWidget {
|
||||||
const MainPager(
|
const MainPager(
|
||||||
{required this.localAccounts,
|
{required this.localAccounts,
|
||||||
required this.activeUserLogin,
|
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 'app.dart';
|
||||||
import 'init.dart';
|
import 'init.dart';
|
||||||
|
import 'settings/preferences_repository.dart';
|
||||||
import 'theme/theme.dart';
|
import 'theme/theme.dart';
|
||||||
import 'tools/tools.dart';
|
import 'tools/tools.dart';
|
||||||
|
|
||||||
|
@ -31,10 +32,11 @@ void main() async {
|
||||||
// Logs
|
// Logs
|
||||||
initLoggy();
|
initLoggy();
|
||||||
|
|
||||||
// Prepare theme
|
// Prepare preferences from SharedPreferences and theme
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
final themeRepository = await ThemeRepository.instance;
|
await PreferencesRepository.instance.init();
|
||||||
final themeData = themeRepository.themeData();
|
final initialThemeData =
|
||||||
|
PreferencesRepository.instance.value.themePreferences.themeData();
|
||||||
|
|
||||||
// Manage window on desktop platforms
|
// Manage window on desktop platforms
|
||||||
await initializeWindowControl();
|
await initializeWindowControl();
|
||||||
|
@ -45,11 +47,12 @@ void main() async {
|
||||||
await initializeDateFormatting();
|
await initializeDateFormatting();
|
||||||
|
|
||||||
// Start up Veilid and Veilid processor in the background
|
// Start up Veilid and Veilid processor in the background
|
||||||
unawaited(initializeVeilid());
|
unawaited(initializeVeilidChat());
|
||||||
|
|
||||||
// Run the app
|
// Run the app
|
||||||
// Hot reloads will only restart this part, not Veilid
|
// Hot reloads will only restart this part, not Veilid
|
||||||
runApp(LocalizedApp(delegate, VeilidChatApp(themeData: themeData)));
|
runApp(LocalizedApp(
|
||||||
|
delegate, VeilidChatApp(initialThemeData: initialThemeData)));
|
||||||
}, (error, stackTrace) {
|
}, (error, stackTrace) {
|
||||||
log.error('Dart Runtime: {$error}\n{$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 '../../../account_manager/account_manager.dart';
|
||||||
import '../../init.dart';
|
import '../../init.dart';
|
||||||
import '../../old_to_refactor/pages/chat_only.dart';
|
import '../../layout/layout.dart';
|
||||||
import '../../old_to_refactor/pages/home.dart';
|
|
||||||
import '../../old_to_refactor/pages/index.dart';
|
|
||||||
import '../../old_to_refactor/pages/settings.dart';
|
|
||||||
import '../../tools/tools.dart';
|
import '../../tools/tools.dart';
|
||||||
import '../../veilid_processor/views/developer.dart';
|
import '../../veilid_processor/views/developer.dart';
|
||||||
|
|
||||||
|
@ -32,7 +29,7 @@ class RouterCubit extends Cubit<RouterState> {
|
||||||
});
|
});
|
||||||
// Subscribe to repository streams
|
// Subscribe to repository streams
|
||||||
_accountRepositorySubscription =
|
_accountRepositorySubscription =
|
||||||
accountRepository.changes().listen((event) {
|
accountRepository.stream().listen((event) {
|
||||||
switch (event) {
|
switch (event) {
|
||||||
case AccountRepositoryChange.localAccounts:
|
case AccountRepositoryChange.localAccounts:
|
||||||
emit(state.copyWith(
|
emit(state.copyWith(
|
||||||
|
@ -98,8 +95,8 @@ class RouterCubit extends Cubit<RouterState> {
|
||||||
switch (goRouterState.matchedLocation) {
|
switch (goRouterState.matchedLocation) {
|
||||||
case '/':
|
case '/':
|
||||||
|
|
||||||
// Wait for veilid to be initialized
|
// Wait for initialization to complete
|
||||||
if (!eventualVeilid.isCompleted) {
|
if (!eventualInitialized.isCompleted) {
|
||||||
return null;
|
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:change_case/change_case.dart';
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
|
import '../../theme/theme.dart';
|
||||||
|
|
||||||
part 'preferences.freezed.dart';
|
part 'preferences.freezed.dart';
|
||||||
part 'preferences.g.dart';
|
part 'preferences.g.dart';
|
||||||
|
|
||||||
|
@ -16,6 +18,12 @@ class LockPreference with _$LockPreference {
|
||||||
|
|
||||||
factory LockPreference.fromJson(dynamic json) =>
|
factory LockPreference.fromJson(dynamic json) =>
|
||||||
_$LockPreferenceFromJson(json as Map<String, dynamic>);
|
_$LockPreferenceFromJson(json as Map<String, dynamic>);
|
||||||
|
|
||||||
|
static const LockPreference defaults = LockPreference(
|
||||||
|
inactivityLockSecs: 0,
|
||||||
|
lockWhenSwitching: false,
|
||||||
|
lockWithSystemLock: false,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Theme supports multiple translations
|
// Theme supports multiple translations
|
||||||
|
@ -25,6 +33,8 @@ enum LanguagePreference {
|
||||||
factory LanguagePreference.fromJson(dynamic j) =>
|
factory LanguagePreference.fromJson(dynamic j) =>
|
||||||
LanguagePreference.values.byName((j as String).toCamelCase());
|
LanguagePreference.values.byName((j as String).toCamelCase());
|
||||||
String toJson() => name.toPascalCase();
|
String toJson() => name.toPascalCase();
|
||||||
|
|
||||||
|
static const LanguagePreference defaults = LanguagePreference.englishUS;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Preferences are stored in a table locally and globally affect all
|
// Preferences are stored in a table locally and globally affect all
|
||||||
|
@ -39,4 +49,9 @@ class Preferences with _$Preferences {
|
||||||
|
|
||||||
factory Preferences.fromJson(dynamic json) =>
|
factory Preferences.fromJson(dynamic json) =>
|
||||||
_$PreferencesFromJson(json as Map<String, dynamic>);
|
_$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,
|
LanguagePreference language,
|
||||||
LockPreference locking});
|
LockPreference locking});
|
||||||
|
|
||||||
|
$ThemePreferencesCopyWith<$Res> get themePreferences;
|
||||||
$LockPreferenceCopyWith<$Res> get locking;
|
$LockPreferenceCopyWith<$Res> get locking;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -242,12 +243,12 @@ class _$PreferencesCopyWithImpl<$Res, $Val extends Preferences>
|
||||||
@pragma('vm:prefer-inline')
|
@pragma('vm:prefer-inline')
|
||||||
@override
|
@override
|
||||||
$Res call({
|
$Res call({
|
||||||
Object? themePreferences = freezed,
|
Object? themePreferences = null,
|
||||||
Object? language = null,
|
Object? language = null,
|
||||||
Object? locking = null,
|
Object? locking = null,
|
||||||
}) {
|
}) {
|
||||||
return _then(_value.copyWith(
|
return _then(_value.copyWith(
|
||||||
themePreferences: freezed == themePreferences
|
themePreferences: null == themePreferences
|
||||||
? _value.themePreferences
|
? _value.themePreferences
|
||||||
: themePreferences // ignore: cast_nullable_to_non_nullable
|
: themePreferences // ignore: cast_nullable_to_non_nullable
|
||||||
as ThemePreferences,
|
as ThemePreferences,
|
||||||
|
@ -262,6 +263,14 @@ class _$PreferencesCopyWithImpl<$Res, $Val extends Preferences>
|
||||||
) as $Val);
|
) 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
|
@override
|
||||||
@pragma('vm:prefer-inline')
|
@pragma('vm:prefer-inline')
|
||||||
$LockPreferenceCopyWith<$Res> get locking {
|
$LockPreferenceCopyWith<$Res> get locking {
|
||||||
|
@ -284,6 +293,8 @@ abstract class _$$PreferencesImplCopyWith<$Res>
|
||||||
LanguagePreference language,
|
LanguagePreference language,
|
||||||
LockPreference locking});
|
LockPreference locking});
|
||||||
|
|
||||||
|
@override
|
||||||
|
$ThemePreferencesCopyWith<$Res> get themePreferences;
|
||||||
@override
|
@override
|
||||||
$LockPreferenceCopyWith<$Res> get locking;
|
$LockPreferenceCopyWith<$Res> get locking;
|
||||||
}
|
}
|
||||||
|
@ -299,12 +310,12 @@ class __$$PreferencesImplCopyWithImpl<$Res>
|
||||||
@pragma('vm:prefer-inline')
|
@pragma('vm:prefer-inline')
|
||||||
@override
|
@override
|
||||||
$Res call({
|
$Res call({
|
||||||
Object? themePreferences = freezed,
|
Object? themePreferences = null,
|
||||||
Object? language = null,
|
Object? language = null,
|
||||||
Object? locking = null,
|
Object? locking = null,
|
||||||
}) {
|
}) {
|
||||||
return _then(_$PreferencesImpl(
|
return _then(_$PreferencesImpl(
|
||||||
themePreferences: freezed == themePreferences
|
themePreferences: null == themePreferences
|
||||||
? _value.themePreferences
|
? _value.themePreferences
|
||||||
: themePreferences // ignore: cast_nullable_to_non_nullable
|
: themePreferences // ignore: cast_nullable_to_non_nullable
|
||||||
as ThemePreferences,
|
as ThemePreferences,
|
||||||
|
@ -348,8 +359,8 @@ class _$PreferencesImpl implements _Preferences {
|
||||||
return identical(this, other) ||
|
return identical(this, other) ||
|
||||||
(other.runtimeType == runtimeType &&
|
(other.runtimeType == runtimeType &&
|
||||||
other is _$PreferencesImpl &&
|
other is _$PreferencesImpl &&
|
||||||
const DeepCollectionEquality()
|
(identical(other.themePreferences, themePreferences) ||
|
||||||
.equals(other.themePreferences, themePreferences) &&
|
other.themePreferences == themePreferences) &&
|
||||||
(identical(other.language, language) ||
|
(identical(other.language, language) ||
|
||||||
other.language == language) &&
|
other.language == language) &&
|
||||||
(identical(other.locking, locking) || other.locking == locking));
|
(identical(other.locking, locking) || other.locking == locking));
|
||||||
|
@ -357,8 +368,8 @@ class _$PreferencesImpl implements _Preferences {
|
||||||
|
|
||||||
@JsonKey(ignore: true)
|
@JsonKey(ignore: true)
|
||||||
@override
|
@override
|
||||||
int get hashCode => Object.hash(runtimeType,
|
int get hashCode =>
|
||||||
const DeepCollectionEquality().hash(themePreferences), language, locking);
|
Object.hash(runtimeType, themePreferences, language, locking);
|
||||||
|
|
||||||
@JsonKey(ignore: true)
|
@JsonKey(ignore: true)
|
||||||
@override
|
@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_color.dart';
|
||||||
export 'scale_scheme.dart';
|
export 'scale_scheme.dart';
|
||||||
export 'theme_preference.dart';
|
export 'theme_preference.dart';
|
||||||
export 'radix_generator.dart';
|
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
import 'package:change_case/change_case.dart';
|
import 'package:change_case/change_case.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
|
import '../../tools/tools.dart';
|
||||||
|
import 'radix_generator.dart';
|
||||||
|
|
||||||
part 'theme_preference.freezed.dart';
|
part 'theme_preference.freezed.dart';
|
||||||
part 'theme_preference.g.dart';
|
part 'theme_preference.g.dart';
|
||||||
|
|
||||||
|
@ -49,4 +53,62 @@ class ThemePreferences with _$ThemePreferences {
|
||||||
|
|
||||||
factory ThemePreferences.fromJson(dynamic json) =>
|
factory ThemePreferences.fromJson(dynamic json) =>
|
||||||
_$ThemePreferencesFromJson(json as Map<String, dynamic>);
|
_$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 '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 'phono_byte.dart';
|
||||||
export 'responsive.dart';
|
export 'responsive.dart';
|
||||||
export 'scanner_error_widget.dart';
|
export 'scanner_error_widget.dart';
|
||||||
|
export 'shared_preferences.dart';
|
||||||
export 'state_logger.dart';
|
export 'state_logger.dart';
|
||||||
export 'stream_wrapper_cubit.dart';
|
export 'stream_wrapper_cubit.dart';
|
||||||
export 'widget_helpers.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:flutter_translate/flutter_translate.dart';
|
||||||
import 'package:motion_toast/motion_toast.dart';
|
import 'package:motion_toast/motion_toast.dart';
|
||||||
import 'package:quickalert/quickalert.dart';
|
import 'package:quickalert/quickalert.dart';
|
||||||
|
import 'package:veilid_support/veilid_support.dart';
|
||||||
|
|
||||||
import '../theme/theme.dart';
|
import '../theme/theme.dart';
|
||||||
|
|
||||||
|
@ -41,6 +42,24 @@ Widget waitingPage(BuildContext context) => ColoredBox(
|
||||||
color: Theme.of(context).scaffoldBackgroundColor,
|
color: Theme.of(context).scaffoldBackgroundColor,
|
||||||
child: Center(child: buildProgressIndicator(context)));
|
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(
|
Future<void> showErrorModal(
|
||||||
BuildContext context, String title, String text) async {
|
BuildContext context, String title, String text) async {
|
||||||
await QuickAlert.show(
|
await QuickAlert.show(
|
||||||
|
@ -135,3 +154,7 @@ Future<T?> showStyledDialog<T>(
|
||||||
borderRadius: BorderRadius.circular(12))),
|
borderRadius: BorderRadius.circular(12))),
|
||||||
child: child.paddingAll(0)))));
|
child: child.paddingAll(0)))));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool get isPlatformDark =>
|
||||||
|
WidgetsBinding.instance.platformDispatcher.platformBrightness ==
|
||||||
|
Brightness.dark;
|
||||||
|
|
|
@ -27,14 +27,15 @@ Future<void> initializeWindowControl() async {
|
||||||
skipTaskbar: false,
|
skipTaskbar: false,
|
||||||
);
|
);
|
||||||
await windowManager.waitUntilReadyToShow(windowOptions, () async {
|
await windowManager.waitUntilReadyToShow(windowOptions, () async {
|
||||||
await _doWindowSetup(TitleBarStyle.hidden, OrientationCapability.normal);
|
await changeWindowSetup(
|
||||||
|
TitleBarStyle.hidden, OrientationCapability.normal);
|
||||||
await windowManager.show();
|
await windowManager.show();
|
||||||
await windowManager.focus();
|
await windowManager.focus();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _doWindowSetup(TitleBarStyle titleBarStyle,
|
Future<void> changeWindowSetup(TitleBarStyle titleBarStyle,
|
||||||
OrientationCapability orientationCapability) async {
|
OrientationCapability orientationCapability) async {
|
||||||
if (isDesktop) {
|
if (isDesktop) {
|
||||||
await windowManager.setTitleBarStyle(titleBarStyle);
|
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);
|
eventualUpdateBytes(protobufUpdate(fromBuffer, update), subkey: subkey);
|
||||||
|
|
||||||
Future<void> watch(
|
Future<void> watch(
|
||||||
Future<void> Function(VeilidUpdateValueChange update) onUpdate,
|
|
||||||
{List<ValueSubkeyRange>? subkeys,
|
{List<ValueSubkeyRange>? subkeys,
|
||||||
Timestamp? expiration,
|
Timestamp? expiration,
|
||||||
int? count}) async {
|
int? count}) async {
|
||||||
|
|
|
@ -5,8 +5,8 @@ import 'package:bloc/bloc.dart';
|
||||||
|
|
||||||
import '../../veilid_support.dart';
|
import '../../veilid_support.dart';
|
||||||
|
|
||||||
class DhtRecordCubit<T> extends Cubit<AsyncValue<T>> {
|
class DHTRecordCubit<T> extends Cubit<AsyncValue<T>> {
|
||||||
DhtRecordCubit({
|
DHTRecordCubit({
|
||||||
required DHTRecord record,
|
required DHTRecord record,
|
||||||
required Future<T?> Function(DHTRecord) initialStateFunction,
|
required Future<T?> Function(DHTRecord) initialStateFunction,
|
||||||
required Future<T?> Function(DHTRecord, List<ValueSubkeyRange>, ValueData)
|
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
|
// Cubit that watches the default subkey value of a dhtrecord
|
||||||
class DefaultDHTRecordCubit<T> extends DhtRecordCubit<T> {
|
class DefaultDHTRecordCubit<T> extends DHTRecordCubit<T> {
|
||||||
DefaultDHTRecordCubit({
|
DefaultDHTRecordCubit({
|
||||||
required super.record,
|
required super.record,
|
||||||
required T Function(List<int> data) decodeState,
|
required T Function(List<int> data) decodeState,
|
||||||
|
|
|
@ -64,10 +64,12 @@ class TableDBValue<T> extends TableDBBacked<T> {
|
||||||
}) : _tableName = tableName,
|
}) : _tableName = tableName,
|
||||||
_valueFromJson = valueFromJson,
|
_valueFromJson = valueFromJson,
|
||||||
_valueToJson = valueToJson,
|
_valueToJson = valueToJson,
|
||||||
_tableKeyName = tableKeyName;
|
_tableKeyName = tableKeyName,
|
||||||
|
_streamController = StreamController<T>.broadcast();
|
||||||
|
|
||||||
T? get value => _value;
|
T? get value => _value;
|
||||||
T get requireValue => _value!;
|
T get requireValue => _value!;
|
||||||
|
Stream<T> get stream => _streamController.stream;
|
||||||
|
|
||||||
Future<T> get() async {
|
Future<T> get() async {
|
||||||
final val = _value;
|
final val = _value;
|
||||||
|
@ -80,6 +82,7 @@ class TableDBValue<T> extends TableDBBacked<T> {
|
||||||
|
|
||||||
Future<void> set(T newVal) async {
|
Future<void> set(T newVal) async {
|
||||||
_value = await store(newVal);
|
_value = await store(newVal);
|
||||||
|
_streamController.add(newVal);
|
||||||
}
|
}
|
||||||
|
|
||||||
T? _value;
|
T? _value;
|
||||||
|
@ -87,6 +90,7 @@ class TableDBValue<T> extends TableDBBacked<T> {
|
||||||
final String _tableKeyName;
|
final String _tableKeyName;
|
||||||
final T Function(Object? obj) _valueFromJson;
|
final T Function(Object? obj) _valueFromJson;
|
||||||
final Object? Function(T obj) _valueToJson;
|
final Object? Function(T obj) _valueToJson;
|
||||||
|
final StreamController<T> _streamController;
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////
|
||||||
/// AsyncTableDBBacked
|
/// AsyncTableDBBacked
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue