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_hooks/flutter_hooks.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: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';
class AppBarTitle extends StatelessWidget {
final TabState tab;
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);
}
}
class AppBarTitle extends HookConsumerWidget {
const AppBarTitle({super.key});
@override
Widget build(BuildContext context) {
Widget build(BuildContext context, WidgetRef ref) {
final theme = Theme.of(context);
return GestureDetector(
onTap: onTap,
onLongPress: onLongPress,
onHorizontalDragStart: onHorizontalDragStart,
onHorizontalDragEnd: onHorizontalDragEnd,
child: Row(
children: [
TabIcon(state: tab),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Skeletonizer(
enabled: tab.title.isEmpty,
child: Skeleton.replace(
replacement: const Padding(
padding: EdgeInsets.only(right: 4, top: 1, bottom: 1),
child: Bone.text(),
),
child: TextScroll(
key: ValueKey(tab.title),
tab.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,
final tabState = ref.watch(selectedTabStateProvider);
if (tabState == null) {
return const SizedBox.shrink();
}
final icon = useMemoized(() {
if (tabState.url.isScheme('http')) {
return Icon(
MdiIcons.lockOff,
color: Theme.of(context).colorScheme.error,
size: 14,
);
} else if (!tabState.securityInfoState.secure) {
return Icon(
MdiIcons.lockAlert,
color: Theme.of(context).colorScheme.errorContainer,
size: 14,
);
} else if (!tabState.isLoading) {
return const Icon(MdiIcons.lock, size: 14);
} else {
return const Icon(MdiIcons.timerSand, size: 14);
}
}, [tabState]);
return Row(
children: [
TabIcon(state: tabState),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
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: [
_securityStatusIcon(context),
const SizedBox(width: 4),
Expanded(
child: Text(
tab.url.authority,
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurface,
),
),
Row(
children: [
icon,
const SizedBox(width: 4),
Expanded(
child: Text(
tabState.url.authority,
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurface,
),
),
],
),
],
),
),
],
),
],
),
],
),
),
],
);
}
}
@@ -41,375 +41,373 @@ class BrowserBottomAppBar extends HookConsumerWidget {
selectedTabStateProvider.select((state) => state?.isPrivate ?? false),
);
final dragStartPosition = useRef(Offset.zero);
return BottomAppBar(
height: AppBar().preferredSize.height,
padding: EdgeInsets.zero,
child: AppBar(
automaticallyImplyLeading: false,
titleSpacing: 8.0,
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);
child: GestureDetector(
onTap: () async {
final tabState = ref.read(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)
? AppBarTitle(
tab: tabState,
onTap: () async {
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 (tabState != null) {
final newUrl = await showDialog<Uri?>(
context: context,
builder: (context) => EditUrlDialog(initialUrl: tabState.url),
);
if (newUrl != null) {
await ref
.read(
tabSessionProvider(tabId: null).notifier,
)
.loadUrl(url: newUrl);
}
},
onHorizontalDragStart: (details) {
dragStartPosition.value = details.globalPosition;
},
onHorizontalDragEnd: (details) async {
final distance =
dragStartPosition.value -
details.globalPosition;
if (newUrl != null) {
await ref
.read(tabSessionProvider(tabId: null).notifier)
.loadUrl(url: newUrl);
}
}
},
onHorizontalDragStart: (details) {
dragStartPosition.value = details.globalPosition;
},
onHorizontalDragEnd: (details) async {
final distance = dragStartPosition.value - details.globalPosition;
if (distance.dx.abs() > 50) {
await ref
.read(tabRepositoryProvider.notifier)
.selectPreviousTab();
}
},
)
: const SizedBox.shrink();
},
)
: null,
actions: [
if (selectedTabId != null && displayedSheet is! ViewTabsSheet)
ReaderButton(),
MenuAnchor(
controller: tabMenuController,
builder: (context, controller, child) {
return child!;
},
menuChildren: [
if (selectedTabId != null) ...[
MenuItemButton(
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());
}
if (distance.dx.abs() > 50) {
await ref.read(tabRepositoryProvider.notifier).selectPreviousTab();
}
},
child: AppBar(
automaticallyImplyLeading: false,
titleSpacing: 8.0,
backgroundColor:
(isPrivateTab && displayedSheet is! ViewTabsSheet)
? const Color(0x648000D7)
: null,
title:
(selectedTabId != null && displayedSheet is! ViewTabsSheet)
? const AppBarTitle()
: null,
actions: [
if (selectedTabId != null && displayedSheet is! ViewTabsSheet)
ReaderButton(),
MenuAnchor(
controller: tabMenuController,
builder: (context, controller, child) {
return child!;
},
onLongPress: () {
if (tabMenuController.isOpen) {
tabMenuController.close();
} else {
tabMenuController.open();
}
},
),
),
MenuAnchor(
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),
menuChildren: [
if (selectedTabId != null) ...[
MenuItemButton(
onPressed: () async {
await ref
.read(tabRepositoryProvider.notifier)
.closeTab(selectedTabId);
},
leadingIcon: const Icon(Icons.close),
child: const Text('Close Tab'),
),
),
);
},
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)
const Divider(),
],
MenuItemButton(
onPressed: () async {
final tabState = ref.read(tabStateProvider(selectedTabId));
if (tabState?.url case final Uri url) {
await ui_helper.launchUrlFeedback(context, url);
}
await const SearchRoute(
tabType: TabType.private,
).push(context);
},
leadingIcon: const Icon(Icons.open_in_browser),
child: const Text('Launch External'),
leadingIcon: const Icon(MdiIcons.tabUnselected),
child: const Text('Add Private Tab'),
),
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));
}
await const SearchRoute(
tabType: TabType.regular,
).push(context);
},
leadingIcon: const Icon(Icons.share),
child: const Text('Share'),
leadingIcon: const Icon(MdiIcons.tabPlus),
child: const Text('Add Tab'),
),
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)
],
child: TabsActionButton(
isActive: displayedSheet is ViewTabsSheet,
onTap: () {
if (displayedSheet case ViewTabsSheet()) {
ref.read(bottomSheetControllerProvider.notifier).dismiss();
} else {
ref
.read(bottomSheetControllerProvider.notifier)
.show(ViewTabsSheet());
}
},
onLongPress: () {
if (tabMenuController.isOpen) {
tabMenuController.close();
} else {
tabMenuController.open();
}
},
),
),
MenuAnchor(
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),
),
),
);
},
menuChildren: [
Consumer(
builder: (context, ref, child) {
final history = ref.watch(
tabStateProvider(
selectedTabId,
).select((value) => value?.historyState),
builder: (context, childRef, child) {
final pageExtensions = childRef.watch(
webExtensionsStateProvider(
WebExtensionActionType.page,
).select((value) => value.values.toList()),
);
final isLoading = ref.watch(
selectedTabStateProvider.select(
(state) => state?.isLoading ?? false,
),
);
return Row(
return Wrap(
alignment: WrapAlignment.center,
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),
...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(
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),
),
),
],
);
},
),
],
),
],
),
),
);
}