refactor bottom app abr title

This commit is contained in:
Fabian Freund
2025-05-19 19:42:42 +02:00
parent 9b88e6d213
commit 4fe6f9663a
2 changed files with 419 additions and 432 deletions
@@ -1,108 +1,97 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart'; import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:skeletonizer/skeletonizer.dart'; import 'package:skeletonizer/skeletonizer.dart';
import 'package:text_scroll/text_scroll.dart'; import 'package:text_scroll/text_scroll.dart';
import 'package:weblibre/features/geckoview/domain/entities/states/tab.dart'; import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_icon.dart'; import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_icon.dart';
class AppBarTitle extends StatelessWidget { class AppBarTitle extends HookConsumerWidget {
final TabState tab; const AppBarTitle({super.key});
final VoidCallback? onTap;
final VoidCallback? onLongPress;
final GestureDragStartCallback? onHorizontalDragStart;
final GestureDragEndCallback? onHorizontalDragEnd;
const AppBarTitle({
required this.tab,
this.onTap,
this.onLongPress,
this.onHorizontalDragStart,
this.onHorizontalDragEnd,
super.key,
});
Icon _securityStatusIcon(BuildContext context) {
if (tab.url.isScheme('http')) {
return Icon(
MdiIcons.lockOff,
color: Theme.of(context).colorScheme.error,
size: 14,
);
} else if (!tab.securityInfoState.secure) {
return Icon(
MdiIcons.lockAlert,
color: Theme.of(context).colorScheme.errorContainer,
size: 14,
);
} else if (!tab.isLoading) {
return const Icon(MdiIcons.lock, size: 14);
} else {
return const Icon(MdiIcons.timerSand, size: 14);
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, WidgetRef ref) {
final theme = Theme.of(context); final theme = Theme.of(context);
return GestureDetector(
onTap: onTap, final tabState = ref.watch(selectedTabStateProvider);
onLongPress: onLongPress,
onHorizontalDragStart: onHorizontalDragStart, if (tabState == null) {
onHorizontalDragEnd: onHorizontalDragEnd, return const SizedBox.shrink();
child: Row( }
children: [
TabIcon(state: tab), final icon = useMemoized(() {
const SizedBox(width: 8), if (tabState.url.isScheme('http')) {
Expanded( return Icon(
child: Column( MdiIcons.lockOff,
crossAxisAlignment: CrossAxisAlignment.start, color: Theme.of(context).colorScheme.error,
mainAxisSize: MainAxisSize.min, size: 14,
children: [ );
Skeletonizer( } else if (!tabState.securityInfoState.secure) {
enabled: tab.title.isEmpty, return Icon(
child: Skeleton.replace( MdiIcons.lockAlert,
replacement: const Padding( color: Theme.of(context).colorScheme.errorContainer,
padding: EdgeInsets.only(right: 4, top: 1, bottom: 1), size: 14,
child: Bone.text(), );
), } else if (!tabState.isLoading) {
child: TextScroll( return const Icon(MdiIcons.lock, size: 14);
key: ValueKey(tab.title), } else {
tab.title, return const Icon(MdiIcons.timerSand, size: 14);
style: theme.textTheme.bodyLarge?.copyWith( }
color: theme.colorScheme.onSurface, }, [tabState]);
),
// mode: TextScrollMode.bouncing, return Row(
velocity: const Velocity(pixelsPerSecond: Offset(75, 0)), children: [
delayBefore: const Duration(milliseconds: 500), TabIcon(state: tabState),
pauseBetween: const Duration(milliseconds: 5000), const SizedBox(width: 8),
fadedBorder: true, Expanded(
fadeBorderSide: FadeBorderSide.right, child: Column(
fadedBorderWidth: 0.05, crossAxisAlignment: CrossAxisAlignment.start,
intervalSpaces: 4, mainAxisSize: MainAxisSize.min,
numberOfReps: 2, children: [
Skeletonizer(
enabled: tabState.title.isEmpty,
child: Skeleton.replace(
replacement: const Padding(
padding: EdgeInsets.only(right: 4, top: 1, bottom: 1),
child: Bone.text(),
),
child: TextScroll(
key: ValueKey(tabState.title),
tabState.title,
style: theme.textTheme.bodyLarge?.copyWith(
color: theme.colorScheme.onSurface,
), ),
// mode: TextScrollMode.bouncing,
velocity: const Velocity(pixelsPerSecond: Offset(75, 0)),
delayBefore: const Duration(milliseconds: 500),
pauseBetween: const Duration(milliseconds: 5000),
fadedBorder: true,
fadeBorderSide: FadeBorderSide.right,
fadedBorderWidth: 0.05,
intervalSpaces: 4,
numberOfReps: 2,
), ),
), ),
Row( ),
children: [ Row(
_securityStatusIcon(context), children: [
const SizedBox(width: 4), icon,
Expanded( const SizedBox(width: 4),
child: Text( Expanded(
tab.url.authority, child: Text(
style: theme.textTheme.bodyMedium?.copyWith( tabState.url.authority,
color: theme.colorScheme.onSurface, style: theme.textTheme.bodyMedium?.copyWith(
), color: theme.colorScheme.onSurface,
), ),
), ),
], ),
), ],
], ),
), ],
), ),
], ),
), ],
); );
} }
} }
@@ -41,375 +41,373 @@ class BrowserBottomAppBar extends HookConsumerWidget {
selectedTabStateProvider.select((state) => state?.isPrivate ?? false), selectedTabStateProvider.select((state) => state?.isPrivate ?? false),
); );
final dragStartPosition = useRef(Offset.zero);
return BottomAppBar( return BottomAppBar(
height: AppBar().preferredSize.height, height: AppBar().preferredSize.height,
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
child: AppBar( child: GestureDetector(
automaticallyImplyLeading: false, onTap: () async {
titleSpacing: 8.0, final tabState = ref.read(selectedTabStateProvider);
backgroundColor:
(isPrivateTab && displayedSheet is! ViewTabsSheet)
? const Color(0x648000D7)
: null,
title:
(selectedTabId != null && displayedSheet is! ViewTabsSheet)
? HookConsumer(
builder: (context, ref, child) {
final tabState = ref.watch(selectedTabStateProvider);
final dragStartPosition = useRef(Offset.zero); if (tabState != null) {
await WebPageRoute(
url: tabState.url.toString(),
$extra: tabState,
).push(context);
}
},
onLongPress: () async {
final tabState = ref.read(selectedTabStateProvider);
return (tabState != null) if (tabState != null) {
? AppBarTitle( final newUrl = await showDialog<Uri?>(
tab: tabState, context: context,
onTap: () async { builder: (context) => EditUrlDialog(initialUrl: tabState.url),
await WebPageRoute( );
url: tabState.url.toString(),
$extra: tabState,
).push(context);
},
onLongPress: () async {
final newUrl = await showDialog<Uri?>(
context: context,
builder:
(context) =>
EditUrlDialog(initialUrl: tabState.url),
);
if (newUrl != null) { if (newUrl != null) {
await ref await ref
.read( .read(tabSessionProvider(tabId: null).notifier)
tabSessionProvider(tabId: null).notifier, .loadUrl(url: newUrl);
) }
.loadUrl(url: newUrl); }
} },
}, onHorizontalDragStart: (details) {
onHorizontalDragStart: (details) { dragStartPosition.value = details.globalPosition;
dragStartPosition.value = details.globalPosition; },
}, onHorizontalDragEnd: (details) async {
onHorizontalDragEnd: (details) async { final distance = dragStartPosition.value - details.globalPosition;
final distance =
dragStartPosition.value -
details.globalPosition;
if (distance.dx.abs() > 50) { if (distance.dx.abs() > 50) {
await ref await ref.read(tabRepositoryProvider.notifier).selectPreviousTab();
.read(tabRepositoryProvider.notifier) }
.selectPreviousTab(); },
} child: AppBar(
}, automaticallyImplyLeading: false,
) titleSpacing: 8.0,
: const SizedBox.shrink(); backgroundColor:
}, (isPrivateTab && displayedSheet is! ViewTabsSheet)
) ? const Color(0x648000D7)
: null, : null,
actions: [ title:
if (selectedTabId != null && displayedSheet is! ViewTabsSheet) (selectedTabId != null && displayedSheet is! ViewTabsSheet)
ReaderButton(), ? const AppBarTitle()
MenuAnchor( : null,
controller: tabMenuController, actions: [
builder: (context, controller, child) { if (selectedTabId != null && displayedSheet is! ViewTabsSheet)
return child!; ReaderButton(),
}, MenuAnchor(
menuChildren: [ controller: tabMenuController,
if (selectedTabId != null) ...[ builder: (context, controller, child) {
MenuItemButton( return child!;
onPressed: () async {
await ref
.read(tabRepositoryProvider.notifier)
.closeTab(selectedTabId);
},
leadingIcon: const Icon(Icons.close),
child: const Text('Close Tab'),
),
const Divider(),
],
MenuItemButton(
onPressed: () async {
await const SearchRoute(
tabType: TabType.private,
).push(context);
},
leadingIcon: const Icon(MdiIcons.tabUnselected),
child: const Text('Add Private Tab'),
),
MenuItemButton(
onPressed: () async {
await const SearchRoute(
tabType: TabType.regular,
).push(context);
},
leadingIcon: const Icon(MdiIcons.tabPlus),
child: const Text('Add Tab'),
),
],
child: TabsActionButton(
isActive: displayedSheet is ViewTabsSheet,
onTap: () {
if (displayedSheet case ViewTabsSheet()) {
ref.read(bottomSheetControllerProvider.notifier).dismiss();
} else {
ref
.read(bottomSheetControllerProvider.notifier)
.show(ViewTabsSheet());
}
}, },
onLongPress: () { menuChildren: [
if (tabMenuController.isOpen) { if (selectedTabId != null) ...[
tabMenuController.close(); MenuItemButton(
} else { onPressed: () async {
tabMenuController.open(); await ref
} .read(tabRepositoryProvider.notifier)
}, .closeTab(selectedTabId);
), },
), leadingIcon: const Icon(Icons.close),
MenuAnchor( child: const Text('Close Tab'),
controller: trippleDotMenuController,
builder: (context, controller, child) {
return Padding(
padding: const EdgeInsets.only(right: 4.0),
child: InkWell(
onTap: () {
if (controller.isOpen) {
controller.close();
} else {
controller.open();
}
},
child: const Padding(
padding: EdgeInsets.symmetric(
vertical: 15.0,
horizontal: 8.0,
),
child: Icon(Icons.more_vert),
), ),
), const Divider(),
); ],
},
menuChildren: [
Consumer(
builder: (context, childRef, child) {
final pageExtensions = childRef.watch(
webExtensionsStateProvider(
WebExtensionActionType.page,
).select((value) => value.values.toList()),
);
return Wrap(
alignment: WrapAlignment.center,
children: [
...pageExtensions.map(
(extension) => IconButton(
onPressed: () async {
//Use parents .ref because after onPressed this consumer gets disposed already
await addonService.invokeAddonAction(
extension.extensionId,
WebExtensionActionType.page,
);
},
icon: ExtensionBadgeIcon(extension),
),
),
],
);
},
),
MenuItemButton(
onPressed: () async {
await AboutRoute().push(context);
},
leadingIcon: const Icon(Icons.info),
child: const Text('About'),
),
const Divider(),
MenuItemButton(
onPressed: () async {
await SettingsRoute().push(context);
},
leadingIcon: const Icon(Icons.settings),
child: const Text('Settings'),
),
Consumer(
builder: (context, ref, child) {
final browserExtensions = ref.watch(
webExtensionsStateProvider(
WebExtensionActionType.browser,
).select((value) => value.values.toList()),
);
return SubmenuButton(
menuChildren: [
...browserExtensions.map(
(extension) => MenuItemButton(
onPressed: () async {
//Use parents .ref because after onPressed this consumer gets disposed already
await addonService.invokeAddonAction(
extension.extensionId,
WebExtensionActionType.browser,
);
},
leadingIcon: ExtensionBadgeIcon(extension),
child: Text(extension.title ?? ''),
),
),
MenuItemButton(
onPressed: () async {
await addonService.startAddonManagerActivity();
},
leadingIcon: const Icon(MdiIcons.puzzleEdit),
child: const Text('Addon Manager'),
),
],
leadingIcon: const Icon(MdiIcons.puzzle),
child: const Text('Addons'),
);
},
),
MenuItemButton(
onPressed: () async {
await TorProxyRoute().push(context);
},
leadingIcon: const Icon(TorIcons.onionAlt),
child: const Text('Tor'),
),
const Divider(),
MenuItemButton(
onPressed: () async {
await BangCategoriesRoute().push(context);
},
leadingIcon: const Icon(MdiIcons.exclamationThick),
child: const Text('Bangs'),
),
MenuItemButton(
onPressed: () async {
await context.push(FeedListRoute().location);
},
leadingIcon: const Icon(Icons.rss_feed),
child: const Text('Feeds'),
),
const Divider(),
if (selectedTabId != null)
MenuItemButton( MenuItemButton(
onPressed: () async { onPressed: () async {
final tabState = ref.read(tabStateProvider(selectedTabId)); await const SearchRoute(
tabType: TabType.private,
if (tabState?.url case final Uri url) { ).push(context);
await ui_helper.launchUrlFeedback(context, url);
}
}, },
leadingIcon: const Icon(Icons.open_in_browser), leadingIcon: const Icon(MdiIcons.tabUnselected),
child: const Text('Launch External'), child: const Text('Add Private Tab'),
), ),
if (selectedTabId != null)
MenuItemButton( MenuItemButton(
onPressed: () async { onPressed: () async {
final tabState = ref.read(tabStateProvider(selectedTabId)); await const SearchRoute(
tabType: TabType.regular,
if (tabState?.url case final Uri url) { ).push(context);
await SharePlus.instance.share(ShareParams(uri: url));
}
}, },
leadingIcon: const Icon(Icons.share), leadingIcon: const Icon(MdiIcons.tabPlus),
child: const Text('Share'), child: const Text('Add Tab'),
), ),
if (selectedTabId != null) const Divider(), ],
if (selectedTabId != null) child: TabsActionButton(
MenuItemButton( isActive: displayedSheet is ViewTabsSheet,
onPressed: () { onTap: () {
ref.read(findInPageControllerProvider.notifier).show(); if (displayedSheet case ViewTabsSheet()) {
}, ref.read(bottomSheetControllerProvider.notifier).dismiss();
leadingIcon: const Icon(Icons.search), } else {
child: const Text('Find in page'), ref
), .read(bottomSheetControllerProvider.notifier)
if (selectedTabId != null) const Divider(), .show(ViewTabsSheet());
if (selectedTabId != null) }
MenuItemButton( },
onPressed: () async { onLongPress: () {
final controller = ref.read( if (tabMenuController.isOpen) {
tabSessionProvider(tabId: selectedTabId).notifier, tabMenuController.close();
); } else {
tabMenuController.open();
await controller.reload(); }
trippleDotMenuController.close(); },
}, ),
leadingIcon: const Icon(Icons.refresh), ),
child: const Text('Reload'), MenuAnchor(
), controller: trippleDotMenuController,
if (selectedTabId != null) const Divider(), builder: (context, controller, child) {
if (selectedTabId != null) return Padding(
padding: const EdgeInsets.only(right: 4.0),
child: InkWell(
onTap: () {
if (controller.isOpen) {
controller.close();
} else {
controller.open();
}
},
child: const Padding(
padding: EdgeInsets.symmetric(
vertical: 15.0,
horizontal: 8.0,
),
child: Icon(Icons.more_vert),
),
),
);
},
menuChildren: [
Consumer( Consumer(
builder: (context, ref, child) { builder: (context, childRef, child) {
final history = ref.watch( final pageExtensions = childRef.watch(
tabStateProvider( webExtensionsStateProvider(
selectedTabId, WebExtensionActionType.page,
).select((value) => value?.historyState), ).select((value) => value.values.toList()),
); );
final isLoading = ref.watch( return Wrap(
selectedTabStateProvider.select( alignment: WrapAlignment.center,
(state) => state?.isLoading ?? false,
),
);
return Row(
children: [ children: [
Expanded( ...pageExtensions.map(
child: (extension) => IconButton(
(history?.canGoBack == true || isLoading) onPressed: () async {
? IconButton( //Use parents .ref because after onPressed this consumer gets disposed already
onPressed: () async { await addonService.invokeAddonAction(
final controller = ref.read( extension.extensionId,
tabSessionProvider( WebExtensionActionType.page,
tabId: selectedTabId, );
).notifier, },
); icon: ExtensionBadgeIcon(extension),
if (isLoading) {
await controller.stopLoading();
} else {
await controller.goBack();
}
trippleDotMenuController.close();
},
icon: const Icon(Icons.arrow_back),
)
: IconButton(
onPressed: () async {
await ref
.read(tabRepositoryProvider.notifier)
.closeTab(selectedTabId);
trippleDotMenuController.close();
},
icon: const Icon(Icons.close),
),
),
const SizedBox(height: 48, child: VerticalDivider()),
Expanded(
child: IconButton(
onPressed:
(history?.canGoForward == true)
? () async {
final controller = ref.read(
tabSessionProvider(
tabId: selectedTabId,
).notifier,
);
await controller.goForward();
trippleDotMenuController.close();
}
: null,
icon: const Icon(Icons.arrow_forward),
), ),
), ),
], ],
); );
}, },
), ),
], MenuItemButton(
), onPressed: () async {
], await AboutRoute().push(context);
},
leadingIcon: const Icon(Icons.info),
child: const Text('About'),
),
const Divider(),
MenuItemButton(
onPressed: () async {
await SettingsRoute().push(context);
},
leadingIcon: const Icon(Icons.settings),
child: const Text('Settings'),
),
Consumer(
builder: (context, ref, child) {
final browserExtensions = ref.watch(
webExtensionsStateProvider(
WebExtensionActionType.browser,
).select((value) => value.values.toList()),
);
return SubmenuButton(
menuChildren: [
...browserExtensions.map(
(extension) => MenuItemButton(
onPressed: () async {
//Use parents .ref because after onPressed this consumer gets disposed already
await addonService.invokeAddonAction(
extension.extensionId,
WebExtensionActionType.browser,
);
},
leadingIcon: ExtensionBadgeIcon(extension),
child: Text(extension.title ?? ''),
),
),
MenuItemButton(
onPressed: () async {
await addonService.startAddonManagerActivity();
},
leadingIcon: const Icon(MdiIcons.puzzleEdit),
child: const Text('Addon Manager'),
),
],
leadingIcon: const Icon(MdiIcons.puzzle),
child: const Text('Addons'),
);
},
),
MenuItemButton(
onPressed: () async {
await TorProxyRoute().push(context);
},
leadingIcon: const Icon(TorIcons.onionAlt),
child: const Text('Tor'),
),
const Divider(),
MenuItemButton(
onPressed: () async {
await BangCategoriesRoute().push(context);
},
leadingIcon: const Icon(MdiIcons.exclamationThick),
child: const Text('Bangs'),
),
MenuItemButton(
onPressed: () async {
await context.push(FeedListRoute().location);
},
leadingIcon: const Icon(Icons.rss_feed),
child: const Text('Feeds'),
),
const Divider(),
if (selectedTabId != null)
MenuItemButton(
onPressed: () async {
final tabState = ref.read(
tabStateProvider(selectedTabId),
);
if (tabState?.url case final Uri url) {
await ui_helper.launchUrlFeedback(context, url);
}
},
leadingIcon: const Icon(Icons.open_in_browser),
child: const Text('Launch External'),
),
if (selectedTabId != null)
MenuItemButton(
onPressed: () async {
final tabState = ref.read(
tabStateProvider(selectedTabId),
);
if (tabState?.url case final Uri url) {
await SharePlus.instance.share(ShareParams(uri: url));
}
},
leadingIcon: const Icon(Icons.share),
child: const Text('Share'),
),
if (selectedTabId != null) const Divider(),
if (selectedTabId != null)
MenuItemButton(
onPressed: () {
ref.read(findInPageControllerProvider.notifier).show();
},
leadingIcon: const Icon(Icons.search),
child: const Text('Find in page'),
),
if (selectedTabId != null) const Divider(),
if (selectedTabId != null)
MenuItemButton(
onPressed: () async {
final controller = ref.read(
tabSessionProvider(tabId: selectedTabId).notifier,
);
await controller.reload();
trippleDotMenuController.close();
},
leadingIcon: const Icon(Icons.refresh),
child: const Text('Reload'),
),
if (selectedTabId != null) const Divider(),
if (selectedTabId != null)
Consumer(
builder: (context, ref, child) {
final history = ref.watch(
tabStateProvider(
selectedTabId,
).select((value) => value?.historyState),
);
final isLoading = ref.watch(
selectedTabStateProvider.select(
(state) => state?.isLoading ?? false,
),
);
return Row(
children: [
Expanded(
child:
(history?.canGoBack == true || isLoading)
? IconButton(
onPressed: () async {
final controller = ref.read(
tabSessionProvider(
tabId: selectedTabId,
).notifier,
);
if (isLoading) {
await controller.stopLoading();
} else {
await controller.goBack();
}
trippleDotMenuController.close();
},
icon: const Icon(Icons.arrow_back),
)
: IconButton(
onPressed: () async {
await ref
.read(
tabRepositoryProvider.notifier,
)
.closeTab(selectedTabId);
trippleDotMenuController.close();
},
icon: const Icon(Icons.close),
),
),
const SizedBox(height: 48, child: VerticalDivider()),
Expanded(
child: IconButton(
onPressed:
(history?.canGoForward == true)
? () async {
final controller = ref.read(
tabSessionProvider(
tabId: selectedTabId,
).notifier,
);
await controller.goForward();
trippleDotMenuController.close();
}
: null,
icon: const Icon(Icons.arrow_forward),
),
),
],
);
},
),
],
),
],
),
), ),
); );
} }