checkpoint

This commit is contained in:
Christien Rioux 2024-01-04 22:29:43 -05:00
parent c516323e7d
commit 31f562119a
70 changed files with 1174 additions and 817 deletions

View file

@ -1,18 +0,0 @@
import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
class DefaultAppBar extends AppBar {
DefaultAppBar(
{required super.title, super.key, Widget? leading, super.actions})
: super(
leading: leading ??
Container(
margin: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: Colors.black.withAlpha(32),
shape: BoxShape.circle),
child:
SvgPicture.asset('assets/images/vlogo.svg', height: 32)
.paddingAll(4)));
}

View file

@ -1,66 +0,0 @@
import 'package:flutter/material.dart';
import 'package:signal_strength_indicator/signal_strength_indicator.dart';
import 'package:veilid_support/veilid_support.dart';
import '../providers/connection_state.dart';
import '../tools/tools.dart';
xxx move to feature level
class SignalStrengthMeterWidget extends Widget {
const SignalStrengthMeterWidget({super.key});
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context, WidgetRef ref) {
final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!;
const iconSize = 16.0;
final connState = ref.watch(connectionStateProvider);
late final double value;
late final Color color;
late final Color inactiveColor;
switch (connState.attachment.state) {
case AttachmentState.detached:
return Icon(Icons.signal_cellular_nodata,
size: iconSize, color: scale.grayScale.text);
case AttachmentState.detaching:
return Icon(Icons.signal_cellular_off,
size: iconSize, color: scale.grayScale.text);
case AttachmentState.attaching:
value = 0;
color = scale.primaryScale.text;
case AttachmentState.attachedWeak:
value = 1;
color = scale.primaryScale.text;
case AttachmentState.attachedStrong:
value = 2;
color = scale.primaryScale.text;
case AttachmentState.attachedGood:
value = 3;
color = scale.primaryScale.text;
case AttachmentState.fullyAttached:
value = 4;
color = scale.primaryScale.text;
case AttachmentState.overAttached:
value = 4;
color = scale.secondaryScale.subtleText;
}
inactiveColor = scale.grayScale.subtleText;
return GestureDetector(
onLongPress: () async {
await context.push('/developer');
},
child: SignalStrengthIndicator.bars(
value: value,
activeColor: color,
inactiveColor: inactiveColor,
size: iconSize,
barCount: 4,
spacing: 1,
));
}
}

View file

@ -1,45 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../providers/window_control.dart';
import 'home.dart';
class ChatOnlyPage extends StatefulWidget {
const ChatOnlyPage({super.key});
@override
ChatOnlyPageState createState() => ChatOnlyPageState();
}
class ChatOnlyPageState extends ConsumerState<ChatOnlyPage>
with TickerProviderStateMixin {
final _unfocusNode = FocusNode();
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) async {
setState(() {});
await ref.read(windowControlProvider.notifier).changeWindowSetup(
TitleBarStyle.normal, OrientationCapability.normal);
});
}
@override
void dispose() {
_unfocusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
ref.watch(windowControlProvider);
return SafeArea(
child: GestureDetector(
onTap: () => FocusScope.of(context).requestFocus(_unfocusNode),
child: HomePage.buildChatComponent(context, ref),
));
}
}

View file

@ -1,256 +0,0 @@
import 'package:ansicolor/ansicolor.dart';
import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:cool_dropdown/cool_dropdown.dart';
import 'package:cool_dropdown/models/cool_dropdown_item.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_translate/flutter_translate.dart';
import 'package:go_router/go_router.dart';
import 'package:loggy/loggy.dart';
import 'package:quickalert/quickalert.dart';
import 'package:xterm/xterm.dart';
import '../../tools/tools.dart';
import '../../../packages/veilid_support/veilid_support.dart';
final globalDebugTerminal = Terminal(
maxLines: 50000,
);
const kDefaultTerminalStyle = TerminalStyle(
fontSize: 11,
// height: 1.2,
fontFamily: 'Source Code Pro');
class DeveloperPage extends StatefulWidget {
const DeveloperPage({super.key});
@override
DeveloperPageState createState() => DeveloperPageState();
}
class DeveloperPageState extends ConsumerState<DeveloperPage> {
final _terminalController = TerminalController();
final _debugCommandController = TextEditingController();
final _logLevelController = DropdownController(duration: 250.ms);
final List<CoolDropdownItem<LogLevel>> _logLevelDropdownItems = [];
var _logLevelDropDown = log.level.logLevel;
var _showEllet = false;
@override
void initState() {
super.initState();
_terminalController.addListener(() {
setState(() {});
});
for (var i = 0; i < logLevels.length; i++) {
_logLevelDropdownItems.add(CoolDropdownItem<LogLevel>(
label: logLevelName(logLevels[i]),
icon: Text(logLevelEmoji(logLevels[i])),
value: logLevels[i]));
}
}
void _debugOut(String out) {
final pen = AnsiPen()..cyan(bold: true);
final colorOut = pen(out);
debugPrint(colorOut);
globalDebugTerminal.write(colorOut.replaceAll('\n', '\r\n'));
}
Future<void> _sendDebugCommand(String debugCommand) async {
if (debugCommand == 'ellet') {
setState(() {
_showEllet = !_showEllet;
});
return;
}
_debugOut('DEBUG >>>\n$debugCommand\n');
try {
final out = await Veilid.instance.debug(debugCommand);
_debugOut('<<< DEBUG\n$out\n');
} on Exception catch (e, st) {
_debugOut('<<< ERROR\n$e\n<<< STACK\n$st');
}
}
Future<void> clear(BuildContext context) async {
globalDebugTerminal.buffer.clear();
if (context.mounted) {
showInfoToast(context, translate('developer.cleared'));
}
}
Future<void> copySelection(BuildContext context) async {
final selection = _terminalController.selection;
if (selection != null) {
final text = globalDebugTerminal.buffer.getText(selection);
_terminalController.clearSelection();
await Clipboard.setData(ClipboardData(text: text));
if (context.mounted) {
showInfoToast(context, translate('developer.copied'));
}
}
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final textTheme = theme.textTheme;
final scale = theme.extension<ScaleScheme>()!;
// WidgetsBinding.instance.addPostFrameCallback((_) {
// if (!_isScrolling && _wantsBottom) {
// _scrollToBottom();
// }
// });
return Scaffold(
appBar: AppBar(
leading: IconButton(
icon: Icon(Icons.arrow_back, color: scale.primaryScale.text),
onPressed: () => GoRouterHelper(context).pop(),
),
actions: [
IconButton(
icon: const Icon(Icons.copy),
color: scale.primaryScale.text,
disabledColor: scale.grayScale.subtleText,
onPressed: _terminalController.selection == null
? null
: () async {
await copySelection(context);
}),
IconButton(
icon: const Icon(Icons.clear_all),
color: scale.primaryScale.text,
disabledColor: scale.grayScale.subtleText,
onPressed: () async {
await QuickAlert.show(
context: context,
type: QuickAlertType.confirm,
title: translate('developer.are_you_sure_clear'),
textColor: scale.primaryScale.text,
confirmBtnColor: scale.primaryScale.elementBackground,
backgroundColor: scale.primaryScale.subtleBackground,
headerBackgroundColor: scale.primaryScale.background,
confirmBtnText: translate('button.ok'),
cancelBtnText: translate('button.cancel'),
onConfirmBtnTap: () async {
Navigator.pop(context);
if (context.mounted) {
await clear(context);
}
});
}),
CoolDropdown<LogLevel>(
controller: _logLevelController,
defaultItem: _logLevelDropdownItems
.singleWhere((x) => x.value == _logLevelDropDown),
onChange: (value) {
setState(() {
_logLevelDropDown = value;
Loggy('').level = getLogOptions(value);
setVeilidLogLevel(value);
_logLevelController.close();
});
},
resultOptions: ResultOptions(
width: 64,
height: 40,
render: ResultRender.icon,
textStyle: textTheme.labelMedium!
.copyWith(color: scale.primaryScale.text),
padding: const EdgeInsets.fromLTRB(8, 4, 8, 4),
openBoxDecoration: BoxDecoration(
color: scale.primaryScale.activeElementBackground),
boxDecoration:
BoxDecoration(color: scale.primaryScale.elementBackground),
),
dropdownOptions: DropdownOptions(
width: 160,
align: DropdownAlign.right,
duration: 150.ms,
color: scale.primaryScale.elementBackground,
borderSide: BorderSide(color: scale.primaryScale.border),
borderRadius: BorderRadius.circular(8),
padding: const EdgeInsets.fromLTRB(8, 4, 8, 4),
),
dropdownTriangleOptions: const DropdownTriangleOptions(
align: DropdownTriangleAlign.right),
dropdownItemOptions: DropdownItemOptions(
selectedTextStyle: textTheme.labelMedium!
.copyWith(color: scale.primaryScale.text),
textStyle: textTheme.labelMedium!
.copyWith(color: scale.primaryScale.text),
selectedBoxDecoration: BoxDecoration(
color: scale.primaryScale.activeElementBackground),
mainAxisAlignment: MainAxisAlignment.spaceBetween,
padding: const EdgeInsets.fromLTRB(8, 4, 8, 4),
selectedPadding: const EdgeInsets.fromLTRB(8, 4, 8, 4)),
dropdownList: _logLevelDropdownItems,
)
],
title: Text(translate('developer.title'),
style:
textTheme.bodyLarge!.copyWith(fontWeight: FontWeight.bold)),
centerTitle: true,
),
body: SafeArea(
child: Column(children: [
Stack(alignment: AlignmentDirectional.center, children: [
Image.asset('assets/images/ellet.png'),
TerminalView(globalDebugTerminal,
textStyle: kDefaultTerminalStyle,
controller: _terminalController,
//autofocus: true,
backgroundOpacity: _showEllet ? 0.75 : 1.0,
onSecondaryTapDown: (details, offset) async {
await copySelection(context);
})
]).expanded(),
TextField(
controller: _debugCommandController,
decoration: InputDecoration(
filled: true,
contentPadding: const EdgeInsets.fromLTRB(8, 2, 8, 2),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: scale.primaryScale.border)),
fillColor: scale.primaryScale.subtleBackground,
hintText: translate('developer.command'),
suffixIcon: IconButton(
icon: const Icon(Icons.send),
onPressed: _debugCommandController.text.isEmpty
? null
: () async {
final debugCommand = _debugCommandController.text;
_debugCommandController.clear();
await _sendDebugCommand(debugCommand);
},
)),
onChanged: (_) {
setState(() => {});
},
onSubmitted: (debugCommand) async {
_debugCommandController.clear();
await _sendDebugCommand(debugCommand);
},
).paddingAll(4)
])));
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties
..add(DiagnosticsProperty<TerminalController>(
'terminalController', _terminalController))
..add(
DiagnosticsProperty<LogLevel>('logLevelDropDown', _logLevelDropDown));
}
}

View file

@ -1,28 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class ContactsPage extends ConsumerWidget {
const ContactsPage({super.key});
static const path = '/contacts';
@override
Widget build(BuildContext context, WidgetRef ref) => const Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Contacts Page'),
// ElevatedButton(
// onPressed: () async {
// ref.watch(authNotifierProvider.notifier).login(
// "myEmail",
// "myPassword",
// );
// },
// child: const Text("Login"),
// ),
],
),
),
);
}

View file

@ -1,269 +0,0 @@
import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_translate/flutter_translate.dart';
import 'package:go_router/go_router.dart';
import '../../proto/proto.dart' as proto;
import '../../components/chat_component.dart';
import '../../components/empty_chat_widget.dart';
import '../../components/profile_widget.dart';
import '../../entities/local_account.dart';
import '../providers/account.dart';
import '../providers/chat.dart';
import '../providers/contact.dart';
import '../../local_accounts/local_accounts.dart';
import '../providers/logins.dart';
import '../providers/window_control.dart';
import '../../tools/tools.dart';
import '../../../packages/veilid_support/veilid_support.dart';
import 'main_pager/main_pager.dart';
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
HomePageState createState() => HomePageState();
static Widget buildChatComponent(BuildContext context, WidgetRef ref) {
final contactList = ref.watch(fetchContactListProvider).asData?.value ??
const IListConst([]);
final activeChat = ref.watch(activeChatStateProvider);
if (activeChat == null) {
return const EmptyChatWidget();
}
final activeAccountInfo =
ref.watch(fetchActiveAccountProvider).asData?.value;
if (activeAccountInfo == null) {
return const EmptyChatWidget();
}
final activeChatContactIdx = contactList.indexWhere(
(c) =>
proto.TypedKeyProto.fromProto(c.remoteConversationRecordKey) ==
activeChat,
);
if (activeChatContactIdx == -1) {
ref.read(activeChatStateProvider.notifier).state = null;
return const EmptyChatWidget();
}
final activeChatContact = contactList[activeChatContactIdx];
return ChatComponent(
activeAccountInfo: activeAccountInfo,
activeChat: activeChat,
activeChatContact: activeChatContact);
}
}
class HomePageState extends ConsumerState<HomePage>
with TickerProviderStateMixin {
final _unfocusNode = FocusNode();
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) async {
setState(() {});
await ref.read(windowControlProvider.notifier).changeWindowSetup(
TitleBarStyle.normal, OrientationCapability.normal);
});
}
@override
void dispose() {
_unfocusNode.dispose();
super.dispose();
}
// ignore: prefer_expression_function_bodies
Widget buildAccountList() {
return const Column(children: [
Center(child: Text('Small Profile')),
Center(child: Text('Contact invitations')),
Center(child: Text('Contacts'))
]);
}
Widget buildUnlockAccount(
BuildContext context,
IList<LocalAccount> localAccounts,
// ignore: prefer_expression_function_bodies
) {
return const Center(child: Text('unlock account'));
}
/// We have an active, unlocked, user login
Widget buildReadyAccount(
BuildContext context,
IList<LocalAccount> localAccounts,
TypedKey activeUserLogin,
proto.Account account) {
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()
]);
}
Widget buildUserPanel() {
final localAccountsV = ref.watch(localAccountsProvider);
final loginsV = ref.watch(loginsProvider);
if (!localAccountsV.hasValue || !loginsV.hasValue) {
return waitingPage(context);
}
final localAccounts = localAccountsV.requireValue;
final logins = loginsV.requireValue;
final activeUserLogin = logins.activeUserLogin;
if (activeUserLogin == null) {
// If no logged in user is active, show the list of account
return buildAccountList();
}
final accountV = ref
.watch(fetchAccountProvider(accountMasterRecordKey: activeUserLogin));
if (!accountV.hasValue) {
return waitingPage(context);
}
final account = accountV.requireValue;
switch (account.status) {
case AccountInfoStatus.noAccount:
Future.delayed(0.ms, () async {
await showErrorModal(context, translate('home.missing_account_title'),
translate('home.missing_account_text'));
// Delete account
await ref
.read(localAccountsProvider.notifier)
.deleteLocalAccount(activeUserLogin);
// Switch to no active user login
await ref.read(loginsProvider.notifier).switchToAccount(null);
});
return waitingPage(context);
case AccountInfoStatus.accountInvalid:
Future.delayed(0.ms, () async {
await showErrorModal(context, translate('home.invalid_account_title'),
translate('home.invalid_account_text'));
// Delete account
await ref
.read(localAccountsProvider.notifier)
.deleteLocalAccount(activeUserLogin);
// Switch to no active user login
await ref.read(loginsProvider.notifier).switchToAccount(null);
});
return waitingPage(context);
case AccountInfoStatus.accountLocked:
// Show unlock widget
return buildUnlockAccount(context, localAccounts);
case AccountInfoStatus.accountReady:
return buildReadyAccount(
context,
localAccounts,
activeUserLogin,
account.account!,
);
}
}
// ignore: prefer_expression_function_bodies
Widget buildPhone(BuildContext context) {
return Material(color: Colors.transparent, child: buildUserPanel());
}
// ignore: prefer_expression_function_bodies
Widget buildTabletLeftPane(BuildContext context) {
//
return Material(color: Colors.transparent, child: buildUserPanel());
}
// ignore: prefer_expression_function_bodies
Widget buildTabletRightPane(BuildContext context) {
//
return HomePage.buildChatComponent(context, ref);
}
// ignore: prefer_expression_function_bodies
Widget buildTablet(BuildContext context) {
final w = MediaQuery.of(context).size.width;
final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!;
final children = [
ConstrainedBox(
constraints: const BoxConstraints(minWidth: 300, maxWidth: 300),
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: w / 2),
child: buildTabletLeftPane(context))),
SizedBox(
width: 2,
height: double.infinity,
child: ColoredBox(color: scale.primaryScale.hoverBorder)),
Expanded(child: buildTabletRightPane(context)),
];
return Row(
children: children,
);
// final theme = MultiSplitViewTheme(
// data: isDesktop
// ? MultiSplitViewThemeData(
// dividerThickness: 1,
// dividerPainter: DividerPainters.grooved2(thickness: 1))
// : MultiSplitViewThemeData(
// dividerThickness: 3,
// dividerPainter: DividerPainters.grooved2(thickness: 1)),
// child: multiSplitView);
}
@override
Widget build(BuildContext context) {
ref.watch(windowControlProvider);
final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!;
return SafeArea(
child: GestureDetector(
onTap: () => FocusScope.of(context).requestFocus(_unfocusNode),
child: DecoratedBox(
decoration: BoxDecoration(
color: scale.primaryScale.activeElementBackground),
child: responsiveVisibility(
context: context,
phone: false,
)
? buildTablet(context)
: buildPhone(context),
)));
}
}

View file

@ -1,50 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:radix_colors/radix_colors.dart';
class IndexPage extends StatelessWidget {
const IndexPage({super.key});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final textTheme = theme.textTheme;
final monoTextStyle = textTheme.labelSmall!
.copyWith(fontFamily: 'Source Code Pro', fontSize: 11);
final emojiTextStyle = textTheme.labelSmall!
.copyWith(fontFamily: 'Noto Color Emoji', fontSize: 11);
return Scaffold(
body: DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: <Color>[
RadixColors.dark.plum.step4,
RadixColors.dark.plum.step2,
])),
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 300),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Hack to preload fonts
Offstage(child: Text('🧱', style: emojiTextStyle)),
// Hack to preload fonts
Offstage(child: Text('A', style: monoTextStyle)),
// Splash Screen
Expanded(
flex: 2,
child: SvgPicture.asset(
'assets/images/icon.svg',
)),
Expanded(
child: SvgPicture.asset(
'assets/images/title.svg',
))
]))),
));
}
}

View file

@ -1,99 +0,0 @@
// ignore_for_file: prefer_const_constructors
import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_translate/flutter_translate.dart';
import '../../../components/contact_invitation_list_widget.dart';
import '../../../components/contact_list_widget.dart';
import '../../../entities/local_account.dart';
import '../../../proto/proto.dart' as proto;
import '../../providers/contact.dart';
import '../../providers/contact_invite.dart';
import '../../../theme/theme.dart';
import '../../../tools/tools.dart';
import '../../../../packages/veilid_support/veilid_support.dart';
class AccountPage extends ConsumerStatefulWidget {
const AccountPage({
required this.localAccounts,
required this.activeUserLogin,
required this.account,
super.key,
});
final IList<LocalAccount> localAccounts;
final TypedKey activeUserLogin;
final proto.Account account;
@override
AccountPageState createState() => AccountPageState();
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties
..add(IterableProperty<LocalAccount>('localAccounts', localAccounts))
..add(DiagnosticsProperty<TypedKey>('activeUserLogin', activeUserLogin))
..add(DiagnosticsProperty<proto.Account>('account', account));
}
}
class AccountPageState extends ConsumerState<AccountPage> {
final _unfocusNode = FocusNode();
@override
void initState() {
super.initState();
}
@override
void dispose() {
_unfocusNode.dispose();
super.dispose();
}
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
final theme = Theme.of(context);
final textTheme = theme.textTheme;
final scale = theme.extension<ScaleScheme>()!;
final contactInvitationRecordList =
ref.watch(fetchContactInvitationRecordsProvider).asData?.value ??
const IListConst([]);
final contactList = ref.watch(fetchContactListProvider).asData?.value ??
const IListConst([]);
return SizedBox(
child: Column(children: <Widget>[
if (contactInvitationRecordList.isNotEmpty)
ExpansionTile(
tilePadding: EdgeInsets.fromLTRB(8, 0, 8, 0),
backgroundColor: scale.primaryScale.border,
collapsedBackgroundColor: scale.primaryScale.border,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
collapsedShape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
title: Text(
translate('account_page.contact_invitations'),
textAlign: TextAlign.center,
style: textTheme.titleMedium!
.copyWith(color: scale.primaryScale.subtleText),
),
initiallyExpanded: true,
children: [
ContactInvitationListWidget(
contactInvitationRecordList: contactInvitationRecordList)
],
).paddingLTRB(8, 0, 8, 8),
ContactListWidget(contactList: contactList).expanded(),
]));
}
}

View file

@ -1,100 +0,0 @@
import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../components/chat_single_contact_list_widget.dart';
import '../../../components/empty_chat_list_widget.dart';
import '../../../entities/local_account.dart';
import '../../../proto/proto.dart' as proto;
import '../../providers/account.dart';
import '../../providers/chat.dart';
import '../../providers/contact.dart';
import '../../../local_accounts/local_accounts.dart';
import '../../providers/logins.dart';
import '../../../tools/tools.dart';
import '../../../../packages/veilid_support/veilid_support.dart';
class ChatsPage extends ConsumerStatefulWidget {
const ChatsPage({super.key});
@override
ChatsPageState createState() => ChatsPageState();
}
class ChatsPageState extends ConsumerState<ChatsPage> {
final _unfocusNode = FocusNode();
@override
void initState() {
super.initState();
}
@override
void dispose() {
_unfocusNode.dispose();
super.dispose();
}
/// We have an active, unlocked, user login
Widget buildChatList(
BuildContext context,
IList<LocalAccount> localAccounts,
TypedKey activeUserLogin,
proto.Account account,
// ignore: prefer_expression_function_bodies
) {
final contactList = ref.watch(fetchContactListProvider).asData?.value ??
const IListConst([]);
final chatList =
ref.watch(fetchChatListProvider).asData?.value ?? const IListConst([]);
return Column(children: <Widget>[
if (chatList.isNotEmpty)
ChatSingleContactListWidget(
contactList: contactList, chatList: chatList)
.expanded(),
if (chatList.isEmpty) const EmptyChatListWidget().expanded(),
]);
}
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
final localAccountsV = ref.watch(localAccountsProvider);
final loginsV = ref.watch(loginsProvider);
if (!localAccountsV.hasValue || !loginsV.hasValue) {
return waitingPage(context);
}
final localAccounts = localAccountsV.requireValue;
final logins = loginsV.requireValue;
final activeUserLogin = logins.activeUserLogin;
if (activeUserLogin == null) {
// If no logged in user is active show a placeholder
return waitingPage(context);
}
final accountV = ref
.watch(fetchAccountProvider(accountMasterRecordKey: activeUserLogin));
if (!accountV.hasValue) {
return waitingPage(context);
}
final account = accountV.requireValue;
switch (account.status) {
case AccountInfoStatus.noAccount:
return waitingPage(context);
case AccountInfoStatus.accountInvalid:
return waitingPage(context);
case AccountInfoStatus.accountLocked:
return waitingPage(context);
case AccountInfoStatus.accountReady:
return buildChatList(
context,
localAccounts,
activeUserLogin,
account.account!,
);
}
}
}

View file

@ -1,315 +0,0 @@
import 'dart:async';
import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_translate/flutter_translate.dart';
import 'package:preload_page_view/preload_page_view.dart';
import 'package:stylish_bottom_bar/model/bar_items.dart';
import 'package:stylish_bottom_bar/stylish_bottom_bar.dart';
import '../../../components/bottom_sheet_action_button.dart';
import '../../../components/paste_invite_dialog.dart';
import '../../../components/scan_invite_dialog.dart';
import '../../../components/send_invite_dialog.dart';
import '../../../entities/local_account.dart';
import '../../../proto/proto.dart' as proto;
import '../../../tools/tools.dart';
import '../../../../packages/veilid_support/veilid_support.dart';
import 'account.dart';
import 'chats.dart';
class MainPager extends ConsumerStatefulWidget {
const MainPager(
{required this.localAccounts,
required this.activeUserLogin,
required this.account,
super.key});
final IList<LocalAccount> localAccounts;
final TypedKey activeUserLogin;
final proto.Account account;
@override
MainPagerState createState() => MainPagerState();
static MainPagerState? of(BuildContext context) =>
context.findAncestorStateOfType<MainPagerState>();
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties
..add(IterableProperty<LocalAccount>('localAccounts', localAccounts))
..add(DiagnosticsProperty<TypedKey>('activeUserLogin', activeUserLogin))
..add(DiagnosticsProperty<proto.Account>('account', account));
}
}
class MainPagerState extends ConsumerState<MainPager>
with TickerProviderStateMixin {
//////////////////////////////////////////////////////////////////
final _unfocusNode = FocusNode();
var _currentPage = 0;
final pageController = PreloadPageController();
final _selectedIconList = <IconData>[Icons.person, Icons.chat];
// final _unselectedIconList = <IconData>[
// Icons.chat_outlined,
// Icons.person_outlined
// ];
final _fabIconList = <IconData>[
Icons.person_add_sharp,
Icons.add_comment_sharp,
];
final _bottomLabelList = <String>[
translate('pager.account'),
translate('pager.chats'),
];
//////////////////////////////////////////////////////////////////
@override
void initState() {
super.initState();
}
@override
void dispose() {
_unfocusNode.dispose();
pageController.dispose();
super.dispose();
}
bool onScrollNotification(ScrollNotification notification) {
if (notification is UserScrollNotification &&
notification.metrics.axis == Axis.vertical) {
switch (notification.direction) {
case ScrollDirection.forward:
// _hideBottomBarAnimationController.reverse();
// _fabAnimationController.forward(from: 0);
break;
case ScrollDirection.reverse:
// _hideBottomBarAnimationController.forward();
// _fabAnimationController.reverse(from: 1);
break;
case ScrollDirection.idle:
break;
}
}
return false;
}
BottomBarItem buildBottomBarItem(int index) {
final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!;
return BottomBarItem(
title: Text(_bottomLabelList[index]),
icon: Icon(_selectedIconList[index], color: scale.primaryScale.text),
selectedIcon:
Icon(_selectedIconList[index], color: scale.primaryScale.text),
backgroundColor: scale.primaryScale.text,
//unSelectedColor: theme.colorScheme.primaryContainer,
//selectedColor: theme.colorScheme.primary,
//badge: const Text('9+'),
//showBadge: true,
);
}
List<BottomBarItem> _buildBottomBarItems() {
final bottomBarItems = List<BottomBarItem>.empty(growable: true);
for (var index = 0; index < _bottomLabelList.length; index++) {
final item = buildBottomBarItem(index);
bottomBarItems.add(item);
}
return bottomBarItems;
}
Future<void> scanContactInvitationDialog(BuildContext context) async {
await showDialog<void>(
context: context,
// ignore: prefer_expression_function_bodies
builder: (context) {
return const AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(20)),
),
contentPadding: EdgeInsets.only(
top: 10,
),
title: Text(
'Scan Contact Invite',
style: TextStyle(fontSize: 24),
),
content: ScanInviteDialog());
});
}
Widget _newContactInvitationBottomSheetBuilder(
// ignore: prefer_expression_function_bodies
BuildContext context) {
final theme = Theme.of(context);
final textTheme = theme.textTheme;
final scale = theme.extension<ScaleScheme>()!;
return KeyboardListener(
focusNode: FocusNode(),
onKeyEvent: (ke) {
if (ke.logicalKey == LogicalKeyboardKey.escape) {
Navigator.pop(context);
}
},
child: SizedBox(
height: 200,
child: Column(children: [
Text(translate('accounts_menu.invite_contact'),
style: textTheme.titleMedium)
.paddingAll(8),
Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [
Column(children: [
IconButton(
onPressed: () async {
Navigator.pop(context);
await SendInviteDialog.show(context);
},
iconSize: 64,
icon: const Icon(Icons.contact_page),
color: scale.primaryScale.background),
Text(translate('accounts_menu.create_invite'))
]),
Column(children: [
IconButton(
onPressed: () async {
Navigator.pop(context);
await ScanInviteDialog.show(context);
},
iconSize: 64,
icon: const Icon(Icons.qr_code_scanner),
color: scale.primaryScale.background),
Text(translate('accounts_menu.scan_invite'))
]),
Column(children: [
IconButton(
onPressed: () async {
Navigator.pop(context);
await PasteInviteDialog.show(context);
},
iconSize: 64,
icon: const Icon(Icons.paste),
color: scale.primaryScale.background),
Text(translate('accounts_menu.paste_invite'))
])
]).expanded()
])));
}
// ignore: prefer_expression_function_bodies
Widget _onNewChatBottomSheetBuilder(BuildContext context) {
return const SizedBox(
height: 200,
child: Center(
child: Text(
'Group and custom chat functionality is not available yet')));
}
Widget _bottomSheetBuilder(BuildContext context) {
if (_currentPage == 0) {
// New contact invitation
return _newContactInvitationBottomSheetBuilder(context);
} else if (_currentPage == 1) {
// New chat
return _onNewChatBottomSheetBuilder(context);
} else {
// Unknown error
return waitingPage(context);
}
}
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!;
return Scaffold(
//extendBody: true,
backgroundColor: Colors.transparent,
body: NotificationListener<ScrollNotification>(
onNotification: onScrollNotification,
child: PreloadPageView(
controller: pageController,
preloadPagesCount: 2,
onPageChanged: (index) {
setState(() {
_currentPage = index;
});
},
children: [
AccountPage(
localAccounts: widget.localAccounts,
activeUserLogin: widget.activeUserLogin,
account: widget.account),
const ChatsPage(),
])),
// appBar: AppBar(
// toolbarHeight: 24,
// title: Text(
// 'C',
// style: Theme.of(context).textTheme.headlineSmall,
// ),
// ),
bottomNavigationBar: StylishBottomBar(
backgroundColor: scale.primaryScale.hoverBorder,
// gradient: LinearGradient(
// begin: Alignment.topCenter,
// end: Alignment.bottomCenter,
// colors: <Color>[
// theme.colorScheme.primary,
// theme.colorScheme.primaryContainer,
// ]),
//borderRadius: BorderRadius.all(Radius.circular(16)),
option: AnimatedBarOptions(
// iconSize: 32,
//barAnimation: BarAnimation.fade,
iconStyle: IconStyle.animated,
inkEffect: true,
inkColor: scale.primaryScale.hoverBackground,
//opacity: 0.3,
),
items: _buildBottomBarItems(),
hasNotch: true,
fabLocation: StylishBarFabLocation.end,
currentIndex: _currentPage,
onTap: (index) async {
await pageController.animateToPage(index,
duration: 250.ms, curve: Curves.easeInOut);
},
),
floatingActionButton: BottomSheetActionButton(
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(14))),
foregroundColor: scale.secondaryScale.text,
backgroundColor: scale.secondaryScale.hoverBorder,
builder: (context) => Icon(
_fabIconList[_currentPage],
color: scale.secondaryScale.text,
),
bottomSheetBuilder: _bottomSheetBuilder),
floatingActionButtonLocation: FloatingActionButtonLocation.endDocked,
);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<PreloadPageController>(
'pageController', pageController));
}
}

View file

@ -1,139 +0,0 @@
import 'package:animated_theme_switcher/animated_theme_switcher.dart';
import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_translate/flutter_translate.dart';
import '../../components/default_app_bar.dart';
import '../../components/signal_strength_meter.dart';
import '../../entities/preferences.dart';
import '../providers/window_control.dart';
import '../../tools/tools.dart';
class SettingsPage extends StatefulWidget {
const SettingsPage({super.key});
@override
SettingsPageState createState() => SettingsPageState();
}
class SettingsPageState extends ConsumerState<SettingsPage> {
final _formKey = GlobalKey<FormBuilderState>();
late bool isInAsyncCall = false;
// ThemePreferences? themePreferences;
static const String formFieldTheme = 'theme';
static const String formFieldBrightness = 'brightness';
// static const String formFieldTitle = 'title';
@override
void initState() {
super.initState();
}
List<DropdownMenuItem<dynamic>> _getThemeDropdownItems() {
const colorPrefs = ColorPreference.values;
final colorNames = {
ColorPreference.scarlet: translate('themes.scarlet'),
ColorPreference.vapor: translate('themes.vapor'),
ColorPreference.babydoll: translate('themes.babydoll'),
ColorPreference.gold: translate('themes.gold'),
ColorPreference.garden: translate('themes.garden'),
ColorPreference.forest: translate('themes.forest'),
ColorPreference.arctic: translate('themes.arctic'),
ColorPreference.lapis: translate('themes.lapis'),
ColorPreference.eggplant: translate('themes.eggplant'),
ColorPreference.lime: translate('themes.lime'),
ColorPreference.grim: translate('themes.grim'),
ColorPreference.contrast: translate('themes.contrast')
};
return colorPrefs
.map((e) => DropdownMenuItem(value: e, child: Text(colorNames[e]!)))
.toList();
}
List<DropdownMenuItem<dynamic>> _getBrightnessDropdownItems() {
const brightnessPrefs = BrightnessPreference.values;
final brightnessNames = {
BrightnessPreference.system: translate('brightness.system'),
BrightnessPreference.light: translate('brightness.light'),
BrightnessPreference.dark: translate('brightness.dark')
};
return brightnessPrefs
.map(
(e) => DropdownMenuItem(value: e, child: Text(brightnessNames[e]!)))
.toList();
}
@override
Widget build(BuildContext context) {
ref.watch(windowControlProvider);
final themeService = ref.watch(themeServiceProvider).valueOrNull;
if (themeService == null) {
return waitingPage(context);
}
final themePreferences = themeService.load();
return ThemeSwitchingArea(
child: Scaffold(
// resizeToAvoidBottomInset: false,
appBar: DefaultAppBar(
title: Text(translate('settings_page.titlebar')),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => context.pop<void>(),
),
actions: <Widget>[
const SignalStrengthMeterWidget().paddingLTRB(16, 0, 16, 0),
]),
body: FormBuilder(
key: _formKey,
child: ListView(
children: [
ThemeSwitcher.withTheme(
builder: (_, switcher, theme) => FormBuilderDropdown(
name: formFieldTheme,
decoration: InputDecoration(
label: Text(translate('settings_page.color_theme'))),
items: _getThemeDropdownItems(),
initialValue: themePreferences.colorPreference,
onChanged: (value) async {
final newPrefs = themePreferences.copyWith(
colorPreference: value as ColorPreference);
await themeService.save(newPrefs);
switcher.changeTheme(theme: themeService.get(newPrefs));
ref.invalidate(themeServiceProvider);
setState(() {});
})),
ThemeSwitcher.withTheme(
builder: (_, switcher, theme) => FormBuilderDropdown(
name: formFieldBrightness,
decoration: InputDecoration(
label:
Text(translate('settings_page.brightness_mode'))),
items: _getBrightnessDropdownItems(),
initialValue: themePreferences.brightnessPreference,
onChanged: (value) async {
final newPrefs = themePreferences.copyWith(
brightnessPreference: value as BrightnessPreference);
await themeService.save(newPrefs);
switcher.changeTheme(theme: themeService.get(newPrefs));
ref.invalidate(themeServiceProvider);
setState(() {});
})),
],
),
).paddingSymmetric(horizontal: 24, vertical: 8),
));
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<bool>('isInAsyncCall', isInAsyncCall));
}
}

View file

@ -1,29 +0,0 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import '../../../packages/veilid_support/veilid_support.dart';
part 'connection_state.freezed.dart';
@freezed
class ConnectionState with _$ConnectionState {
const factory ConnectionState({
required VeilidStateAttachment attachment,
}) = _ConnectionState;
const ConnectionState._();
bool get isAttached => !(attachment.state == AttachmentState.detached ||
attachment.state == AttachmentState.detaching ||
attachment.state == AttachmentState.attaching);
bool get isPublicInternetReady => attachment.publicInternetReady;
}
final connectionState = StateController<ConnectionState>(const ConnectionState(
attachment: VeilidStateAttachment(
state: AttachmentState.detached,
publicInternetReady: false,
localNetworkReady: false)));
final connectionStateProvider =
StateNotifierProvider<StateController<ConnectionState>, ConnectionState>(
(ref) => connectionState);

View file

@ -1,138 +0,0 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'connection_state.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods');
/// @nodoc
mixin _$ConnectionState {
VeilidStateAttachment get attachment => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$ConnectionStateCopyWith<ConnectionState> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $ConnectionStateCopyWith<$Res> {
factory $ConnectionStateCopyWith(
ConnectionState value, $Res Function(ConnectionState) then) =
_$ConnectionStateCopyWithImpl<$Res, ConnectionState>;
@useResult
$Res call({VeilidStateAttachment attachment});
}
/// @nodoc
class _$ConnectionStateCopyWithImpl<$Res, $Val extends ConnectionState>
implements $ConnectionStateCopyWith<$Res> {
_$ConnectionStateCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? attachment = freezed,
}) {
return _then(_value.copyWith(
attachment: freezed == attachment
? _value.attachment
: attachment // ignore: cast_nullable_to_non_nullable
as VeilidStateAttachment,
) as $Val);
}
}
/// @nodoc
abstract class _$$ConnectionStateImplCopyWith<$Res>
implements $ConnectionStateCopyWith<$Res> {
factory _$$ConnectionStateImplCopyWith(_$ConnectionStateImpl value,
$Res Function(_$ConnectionStateImpl) then) =
__$$ConnectionStateImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({VeilidStateAttachment attachment});
}
/// @nodoc
class __$$ConnectionStateImplCopyWithImpl<$Res>
extends _$ConnectionStateCopyWithImpl<$Res, _$ConnectionStateImpl>
implements _$$ConnectionStateImplCopyWith<$Res> {
__$$ConnectionStateImplCopyWithImpl(
_$ConnectionStateImpl _value, $Res Function(_$ConnectionStateImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? attachment = freezed,
}) {
return _then(_$ConnectionStateImpl(
attachment: freezed == attachment
? _value.attachment
: attachment // ignore: cast_nullable_to_non_nullable
as VeilidStateAttachment,
));
}
}
/// @nodoc
class _$ConnectionStateImpl extends _ConnectionState {
const _$ConnectionStateImpl({required this.attachment}) : super._();
@override
final VeilidStateAttachment attachment;
@override
String toString() {
return 'ConnectionState(attachment: $attachment)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$ConnectionStateImpl &&
const DeepCollectionEquality()
.equals(other.attachment, attachment));
}
@override
int get hashCode =>
Object.hash(runtimeType, const DeepCollectionEquality().hash(attachment));
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$ConnectionStateImplCopyWith<_$ConnectionStateImpl> get copyWith =>
__$$ConnectionStateImplCopyWithImpl<_$ConnectionStateImpl>(
this, _$identity);
}
abstract class _ConnectionState extends ConnectionState {
const factory _ConnectionState(
{required final VeilidStateAttachment attachment}) =
_$ConnectionStateImpl;
const _ConnectionState._() : super._();
@override
VeilidStateAttachment get attachment;
@override
@JsonKey(ignore: true)
_$$ConnectionStateImplCopyWith<_$ConnectionStateImpl> get copyWith =>
throw _privateConstructorUsedError;
}