decouple app bar related widgets from logic

This commit is contained in:
Fabian Freund
2026-03-05 07:54:45 +01:00
parent c3e39b3a1d
commit 1fc8cc5fab
4 changed files with 721 additions and 457 deletions
@@ -18,7 +18,6 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
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:hooks_riverpod/hooks_riverpod.dart';
import 'package:skeletonizer/skeletonizer.dart'; import 'package:skeletonizer/skeletonizer.dart';
@@ -27,6 +26,7 @@ import 'package:weblibre/core/design/app_colors.dart';
import 'package:weblibre/core/routing/routes.dart'; import 'package:weblibre/core/routing/routes.dart';
import 'package:weblibre/extensions/uri.dart'; import 'package:weblibre/extensions/uri.dart';
import 'package:weblibre/features/geckoview/domain/controllers/bottom_sheet.dart'; import 'package:weblibre/features/geckoview/domain/controllers/bottom_sheet.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/domain/providers/tab_state.dart';
import 'package:weblibre/features/geckoview/features/browser/domain/entities/sheet.dart'; import 'package:weblibre/features/geckoview/features/browser/domain/entities/sheet.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/providers/site_settings_badge_provider.dart'; import 'package:weblibre/features/geckoview/features/browser/presentation/providers/site_settings_badge_provider.dart';
@@ -35,14 +35,11 @@ import 'package:weblibre/features/geckoview/features/browser/presentation/widget
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_mode.dart'; import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_mode.dart';
import 'package:weblibre/presentation/widgets/uri_breadcrumb.dart'; import 'package:weblibre/presentation/widgets/uri_breadcrumb.dart';
class CompactAppBarTitle extends HookConsumerWidget { class CompactAppBarTitle extends ConsumerWidget {
const CompactAppBarTitle({super.key}); const CompactAppBarTitle({super.key});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final theme = Theme.of(context);
final appColors = AppColors.of(context);
final tabState = ref.watch(selectedTabStateProvider); final tabState = ref.watch(selectedTabStateProvider);
final isTabTuneledAsync = ref.watch(isTabTunneledProvider(tabState?.id)); final isTabTuneledAsync = ref.watch(isTabTunneledProvider(tabState?.id));
final showSiteSettingsBadge = ref.watch( final showSiteSettingsBadge = ref.watch(
@@ -53,42 +50,60 @@ class CompactAppBarTitle extends HookConsumerWidget {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
final icon = useMemoized(() { return CompactAppBarTitleView(
if (tabState.url.isHttp) { tabState: tabState,
return Icon( isTabTunneled:
MdiIcons.lockOff, isTabTuneledAsync.hasValue && isTabTuneledAsync.value == true,
color: Theme.of(context).colorScheme.error, showSiteSettingsBadge: showSiteSettingsBadge,
size: 16, onSiteSettingsTap: () {
); ref
} else if (tabState.readerableState.active) { .read(bottomSheetControllerProvider.notifier)
return const Icon(MdiIcons.lockMinus, size: 16); .show(SiteSettingsSheet(tabState: tabState));
} else if (!tabState.securityInfoState.secure) { },
return Icon( onTitleTap: () async {
MdiIcons.lockAlert, await SearchRoute(
color: Theme.of(context).colorScheme.errorContainer, tabId: tabState.id,
size: 16, searchText: _searchTextForTab(tabState),
); tabType: tabState.tabMode.toTabType(),
} else if (!tabState.isLoading) { ).push(context);
return const Icon(MdiIcons.lock, size: 16); },
} else { );
return const Icon(MdiIcons.timerSand, size: 16); }
} }
}, [tabState]);
class CompactAppBarTitleView extends StatelessWidget {
const CompactAppBarTitleView({
super.key,
required this.tabState,
required this.isTabTunneled,
required this.showSiteSettingsBadge,
required this.onSiteSettingsTap,
required this.onTitleTap,
this.tabIcon,
});
final TabState tabState;
final bool isTabTunneled;
final bool showSiteSettingsBadge;
final VoidCallback onSiteSettingsTap;
final VoidCallback onTitleTap;
final Widget? tabIcon;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final appColors = AppColors.of(context);
return Row( return Row(
children: [ children: [
ToolbarButton( ToolbarButton(
onTap: () { onTap: onSiteSettingsTap,
ref
.read(bottomSheetControllerProvider.notifier)
.show(SiteSettingsSheet(tabState: tabState));
},
child: Padding( child: Padding(
padding: const EdgeInsets.only(right: 4.0), padding: const EdgeInsets.only(right: 4.0),
child: Stack( child: Stack(
clipBehavior: Clip.none, clipBehavior: Clip.none,
children: [ children: [
TabIcon(tabState: tabState, iconSize: 24), tabIcon ?? TabIcon(tabState: tabState, iconSize: 24),
Positioned( Positioned(
top: -4, top: -4,
right: -4, right: -4,
@@ -106,19 +121,7 @@ class CompactAppBarTitle extends HookConsumerWidget {
), ),
Expanded( Expanded(
child: GestureDetector( child: GestureDetector(
onTap: () async { onTap: onTitleTap,
final searchText = tabState.url.scheme == 'about'
? ''
: tabState.url.toString();
await SearchRoute(
tabId: tabState.id,
searchText: searchText.isEmpty
? SearchRoute.emptySearchText
: searchText,
tabType: tabState.tabMode.toTabType(),
).push(context);
},
child: Container( child: Container(
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12), padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12),
decoration: BoxDecoration( decoration: BoxDecoration(
@@ -144,12 +147,11 @@ class CompactAppBarTitle extends HookConsumerWidget {
), ),
const SizedBox(width: 4), const SizedBox(width: 4),
], ],
if (isTabTuneledAsync.hasValue && if (isTabTunneled) ...[
isTabTuneledAsync.value == true) ...[
const Icon(MdiIcons.tunnelOutline, size: 16), const Icon(MdiIcons.tunnelOutline, size: 16),
const SizedBox(width: 4), const SizedBox(width: 4),
], ],
icon, _SecurityStatusIcon(tabState: tabState, size: 16),
const SizedBox(width: 6), const SizedBox(width: 6),
Flexible( Flexible(
child: UriBreadcrumb( child: UriBreadcrumb(
@@ -170,14 +172,11 @@ class CompactAppBarTitle extends HookConsumerWidget {
} }
} }
class AppBarTitle extends HookConsumerWidget { class AppBarTitle extends ConsumerWidget {
const AppBarTitle({super.key}); const AppBarTitle({super.key});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final theme = Theme.of(context);
final appColors = AppColors.of(context);
final tabState = ref.watch(selectedTabStateProvider); final tabState = ref.watch(selectedTabStateProvider);
final isTabTuneledAsync = ref.watch(isTabTunneledProvider(tabState?.id)); final isTabTuneledAsync = ref.watch(isTabTunneledProvider(tabState?.id));
final showSiteSettingsBadge = ref.watch( final showSiteSettingsBadge = ref.watch(
@@ -188,42 +187,60 @@ class AppBarTitle extends HookConsumerWidget {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
final icon = useMemoized(() { return AppBarTitleView(
if (tabState.url.isHttp) { tabState: tabState,
return Icon( isTabTunneled:
MdiIcons.lockOff, isTabTuneledAsync.hasValue && isTabTuneledAsync.value == true,
color: Theme.of(context).colorScheme.error, showSiteSettingsBadge: showSiteSettingsBadge,
size: 14, onSiteSettingsTap: () {
); ref
} else if (tabState.readerableState.active) { .read(bottomSheetControllerProvider.notifier)
return const Icon(MdiIcons.lockMinus, size: 14); .show(SiteSettingsSheet(tabState: tabState));
} else if (!tabState.securityInfoState.secure) { },
return Icon( onTitleTap: () async {
MdiIcons.lockAlert, await SearchRoute(
color: Theme.of(context).colorScheme.errorContainer, tabId: tabState.id,
size: 14, searchText: _searchTextForTab(tabState),
); tabType: tabState.tabMode.toTabType(),
} else if (!tabState.isLoading) { ).push(context);
return const Icon(MdiIcons.lock, size: 14); },
} else { );
return const Icon(MdiIcons.timerSand, size: 14); }
} }
}, [tabState]);
class AppBarTitleView extends StatelessWidget {
const AppBarTitleView({
super.key,
required this.tabState,
required this.isTabTunneled,
required this.showSiteSettingsBadge,
required this.onSiteSettingsTap,
required this.onTitleTap,
this.tabIcon,
});
final TabState tabState;
final bool isTabTunneled;
final bool showSiteSettingsBadge;
final VoidCallback onSiteSettingsTap;
final VoidCallback onTitleTap;
final Widget? tabIcon;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final appColors = AppColors.of(context);
return Row( return Row(
children: [ children: [
ToolbarButton( ToolbarButton(
onTap: () { onTap: onSiteSettingsTap,
ref
.read(bottomSheetControllerProvider.notifier)
.show(SiteSettingsSheet(tabState: tabState));
},
child: Padding( child: Padding(
padding: const EdgeInsets.only(right: 4.0), padding: const EdgeInsets.only(right: 4.0),
child: Stack( child: Stack(
clipBehavior: Clip.none, clipBehavior: Clip.none,
children: [ children: [
TabIcon(tabState: tabState, iconSize: 24), tabIcon ?? TabIcon(tabState: tabState, iconSize: 24),
Positioned( Positioned(
top: -4, top: -4,
right: -4, right: -4,
@@ -239,23 +256,9 @@ class AppBarTitle extends HookConsumerWidget {
), ),
), ),
), ),
// Title/URL tap → opens search screen
Expanded( Expanded(
child: GestureDetector( child: GestureDetector(
onTap: () async { onTap: onTitleTap,
// Don't pre-fill for internal URLs
final searchText = tabState.url.scheme == 'about'
? ''
: tabState.url.toString();
await SearchRoute(
tabId: tabState.id,
searchText: searchText.isEmpty
? SearchRoute.emptySearchText
: searchText,
tabType: tabState.tabMode.toTabType(),
).push(context);
},
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@@ -273,7 +276,6 @@ class AppBarTitle extends HookConsumerWidget {
style: theme.textTheme.bodyLarge?.copyWith( style: theme.textTheme.bodyLarge?.copyWith(
color: theme.colorScheme.onSurface, color: theme.colorScheme.onSurface,
), ),
// mode: TextScrollMode.bouncing,
velocity: const Velocity(pixelsPerSecond: Offset(75, 0)), velocity: const Velocity(pixelsPerSecond: Offset(75, 0)),
delayBefore: const Duration(milliseconds: 500), delayBefore: const Duration(milliseconds: 500),
pauseBetween: const Duration(milliseconds: 5000), pauseBetween: const Duration(milliseconds: 5000),
@@ -302,12 +304,11 @@ class AppBarTitle extends HookConsumerWidget {
), ),
const SizedBox(width: 4), const SizedBox(width: 4),
], ],
if (isTabTuneledAsync.hasValue && if (isTabTunneled) ...[
isTabTuneledAsync.value == true) ...[
const Icon(MdiIcons.tunnelOutline, size: 14), const Icon(MdiIcons.tunnelOutline, size: 14),
const SizedBox(width: 4), const SizedBox(width: 4),
], ],
icon, _SecurityStatusIcon(tabState: tabState, size: 14),
const SizedBox(width: 4), const SizedBox(width: 4),
Expanded( Expanded(
child: UriBreadcrumb( child: UriBreadcrumb(
@@ -328,3 +329,41 @@ class AppBarTitle extends HookConsumerWidget {
); );
} }
} }
class _SecurityStatusIcon extends StatelessWidget {
const _SecurityStatusIcon({required this.tabState, required this.size});
final TabState tabState;
final double size;
@override
Widget build(BuildContext context) {
if (tabState.url.isHttp) {
return Icon(
MdiIcons.lockOff,
color: Theme.of(context).colorScheme.error,
size: size,
);
} else if (tabState.readerableState.active) {
return Icon(MdiIcons.lockMinus, size: size);
} else if (!tabState.securityInfoState.secure) {
return Icon(
MdiIcons.lockAlert,
color: Theme.of(context).colorScheme.errorContainer,
size: size,
);
} else if (!tabState.isLoading) {
return Icon(MdiIcons.lock, size: size);
}
return Icon(MdiIcons.timerSand, size: size);
}
}
String _searchTextForTab(TabState tabState) {
final searchText = tabState.url.scheme == 'about'
? ''
: tabState.url.toString();
return searchText.isEmpty ? SearchRoute.emptySearchText : searchText;
}
@@ -27,12 +27,10 @@ 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:flutter_mozilla_components/flutter_mozilla_components.dart'; import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:nullability/nullability.dart';
import 'package:weblibre/core/design/app_colors.dart'; import 'package:weblibre/core/design/app_colors.dart';
import 'package:weblibre/core/routing/routes.dart'; import 'package:weblibre/core/routing/routes.dart';
import 'package:weblibre/features/geckoview/domain/controllers/bottom_sheet.dart'; import 'package:weblibre/features/geckoview/domain/controllers/bottom_sheet.dart';
import 'package:weblibre/features/geckoview/domain/entities/states/readerable.dart'; import 'package:weblibre/features/geckoview/domain/entities/states/readerable.dart';
import 'package:weblibre/features/geckoview/domain/entities/states/tab.dart';
import 'package:weblibre/features/geckoview/domain/providers/selected_tab.dart'; import 'package:weblibre/features/geckoview/domain/providers/selected_tab.dart';
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart'; import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
import 'package:weblibre/features/geckoview/domain/repositories/tab.dart'; import 'package:weblibre/features/geckoview/domain/repositories/tab.dart';
@@ -60,7 +58,7 @@ import 'package:weblibre/presentation/hooks/menu_controller.dart';
import 'package:weblibre/presentation/widgets/selectable_chips.dart'; import 'package:weblibre/presentation/widgets/selectable_chips.dart';
import 'package:weblibre/presentation/widgets/url_icon.dart'; import 'package:weblibre/presentation/widgets/url_icon.dart';
class BrowserTopAppBar extends HookConsumerWidget { class BrowserTopAppBar extends StatelessWidget {
final bool showMainToolbar; final bool showMainToolbar;
final bool showContextualToolbar; final bool showContextualToolbar;
final bool showQuickTabSwitcherBar; final bool showQuickTabSwitcherBar;
@@ -85,7 +83,7 @@ class BrowserTopAppBar extends HookConsumerWidget {
} }
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context) {
return SafeArea( return SafeArea(
child: SizedBox(height: preferredSize.height, child: _tabBar), child: SizedBox(height: preferredSize.height, child: _tabBar),
); );
@@ -94,7 +92,7 @@ class BrowserTopAppBar extends HookConsumerWidget {
Size get preferredSize => _size; Size get preferredSize => _size;
} }
class BrowserBottomAppBar extends HookConsumerWidget { class BrowserBottomAppBar extends StatelessWidget {
final bool showMainToolbar; final bool showMainToolbar;
final bool showContextualToolbar; final bool showContextualToolbar;
final bool showQuickTabSwitcherBar; final bool showQuickTabSwitcherBar;
@@ -121,7 +119,7 @@ class BrowserBottomAppBar extends HookConsumerWidget {
} }
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context) {
final bottomPadding = MediaQuery.of(context).padding.bottom; final bottomPadding = MediaQuery.of(context).padding.bottom;
return Material( return Material(
@@ -205,8 +203,97 @@ class BrowserTabBar extends HookConsumerWidget {
final dragStartPosition = useRef(Offset.zero); final dragStartPosition = useRef(Offset.zero);
return GestureDetector( final showTabTitle =
// Tap handling moved to AppBarTitle for split icon/title behavior selectedTabId != null && displayedSheet is! ViewTabsSheet;
final backgroundColor =
(settings.showContainerUi &&
containerColor != null &&
displayedSheet is! ViewTabsSheet)
? ContainerColors.forAppBar(containerColor)
: null;
return BrowserTabBarView(
showMainToolbar: showMainToolbar,
showContextualToolbar: showContextualToolbar,
showQuickTabSwitcherBar: showQuickTabSwitcherBar,
displayAppBar: displayAppBar,
displayQuickTabSwitcher: displayQuickTabSwitcher,
backgroundColor: backgroundColor,
title: showTabTitle
? settings.tabBarLayout == TabBarLayout.compact
? const CompactAppBarTitle()
: const AppBarTitle()
: null,
actions: [
if (showTabTitle)
Consumer(
builder: (context, ref, child) {
final tabBarReaderView = ref.watch(
generalSettingsWithDefaultsProvider.select(
(value) => value.tabBarReaderView,
),
);
final readerabilityStateActive = ref.watch(
selectedTabStateProvider.select(
(state) =>
(state?.readerableState ?? ReaderableState.$default())
.active,
),
);
return Visibility(
visible: tabBarReaderView || readerabilityStateActive,
child: ReaderButton(
buttonBuilder: (isLoading, readerActive, icon) =>
ToolbarButton(
onTap: isLoading
? null
: () async {
await ref
.read(
readerableScreenControllerProvider
.notifier,
)
.toggleReaderView(!readerActive);
},
child: icon,
),
),
);
},
),
if (showExtensionShortcut)
ExtensionShortcutMenu(
controller: extensionMenuController,
child: ToolbarButton(
onTap: () {
if (extensionMenuController.isOpen) {
extensionMenuController.close();
} else {
extensionMenuController.open();
}
},
child: const Icon(MdiIcons.puzzle),
),
),
if (showMainToolbarTabsCount)
TabsCountButton(
selectedTabId: selectedTabId,
displayedSheet: displayedSheet,
showLongPressMenu: true,
),
if (showMainToolbarNavigationButton)
NavigationMenuButton(selectedTabId: selectedTabId),
],
quickTabSwitcher: QuickTabSwitcher(
quickTabSwitcherMode: quickTabSwitcherMode,
),
contextualToolbar: ContextualToolbar(
selectedTabId: selectedTabId,
displayedSheet: displayedSheet,
),
onHorizontalDragStart: (details) { onHorizontalDragStart: (details) {
dragStartPosition.value = details.globalPosition; dragStartPosition.value = details.globalPosition;
}, },
@@ -262,6 +349,52 @@ class BrowserTabBar extends HookConsumerWidget {
.dismiss(); .dismiss();
} }
}, },
);
}
}
class BrowserTabBarView extends StatelessWidget {
const BrowserTabBarView({
super.key,
required this.showMainToolbar,
required this.showContextualToolbar,
required this.showQuickTabSwitcherBar,
required this.displayAppBar,
required this.displayQuickTabSwitcher,
required this.backgroundColor,
required this.title,
required this.actions,
required this.quickTabSwitcher,
required this.contextualToolbar,
this.onHorizontalDragStart,
this.onHorizontalDragEnd,
this.onVerticalDragStart,
this.onVerticalDragEnd,
});
final bool showMainToolbar;
final bool showContextualToolbar;
final bool showQuickTabSwitcherBar;
final bool displayAppBar;
final bool displayQuickTabSwitcher;
final Color? backgroundColor;
final Widget? title;
final List<Widget> actions;
final Widget quickTabSwitcher;
final Widget contextualToolbar;
final GestureDragStartCallback? onHorizontalDragStart;
final GestureDragEndCallback? onHorizontalDragEnd;
final GestureDragStartCallback? onVerticalDragStart;
final GestureDragEndCallback? onVerticalDragEnd;
@override
Widget build(BuildContext context) {
return GestureDetector(
// Tap handling moved to AppBarTitle for split icon/title behavior
onHorizontalDragStart: onHorizontalDragStart,
onHorizontalDragEnd: onHorizontalDragEnd,
onVerticalDragStart: onVerticalDragStart,
onVerticalDragEnd: onVerticalDragEnd,
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
@@ -269,9 +402,7 @@ class BrowserTabBar extends HookConsumerWidget {
Visibility( Visibility(
visible: displayQuickTabSwitcher, visible: displayQuickTabSwitcher,
maintainState: true, maintainState: true,
child: QuickTabSwitcher( child: quickTabSwitcher,
quickTabSwitcherMode: quickTabSwitcherMode,
),
), ),
if (showMainToolbar) if (showMainToolbar)
Visibility( Visibility(
@@ -283,95 +414,19 @@ class BrowserTabBar extends HookConsumerWidget {
titleSpacing: 0.0, titleSpacing: 0.0,
leadingWidth: 40.0, leadingWidth: 40.0,
toolbarHeight: kToolbarHeight, toolbarHeight: kToolbarHeight,
backgroundColor: backgroundColor: backgroundColor,
(settings.showContainerUi && title: title,
containerColor != null && actions: actions,
displayedSheet is! ViewTabsSheet)
? ContainerColors.forAppBar(containerColor)
: null,
title:
(selectedTabId != null && displayedSheet is! ViewTabsSheet)
? settings.tabBarLayout == TabBarLayout.compact
? const CompactAppBarTitle()
: const AppBarTitle()
: null,
actions: [
if (selectedTabId != null && displayedSheet is! ViewTabsSheet)
Consumer(
builder: (context, ref, child) {
final tabBarReaderView = ref.watch(
generalSettingsWithDefaultsProvider.select(
(value) => value.tabBarReaderView,
),
);
final readerabilityStateActive = ref.watch(
selectedTabStateProvider.select(
(state) =>
(state?.readerableState ??
ReaderableState.$default())
.active,
),
);
return Visibility(
visible: tabBarReaderView || readerabilityStateActive,
child: ReaderButton(
buttonBuilder: (isLoading, readerActive, icon) =>
ToolbarButton(
onTap: isLoading
? null
: () async {
await ref
.read(
readerableScreenControllerProvider
.notifier,
)
.toggleReaderView(!readerActive);
},
child: icon,
),
),
);
},
),
if (showExtensionShortcut)
ExtensionShortcutMenu(
controller: extensionMenuController,
child: ToolbarButton(
onTap: () {
if (extensionMenuController.isOpen) {
extensionMenuController.close();
} else {
extensionMenuController.open();
}
},
child: const Icon(MdiIcons.puzzle),
),
),
if (showMainToolbarTabsCount)
TabsCountButton(
selectedTabId: selectedTabId,
displayedSheet: displayedSheet,
showLongPressMenu: true,
),
if (showMainToolbarNavigationButton)
NavigationMenuButton(selectedTabId: selectedTabId),
],
), ),
), ),
if (showContextualToolbar) if (showContextualToolbar) contextualToolbar,
ContextualToolbar(
selectedTabId: selectedTabId,
displayedSheet: displayedSheet,
),
], ],
), ),
); );
} }
} }
class _QuickTabItem with FastEquatable { class QuickTabSwitcherItem with FastEquatable {
final Color? color; final Color? color;
final String id; final String id;
final TabMode tabMode; final TabMode tabMode;
@@ -379,9 +434,9 @@ class _QuickTabItem with FastEquatable {
final bool isPinned; final bool isPinned;
final String title; final String title;
final Uri url; final Uri url;
final TabState? tabState; final Widget avatar;
_QuickTabItem({ QuickTabSwitcherItem({
required this.color, required this.color,
required this.id, required this.id,
required this.tabMode, required this.tabMode,
@@ -389,7 +444,7 @@ class _QuickTabItem with FastEquatable {
required this.isPinned, required this.isPinned,
required this.title, required this.title,
required this.url, required this.url,
required this.tabState, required this.avatar,
}); });
@override @override
@@ -401,7 +456,7 @@ class _QuickTabItem with FastEquatable {
isPinned, isPinned,
title, title,
url, url,
tabState, avatar,
]; ];
} }
@@ -419,35 +474,71 @@ class ContextualToolbar extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final tabState = ref.watch(tabStateProvider(selectedTabId)); final tabState = ref.watch(tabStateProvider(selectedTabId));
return ContextualToolbarView(
canGoBack:
tabState?.historyState.canGoBack == true ||
tabState?.isLoading == true,
canGoForward: tabState?.historyState.canGoForward == true,
onBookmarksTap: () async {
await BookmarkListRoute(entryGuid: BookmarkRoot.root.id).push(context);
},
backButton: NavigateBackButton(
selectedTabId: selectedTabId,
isLoading: tabState?.isLoading ?? false,
),
forwardButton: NavigateForwardButton(selectedTabId: selectedTabId),
shareButton: ShareMenuButton(selectedTabId: selectedTabId),
addTabButton: const AddTabButton(),
tabsCountButton: TabsCountButton(
selectedTabId: selectedTabId,
displayedSheet: displayedSheet,
showLongPressMenu: false,
),
navigationButton: NavigationMenuButton(selectedTabId: selectedTabId),
);
}
}
class ContextualToolbarView extends StatelessWidget {
const ContextualToolbarView({
super.key,
required this.canGoBack,
required this.canGoForward,
required this.onBookmarksTap,
required this.backButton,
required this.forwardButton,
required this.shareButton,
required this.addTabButton,
required this.tabsCountButton,
required this.navigationButton,
});
final bool canGoBack;
final bool canGoForward;
final VoidCallback onBookmarksTap;
final Widget backButton;
final Widget forwardButton;
final Widget shareButton;
final Widget addTabButton;
final Widget tabsCountButton;
final Widget navigationButton;
@override
Widget build(BuildContext context) {
return Row( return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [ children: [
if (tabState?.historyState.canGoBack == true || if (canGoBack)
tabState?.isLoading == true) backButton
NavigateBackButton(
selectedTabId: selectedTabId,
isLoading: tabState?.isLoading ?? false,
)
else else
IconButton( IconButton(
onPressed: () async { onPressed: onBookmarksTap,
await BookmarkListRoute(
entryGuid: BookmarkRoot.root.id,
).push(context);
},
icon: const Icon(MdiIcons.bookmarkMultiple), icon: const Icon(MdiIcons.bookmarkMultiple),
), ),
if (tabState?.historyState.canGoForward == true) if (canGoForward) forwardButton else shareButton,
NavigateForwardButton(selectedTabId: selectedTabId) addTabButton,
else tabsCountButton,
ShareMenuButton(selectedTabId: selectedTabId), navigationButton,
const AddTabButton(),
TabsCountButton(
selectedTabId: selectedTabId,
displayedSheet: displayedSheet,
showLongPressMenu: false,
),
NavigationMenuButton(selectedTabId: selectedTabId),
], ],
); );
} }
@@ -460,7 +551,6 @@ class QuickTabSwitcher extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final appColors = AppColors.of(context);
final showIsolatedTabUi = ref.watch( final showIsolatedTabUi = ref.watch(
generalSettingsWithDefaultsProvider.select((s) => s.showIsolatedTabUi), generalSettingsWithDefaultsProvider.select((s) => s.showIsolatedTabUi),
); );
@@ -484,8 +574,8 @@ class QuickTabSwitcher extends HookConsumerWidget {
.watch(quickTabSwitcherHistorySuggestionsProvider(quickTabSwitcherMode)) .watch(quickTabSwitcherHistorySuggestionsProvider(quickTabSwitcherMode))
.value; .value;
final availableItems = tabStates.value final availableItems = tabStates.value
.map<_QuickTabItem>( .map<QuickTabSwitcherItem>(
(state) => _QuickTabItem( (state) => QuickTabSwitcherItem(
id: state.$1.id, id: state.$1.id,
title: state.$1.titleOrAuthority, title: state.$1.titleOrAuthority,
tabMode: state.$1.tabMode, tabMode: state.$1.tabMode,
@@ -493,14 +583,14 @@ class QuickTabSwitcher extends HookConsumerWidget {
isPinned: pinnedTabIds?.contains(state.$1.id) ?? false, isPinned: pinnedTabIds?.contains(state.$1.id) ?? false,
url: state.$1.url, url: state.$1.url,
color: state.$2?.color, color: state.$2?.color,
tabState: state.$1, avatar: TabIcon(tabState: state.$1, iconSize: 20),
), ),
) )
.followedBy( .followedBy(
(historySuggestions ?? []).map<_QuickTabItem>((state) { (historySuggestions ?? []).map<QuickTabSwitcherItem>((state) {
final url = Uri.parse(state.url); final url = Uri.parse(state.url);
return _QuickTabItem( return QuickTabSwitcherItem(
id: state.url, id: state.url,
title: state.title ?? url.authority, title: state.title ?? url.authority,
tabMode: TabMode.regular, tabMode: TabMode.regular,
@@ -508,136 +598,164 @@ class QuickTabSwitcher extends HookConsumerWidget {
isPinned: false, isPinned: false,
url: url, url: url,
color: null, color: null,
tabState: null, avatar: UrlIcon([url], iconSize: 20),
); );
}), }),
) )
.toList(); .toList();
final chipScrollController = useScrollController();
return QuickTabSwitcherView(
availableItems: availableItems,
scrollController: chipScrollController,
showTitles: showTitles,
showIsolatedTabUi: showIsolatedTabUi,
onSelected: (item) async {
final animation = chipScrollController.animateTo(
0,
duration: const Duration(milliseconds: 200),
curve: Curves.easeOutBack,
);
if (item.isHistory) {
await ref
.read(tabRepositoryProvider.notifier)
.addTab(url: item.url, tabMode: TabMode.regular, selectTab: true);
} else {
await ref.read(tabRepositoryProvider.notifier).selectTab(item.id);
}
await animation;
},
itemWrapBuilder: (child, item) {
if (item.isHistory) {
return child;
}
return TabMenu(
selectedTabId: item.id,
enableFindInPage: false,
enableFetchFeeds: false,
enableDesktopMode: false,
enableReaderMode: false,
enableReloadButton: false,
enableNavigationButtons: false,
enableAddToHomeScreen: false,
enablePinTab: effectiveMode == QuickTabSwitcherMode.containerTabs,
builder: (context, controller, _) {
return InkWell(
onLongPress: () {
if (controller.isOpen) {
controller.close();
} else {
controller.open();
}
},
child: child,
);
},
);
},
);
}
}
class QuickTabSwitcherView extends StatelessWidget {
const QuickTabSwitcherView({
super.key,
required this.availableItems,
required this.scrollController,
required this.showTitles,
required this.showIsolatedTabUi,
required this.onSelected,
required this.itemWrapBuilder,
});
final List<QuickTabSwitcherItem> availableItems;
final ScrollController scrollController;
final bool showTitles;
final bool showIsolatedTabUi;
final Future<void> Function(QuickTabSwitcherItem item) onSelected;
final Widget Function(Widget child, QuickTabSwitcherItem item)
itemWrapBuilder;
@override
Widget build(BuildContext context) {
final appColors = AppColors.of(context);
if (availableItems.isEmpty) { if (availableItems.isEmpty) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
final chipScrollController = useScrollController();
return Padding( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 4.0), padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: SizedBox( child: SizedBox(
height: 48, height: 48,
width: double.maxFinite, width: double.maxFinite,
child: SelectableChips<_QuickTabItem, _QuickTabItem, String>( child:
enableDelete: false, SelectableChips<QuickTabSwitcherItem, QuickTabSwitcherItem, String>(
scrollController: chipScrollController, enableDelete: false,
itemId: (item) => item.id, scrollController: scrollController,
labelPadding: (item) => itemId: (item) => item.id,
(!showTitles && labelPadding: (item) =>
!item.isHistory && (!showTitles &&
!item.isPinned && !item.isHistory &&
item.tabMode is! PrivateTabMode && !item.isPinned &&
item.tabMode is! IsolatedTabMode) item.tabMode is! PrivateTabMode &&
? EdgeInsets.zero item.tabMode is! IsolatedTabMode)
: null, ? EdgeInsets.zero
itemLabel: (item) { : null,
return Row( itemLabel: (item) {
mainAxisSize: MainAxisSize.min, return Row(
children: [ mainAxisSize: MainAxisSize.min,
if (item.isHistory || showTitles) children: [
ConstrainedBox( if (item.isHistory || showTitles)
constraints: const BoxConstraints(maxWidth: 64), ConstrainedBox(
child: Text(item.title), constraints: const BoxConstraints(maxWidth: 64),
), child: Text(item.title),
if (showIsolatedTabUi && item.tabMode is IsolatedTabMode) ),
Padding( if (showIsolatedTabUi && item.tabMode is IsolatedTabMode)
padding: const EdgeInsets.only(left: 8.0), Padding(
child: Icon( padding: const EdgeInsets.only(left: 8.0),
MdiIcons.snowflake, child: Icon(
color: appColors.isolatedTabTeal, MdiIcons.snowflake,
size: 20, color: appColors.isolatedTabTeal,
), size: 20,
) ),
else if (item.tabMode is PrivateTabMode) )
Padding( else if (item.tabMode is PrivateTabMode)
padding: const EdgeInsets.only(left: 8.0), Padding(
child: Icon( padding: const EdgeInsets.only(left: 8.0),
MdiIcons.dominoMask, child: Icon(
color: appColors.privateTabPurple, MdiIcons.dominoMask,
size: 20, color: appColors.privateTabPurple,
), size: 20,
), ),
if (item.isPinned) ),
Padding( if (item.isPinned)
padding: const EdgeInsets.only(left: 8.0), Padding(
child: Icon( padding: const EdgeInsets.only(left: 8.0),
MdiIcons.pin, child: Icon(
color: Theme.of(context).colorScheme.primary, MdiIcons.pin,
size: 20, color: Theme.of(context).colorScheme.primary,
), size: 20,
), ),
if (item.isHistory) ),
const Padding( if (item.isHistory)
padding: EdgeInsets.only(left: 8.0), const Padding(
child: Icon(MdiIcons.history, size: 20), padding: EdgeInsets.only(left: 8.0),
), child: Icon(MdiIcons.history, size: 20),
], ),
); ],
},
itemAvatar: (item) =>
item.tabState.mapNotNull(
(tabState) => TabIcon(tabState: tabState, iconSize: 20),
) ??
UrlIcon([item.url], iconSize: 20),
itemBackgroundColor: (item) =>
item.color != null ? ContainerColors.forChip(item.color!) : null,
onSelected: (item) async {
final animation = chipScrollController.animateTo(
0,
duration: const Duration(milliseconds: 200),
curve: Curves.easeOutBack,
);
if (item.isHistory) {
await ref
.read(tabRepositoryProvider.notifier)
.addTab(
url: item.url,
tabMode: TabMode.regular,
selectTab: true,
);
} else {
await ref.read(tabRepositoryProvider.notifier).selectTab(item.id);
}
await animation;
},
itemWrap: (child, item) {
if (item.isHistory) {
return child;
}
return TabMenu(
selectedTabId: item.id,
enableFindInPage: false,
enableFetchFeeds: false,
enableDesktopMode: false,
enableReaderMode: false,
enableReloadButton: false,
enableNavigationButtons: false,
enableAddToHomeScreen: false,
enablePinTab: effectiveMode == QuickTabSwitcherMode.containerTabs,
builder: (context, controller, _) {
return InkWell(
onLongPress: () {
if (controller.isOpen) {
controller.close();
} else {
controller.open();
}
},
child: child,
); );
}, },
); itemAvatar: (item) => item.avatar,
}, itemBackgroundColor: (item) => item.color != null
availableItems: availableItems, ? ContainerColors.forChip(item.color!)
), : null,
onSelected: onSelected,
itemWrap: itemWrapBuilder,
availableItems: availableItems,
),
), ),
); );
} }
@@ -650,18 +768,28 @@ class ShareMenuButton extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return IconButton( return ShareMenuButtonView(
onPressed: () async { onPressed: () async {
final tabId = selectedTabId; final tabId = selectedTabId;
if (tabId != null) { if (tabId != null) {
await showShareBottomSheet(context, selectedTabId: tabId); await showShareBottomSheet(context, selectedTabId: tabId);
} }
}, },
icon: const Icon(Icons.share),
); );
} }
} }
class ShareMenuButtonView extends StatelessWidget {
const ShareMenuButtonView({super.key, this.onPressed});
final VoidCallback? onPressed;
@override
Widget build(BuildContext context) {
return IconButton(onPressed: onPressed, icon: const Icon(Icons.share));
}
}
class NavigationMenuButton extends StatelessWidget { class NavigationMenuButton extends StatelessWidget {
final String? selectedTabId; final String? selectedTabId;
@@ -669,15 +797,25 @@ class NavigationMenuButton extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ToolbarButton( return NavigationMenuButtonView(
onTap: () async { onTap: () async {
await showBrowserMenuSheet(context); await showBrowserMenuSheet(context);
}, },
child: const Icon(Icons.more_vert),
); );
} }
} }
class NavigationMenuButtonView extends StatelessWidget {
const NavigationMenuButtonView({super.key, this.onTap});
final VoidCallback? onTap;
@override
Widget build(BuildContext context) {
return ToolbarButton(onTap: onTap, child: const Icon(Icons.more_vert));
}
}
class AddTabButton extends HookConsumerWidget { class AddTabButton extends HookConsumerWidget {
const AddTabButton({super.key}); const AddTabButton({super.key});
@@ -687,7 +825,7 @@ class AddTabButton extends HookConsumerWidget {
return TabCreationMenu( return TabCreationMenu(
controller: tabMenuController, controller: tabMenuController,
child: IconButton( child: AddTabButtonView(
onPressed: () async { onPressed: () async {
final settings = ref.read(generalSettingsWithDefaultsProvider); final settings = ref.read(generalSettingsWithDefaultsProvider);
@@ -701,7 +839,6 @@ class AddTabButton extends HookConsumerWidget {
const BrowserRoute().go(context); const BrowserRoute().go(context);
} }
}, },
icon: const Icon(MdiIcons.tabPlus),
onLongPress: () { onLongPress: () {
if (tabMenuController.isOpen) { if (tabMenuController.isOpen) {
tabMenuController.close(); tabMenuController.close();
@@ -714,6 +851,53 @@ class AddTabButton extends HookConsumerWidget {
} }
} }
class AddTabButtonView extends StatelessWidget {
const AddTabButtonView({super.key, this.onPressed, this.onLongPress});
final VoidCallback? onPressed;
final VoidCallback? onLongPress;
@override
Widget build(BuildContext context) {
return IconButton(
onPressed: onPressed,
icon: const Icon(MdiIcons.tabPlus),
onLongPress: onLongPress,
);
}
}
class TabsCountButtonView extends StatelessWidget {
const TabsCountButtonView({
super.key,
required this.isActive,
required this.onTap,
this.onLongPress,
this.buttonBuilder,
});
final bool isActive;
final VoidCallback onTap;
final VoidCallback? onLongPress;
final Widget Function(
bool isActive,
VoidCallback onTap,
VoidCallback? onLongPress,
)?
buttonBuilder;
@override
Widget build(BuildContext context) {
return (buttonBuilder != null)
? buttonBuilder!(isActive, onTap, onLongPress)
: TabsActionButton(
isActive: isActive,
onTap: onTap,
onLongPress: onLongPress,
);
}
}
class TabsCountButton extends HookConsumerWidget { class TabsCountButton extends HookConsumerWidget {
const TabsCountButton({ const TabsCountButton({
super.key, super.key,
@@ -732,7 +916,7 @@ class TabsCountButton extends HookConsumerWidget {
return TabCreationMenu( return TabCreationMenu(
controller: tabMenuController, controller: tabMenuController,
child: TabsActionButton( child: TabsCountButtonView(
isActive: displayedSheet is ViewTabsSheet, isActive: displayedSheet is ViewTabsSheet,
onTap: () async { onTap: () async {
final tabViewBottomSheet = ref final tabViewBottomSheet = ref
@@ -25,6 +25,52 @@ import 'package:weblibre/features/geckoview/features/browser/presentation/widget
import 'package:weblibre/features/geckoview/features/readerview/presentation/controllers/readerable.dart'; import 'package:weblibre/features/geckoview/features/readerview/presentation/controllers/readerable.dart';
import 'package:weblibre/presentation/hooks/menu_controller.dart'; import 'package:weblibre/presentation/hooks/menu_controller.dart';
class NavigateForwardButtonView extends StatelessWidget {
const NavigateForwardButtonView({
super.key,
required this.canGoForward,
this.onPressed,
this.onLongPress,
});
final bool canGoForward;
final VoidCallback? onPressed;
final VoidCallback? onLongPress;
@override
Widget build(BuildContext context) {
return IconButton(
onPressed: canGoForward ? onPressed : null,
onLongPress: canGoForward ? onLongPress : null,
icon: const Icon(Icons.arrow_forward),
);
}
}
class NavigateBackButtonView extends StatelessWidget {
const NavigateBackButtonView({
super.key,
required this.canGoBack,
required this.isLoading,
this.onPressed,
this.onLongPress,
});
final bool canGoBack;
final bool isLoading;
final VoidCallback? onPressed;
final VoidCallback? onLongPress;
@override
Widget build(BuildContext context) {
return IconButton(
onPressed: (canGoBack || isLoading) ? onPressed : null,
onLongPress: (canGoBack && !isLoading) ? onLongPress : null,
icon: isLoading ? const Icon(Icons.close) : const Icon(Icons.arrow_back),
);
}
}
class NavigateForwardButton extends HookConsumerWidget { class NavigateForwardButton extends HookConsumerWidget {
const NavigateForwardButton({ const NavigateForwardButton({
super.key, super.key,
@@ -45,25 +91,21 @@ class NavigateForwardButton extends HookConsumerWidget {
selectedTabId: selectedTabId, selectedTabId: selectedTabId,
controller: historyMenuController, controller: historyMenuController,
direction: HistoryMenuDirection.forward, direction: HistoryMenuDirection.forward,
child: IconButton( child: NavigateForwardButtonView(
onPressed: canGoForward canGoForward: canGoForward,
? () async { onPressed: () async {
final controller = ref.read( final controller = ref.read(
tabSessionProvider(tabId: selectedTabId).notifier, tabSessionProvider(tabId: selectedTabId).notifier,
); );
await controller.goForward(); await controller.goForward();
menuControllerToClose?.close(); menuControllerToClose?.close();
} },
: null, onLongPress: () {
onLongPress: canGoForward if (!historyMenuController.isOpen) {
? () { historyMenuController.open();
if (!historyMenuController.isOpen) { }
historyMenuController.open(); },
}
}
: null,
icon: const Icon(Icons.arrow_forward),
), ),
); );
} }
@@ -91,42 +133,37 @@ class NavigateBackButton extends HookConsumerWidget {
selectedTabId: selectedTabId, selectedTabId: selectedTabId,
controller: historyMenuController, controller: historyMenuController,
direction: HistoryMenuDirection.back, direction: HistoryMenuDirection.back,
child: IconButton( child: NavigateBackButtonView(
onPressed: (canGoBack || isLoading) canGoBack: canGoBack,
? () async { isLoading: isLoading,
final controller = ref.read( onPressed: () async {
tabSessionProvider(tabId: selectedTabId).notifier, final controller = ref.read(
); tabSessionProvider(tabId: selectedTabId).notifier,
);
final isReaderActive = ref.read( final isReaderActive = ref.read(
selectedTabStateProvider.select( selectedTabStateProvider.select(
(state) => state?.readerableState.active ?? false, (state) => state?.readerableState.active ?? false,
), ),
); );
if (isLoading) { if (isLoading) {
await controller.stopLoading(); await controller.stopLoading();
} else if (isReaderActive) { } else if (isReaderActive) {
await ref await ref
.read(readerableScreenControllerProvider.notifier) .read(readerableScreenControllerProvider.notifier)
.toggleReaderView(false); .toggleReaderView(false);
} else { } else {
await controller.goBack(); await controller.goBack();
} }
menuControllerToClose?.close(); menuControllerToClose?.close();
} },
: null, onLongPress: () {
onLongPress: (canGoBack && !isLoading) if (!historyMenuController.isOpen) {
? () { historyMenuController.open();
if (!historyMenuController.isOpen) { }
historyMenuController.open(); },
}
}
: null,
icon: isLoading
? const Icon(Icons.close)
: const Icon(Icons.arrow_back),
), ),
); );
} }
@@ -26,6 +26,56 @@ import 'package:weblibre/features/geckoview/features/tabs/data/entities/containe
import 'package:weblibre/features/geckoview/features/tabs/domain/providers.dart'; import 'package:weblibre/features/geckoview/features/tabs/domain/providers.dart';
import 'package:weblibre/features/geckoview/features/tabs/domain/providers/selected_container.dart'; import 'package:weblibre/features/geckoview/features/tabs/domain/providers/selected_container.dart';
class TabsActionButtonView extends StatelessWidget {
const TabsActionButtonView({
super.key,
required this.isActive,
required this.tabCountText,
this.showSkeleton = false,
this.onTap,
this.onDoubleTap,
this.onLongPress,
});
final bool isActive;
final String tabCountText;
final bool showSkeleton;
final VoidCallback? onTap;
final VoidCallback? onDoubleTap;
final VoidCallback? onLongPress;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final iconColor = isActive
? theme.colorScheme.primary
: theme.colorScheme.onSurfaceVariant;
final text = Text(
tabCountText,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14.0,
color: iconColor,
),
);
return ToolbarButton(
onTap: onTap,
onDoubleTap: onDoubleTap,
onLongPress: onLongPress,
child: Container(
decoration: BoxDecoration(
border: Border.all(width: 2.0, color: iconColor),
borderRadius: BorderRadius.circular(5.0),
),
constraints: const BoxConstraints(minWidth: 25.0),
child: Center(child: showSkeleton ? Skeletonizer(child: text) : text),
),
);
}
}
class TabsActionButton extends HookConsumerWidget { class TabsActionButton extends HookConsumerWidget {
final bool isActive; final bool isActive;
final VoidCallback? onTap; final VoidCallback? onTap;
@@ -42,78 +92,32 @@ class TabsActionButton extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final theme = Theme.of(context);
final tabCount = isActive final tabCount = isActive
// ignore: provider_parameters // ignore: provider_parameters
? ref.watch(containerTabCountProvider(ContainerFilterDisabled())) ? ref.watch(containerTabCountProvider(ContainerFilterDisabled()))
: ref.watch(selectedContainerTabCountProvider); : ref.watch(selectedContainerTabCountProvider);
final iconColor = isActive final tabCountText = tabCount.when(
? theme.colorScheme.primary skipLoadingOnReload: true,
: theme.colorScheme.onSurfaceVariant; data: (count) => count.toString(),
loading: () => tabCount.hasValue ? tabCount.value.toString() : '0',
error: (error, stackTrace) {
logger.e(
'Could not determine tab count',
error: error,
stackTrace: stackTrace,
);
return '-1';
},
);
return ToolbarButton( return TabsActionButtonView(
isActive: isActive,
tabCountText: tabCountText,
showSkeleton: tabCount.isLoading && !tabCount.hasValue,
onTap: onTap, onTap: onTap,
onDoubleTap: onDoubleTap, onDoubleTap: onDoubleTap,
onLongPress: onLongPress, onLongPress: onLongPress,
child: Container(
decoration: BoxDecoration(
border: Border.all(width: 2.0, color: iconColor),
borderRadius: BorderRadius.circular(5.0),
),
constraints: const BoxConstraints(minWidth: 25.0),
child: Center(
child: tabCount.when(
skipLoadingOnReload: true,
data: (count) {
return Text(
count.toString(),
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14.0,
color: iconColor,
),
);
},
loading: () => (tabCount.hasValue)
? Text(
tabCount.value.toString(),
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14.0,
color: iconColor,
),
)
: Skeletonizer(
child: Text(
'0',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14.0,
color: iconColor,
),
),
),
error: (error, stackTrace) {
logger.e(
'Could not determine tab count',
error: error,
stackTrace: stackTrace,
);
return Text(
'-1',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14.0,
color: iconColor,
),
);
},
),
),
),
); );
} }
} }