decouple app bar related widgets from logic
This commit is contained in:
+137
-98
@@ -18,7 +18,6 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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';
|
||||
@@ -27,6 +26,7 @@ import 'package:weblibre/core/design/app_colors.dart';
|
||||
import 'package:weblibre/core/routing/routes.dart';
|
||||
import 'package:weblibre/extensions/uri.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/features/browser/domain/entities/sheet.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/presentation/widgets/uri_breadcrumb.dart';
|
||||
|
||||
class CompactAppBarTitle extends HookConsumerWidget {
|
||||
class CompactAppBarTitle extends ConsumerWidget {
|
||||
const CompactAppBarTitle({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = Theme.of(context);
|
||||
final appColors = AppColors.of(context);
|
||||
|
||||
final tabState = ref.watch(selectedTabStateProvider);
|
||||
final isTabTuneledAsync = ref.watch(isTabTunneledProvider(tabState?.id));
|
||||
final showSiteSettingsBadge = ref.watch(
|
||||
@@ -53,42 +50,60 @@ class CompactAppBarTitle extends HookConsumerWidget {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final icon = useMemoized(() {
|
||||
if (tabState.url.isHttp) {
|
||||
return Icon(
|
||||
MdiIcons.lockOff,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
size: 16,
|
||||
);
|
||||
} else if (tabState.readerableState.active) {
|
||||
return const Icon(MdiIcons.lockMinus, size: 16);
|
||||
} else if (!tabState.securityInfoState.secure) {
|
||||
return Icon(
|
||||
MdiIcons.lockAlert,
|
||||
color: Theme.of(context).colorScheme.errorContainer,
|
||||
size: 16,
|
||||
);
|
||||
} else if (!tabState.isLoading) {
|
||||
return const Icon(MdiIcons.lock, size: 16);
|
||||
} else {
|
||||
return const Icon(MdiIcons.timerSand, size: 16);
|
||||
}
|
||||
}, [tabState]);
|
||||
return CompactAppBarTitleView(
|
||||
tabState: tabState,
|
||||
isTabTunneled:
|
||||
isTabTuneledAsync.hasValue && isTabTuneledAsync.value == true,
|
||||
showSiteSettingsBadge: showSiteSettingsBadge,
|
||||
onSiteSettingsTap: () {
|
||||
ref
|
||||
.read(bottomSheetControllerProvider.notifier)
|
||||
.show(SiteSettingsSheet(tabState: tabState));
|
||||
},
|
||||
onTitleTap: () async {
|
||||
await SearchRoute(
|
||||
tabId: tabState.id,
|
||||
searchText: _searchTextForTab(tabState),
|
||||
tabType: tabState.tabMode.toTabType(),
|
||||
).push(context);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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(
|
||||
children: [
|
||||
ToolbarButton(
|
||||
onTap: () {
|
||||
ref
|
||||
.read(bottomSheetControllerProvider.notifier)
|
||||
.show(SiteSettingsSheet(tabState: tabState));
|
||||
},
|
||||
onTap: onSiteSettingsTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: 4.0),
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
TabIcon(tabState: tabState, iconSize: 24),
|
||||
tabIcon ?? TabIcon(tabState: tabState, iconSize: 24),
|
||||
Positioned(
|
||||
top: -4,
|
||||
right: -4,
|
||||
@@ -106,19 +121,7 @@ class CompactAppBarTitle extends HookConsumerWidget {
|
||||
),
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () async {
|
||||
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);
|
||||
},
|
||||
onTap: onTitleTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12),
|
||||
decoration: BoxDecoration(
|
||||
@@ -144,12 +147,11 @@ class CompactAppBarTitle extends HookConsumerWidget {
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
],
|
||||
if (isTabTuneledAsync.hasValue &&
|
||||
isTabTuneledAsync.value == true) ...[
|
||||
if (isTabTunneled) ...[
|
||||
const Icon(MdiIcons.tunnelOutline, size: 16),
|
||||
const SizedBox(width: 4),
|
||||
],
|
||||
icon,
|
||||
_SecurityStatusIcon(tabState: tabState, size: 16),
|
||||
const SizedBox(width: 6),
|
||||
Flexible(
|
||||
child: UriBreadcrumb(
|
||||
@@ -170,14 +172,11 @@ class CompactAppBarTitle extends HookConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class AppBarTitle extends HookConsumerWidget {
|
||||
class AppBarTitle extends ConsumerWidget {
|
||||
const AppBarTitle({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = Theme.of(context);
|
||||
final appColors = AppColors.of(context);
|
||||
|
||||
final tabState = ref.watch(selectedTabStateProvider);
|
||||
final isTabTuneledAsync = ref.watch(isTabTunneledProvider(tabState?.id));
|
||||
final showSiteSettingsBadge = ref.watch(
|
||||
@@ -188,42 +187,60 @@ class AppBarTitle extends HookConsumerWidget {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final icon = useMemoized(() {
|
||||
if (tabState.url.isHttp) {
|
||||
return Icon(
|
||||
MdiIcons.lockOff,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
size: 14,
|
||||
);
|
||||
} else if (tabState.readerableState.active) {
|
||||
return const Icon(MdiIcons.lockMinus, 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 AppBarTitleView(
|
||||
tabState: tabState,
|
||||
isTabTunneled:
|
||||
isTabTuneledAsync.hasValue && isTabTuneledAsync.value == true,
|
||||
showSiteSettingsBadge: showSiteSettingsBadge,
|
||||
onSiteSettingsTap: () {
|
||||
ref
|
||||
.read(bottomSheetControllerProvider.notifier)
|
||||
.show(SiteSettingsSheet(tabState: tabState));
|
||||
},
|
||||
onTitleTap: () async {
|
||||
await SearchRoute(
|
||||
tabId: tabState.id,
|
||||
searchText: _searchTextForTab(tabState),
|
||||
tabType: tabState.tabMode.toTabType(),
|
||||
).push(context);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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(
|
||||
children: [
|
||||
ToolbarButton(
|
||||
onTap: () {
|
||||
ref
|
||||
.read(bottomSheetControllerProvider.notifier)
|
||||
.show(SiteSettingsSheet(tabState: tabState));
|
||||
},
|
||||
onTap: onSiteSettingsTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: 4.0),
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
TabIcon(tabState: tabState, iconSize: 24),
|
||||
tabIcon ?? TabIcon(tabState: tabState, iconSize: 24),
|
||||
Positioned(
|
||||
top: -4,
|
||||
right: -4,
|
||||
@@ -239,23 +256,9 @@ class AppBarTitle extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
// Title/URL tap → opens search screen
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () async {
|
||||
// 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);
|
||||
},
|
||||
onTap: onTitleTap,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -273,7 +276,6 @@ class AppBarTitle extends HookConsumerWidget {
|
||||
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),
|
||||
@@ -302,12 +304,11 @@ class AppBarTitle extends HookConsumerWidget {
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
],
|
||||
if (isTabTuneledAsync.hasValue &&
|
||||
isTabTuneledAsync.value == true) ...[
|
||||
if (isTabTunneled) ...[
|
||||
const Icon(MdiIcons.tunnelOutline, size: 14),
|
||||
const SizedBox(width: 4),
|
||||
],
|
||||
icon,
|
||||
_SecurityStatusIcon(tabState: tabState, size: 14),
|
||||
const SizedBox(width: 4),
|
||||
Expanded(
|
||||
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;
|
||||
}
|
||||
|
||||
+429
-245
@@ -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_mozilla_components/flutter_mozilla_components.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/routing/routes.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/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/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/url_icon.dart';
|
||||
|
||||
class BrowserTopAppBar extends HookConsumerWidget {
|
||||
class BrowserTopAppBar extends StatelessWidget {
|
||||
final bool showMainToolbar;
|
||||
final bool showContextualToolbar;
|
||||
final bool showQuickTabSwitcherBar;
|
||||
@@ -85,7 +83,7 @@ class BrowserTopAppBar extends HookConsumerWidget {
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
Widget build(BuildContext context) {
|
||||
return SafeArea(
|
||||
child: SizedBox(height: preferredSize.height, child: _tabBar),
|
||||
);
|
||||
@@ -94,7 +92,7 @@ class BrowserTopAppBar extends HookConsumerWidget {
|
||||
Size get preferredSize => _size;
|
||||
}
|
||||
|
||||
class BrowserBottomAppBar extends HookConsumerWidget {
|
||||
class BrowserBottomAppBar extends StatelessWidget {
|
||||
final bool showMainToolbar;
|
||||
final bool showContextualToolbar;
|
||||
final bool showQuickTabSwitcherBar;
|
||||
@@ -121,7 +119,7 @@ class BrowserBottomAppBar extends HookConsumerWidget {
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
Widget build(BuildContext context) {
|
||||
final bottomPadding = MediaQuery.of(context).padding.bottom;
|
||||
|
||||
return Material(
|
||||
@@ -205,8 +203,97 @@ class BrowserTabBar extends HookConsumerWidget {
|
||||
|
||||
final dragStartPosition = useRef(Offset.zero);
|
||||
|
||||
return GestureDetector(
|
||||
// Tap handling moved to AppBarTitle for split icon/title behavior
|
||||
final showTabTitle =
|
||||
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) {
|
||||
dragStartPosition.value = details.globalPosition;
|
||||
},
|
||||
@@ -262,6 +349,52 @@ class BrowserTabBar extends HookConsumerWidget {
|
||||
.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(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
@@ -269,9 +402,7 @@ class BrowserTabBar extends HookConsumerWidget {
|
||||
Visibility(
|
||||
visible: displayQuickTabSwitcher,
|
||||
maintainState: true,
|
||||
child: QuickTabSwitcher(
|
||||
quickTabSwitcherMode: quickTabSwitcherMode,
|
||||
),
|
||||
child: quickTabSwitcher,
|
||||
),
|
||||
if (showMainToolbar)
|
||||
Visibility(
|
||||
@@ -283,95 +414,19 @@ class BrowserTabBar extends HookConsumerWidget {
|
||||
titleSpacing: 0.0,
|
||||
leadingWidth: 40.0,
|
||||
toolbarHeight: kToolbarHeight,
|
||||
backgroundColor:
|
||||
(settings.showContainerUi &&
|
||||
containerColor != null &&
|
||||
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),
|
||||
],
|
||||
backgroundColor: backgroundColor,
|
||||
title: title,
|
||||
actions: actions,
|
||||
),
|
||||
),
|
||||
if (showContextualToolbar)
|
||||
ContextualToolbar(
|
||||
selectedTabId: selectedTabId,
|
||||
displayedSheet: displayedSheet,
|
||||
),
|
||||
if (showContextualToolbar) contextualToolbar,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _QuickTabItem with FastEquatable {
|
||||
class QuickTabSwitcherItem with FastEquatable {
|
||||
final Color? color;
|
||||
final String id;
|
||||
final TabMode tabMode;
|
||||
@@ -379,9 +434,9 @@ class _QuickTabItem with FastEquatable {
|
||||
final bool isPinned;
|
||||
final String title;
|
||||
final Uri url;
|
||||
final TabState? tabState;
|
||||
final Widget avatar;
|
||||
|
||||
_QuickTabItem({
|
||||
QuickTabSwitcherItem({
|
||||
required this.color,
|
||||
required this.id,
|
||||
required this.tabMode,
|
||||
@@ -389,7 +444,7 @@ class _QuickTabItem with FastEquatable {
|
||||
required this.isPinned,
|
||||
required this.title,
|
||||
required this.url,
|
||||
required this.tabState,
|
||||
required this.avatar,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -401,7 +456,7 @@ class _QuickTabItem with FastEquatable {
|
||||
isPinned,
|
||||
title,
|
||||
url,
|
||||
tabState,
|
||||
avatar,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -419,35 +474,71 @@ class ContextualToolbar extends HookConsumerWidget {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
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(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
if (tabState?.historyState.canGoBack == true ||
|
||||
tabState?.isLoading == true)
|
||||
NavigateBackButton(
|
||||
selectedTabId: selectedTabId,
|
||||
isLoading: tabState?.isLoading ?? false,
|
||||
)
|
||||
if (canGoBack)
|
||||
backButton
|
||||
else
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
await BookmarkListRoute(
|
||||
entryGuid: BookmarkRoot.root.id,
|
||||
).push(context);
|
||||
},
|
||||
onPressed: onBookmarksTap,
|
||||
icon: const Icon(MdiIcons.bookmarkMultiple),
|
||||
),
|
||||
if (tabState?.historyState.canGoForward == true)
|
||||
NavigateForwardButton(selectedTabId: selectedTabId)
|
||||
else
|
||||
ShareMenuButton(selectedTabId: selectedTabId),
|
||||
const AddTabButton(),
|
||||
TabsCountButton(
|
||||
selectedTabId: selectedTabId,
|
||||
displayedSheet: displayedSheet,
|
||||
showLongPressMenu: false,
|
||||
),
|
||||
NavigationMenuButton(selectedTabId: selectedTabId),
|
||||
if (canGoForward) forwardButton else shareButton,
|
||||
addTabButton,
|
||||
tabsCountButton,
|
||||
navigationButton,
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -460,7 +551,6 @@ class QuickTabSwitcher extends HookConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final appColors = AppColors.of(context);
|
||||
final showIsolatedTabUi = ref.watch(
|
||||
generalSettingsWithDefaultsProvider.select((s) => s.showIsolatedTabUi),
|
||||
);
|
||||
@@ -484,8 +574,8 @@ class QuickTabSwitcher extends HookConsumerWidget {
|
||||
.watch(quickTabSwitcherHistorySuggestionsProvider(quickTabSwitcherMode))
|
||||
.value;
|
||||
final availableItems = tabStates.value
|
||||
.map<_QuickTabItem>(
|
||||
(state) => _QuickTabItem(
|
||||
.map<QuickTabSwitcherItem>(
|
||||
(state) => QuickTabSwitcherItem(
|
||||
id: state.$1.id,
|
||||
title: state.$1.titleOrAuthority,
|
||||
tabMode: state.$1.tabMode,
|
||||
@@ -493,14 +583,14 @@ class QuickTabSwitcher extends HookConsumerWidget {
|
||||
isPinned: pinnedTabIds?.contains(state.$1.id) ?? false,
|
||||
url: state.$1.url,
|
||||
color: state.$2?.color,
|
||||
tabState: state.$1,
|
||||
avatar: TabIcon(tabState: state.$1, iconSize: 20),
|
||||
),
|
||||
)
|
||||
.followedBy(
|
||||
(historySuggestions ?? []).map<_QuickTabItem>((state) {
|
||||
(historySuggestions ?? []).map<QuickTabSwitcherItem>((state) {
|
||||
final url = Uri.parse(state.url);
|
||||
|
||||
return _QuickTabItem(
|
||||
return QuickTabSwitcherItem(
|
||||
id: state.url,
|
||||
title: state.title ?? url.authority,
|
||||
tabMode: TabMode.regular,
|
||||
@@ -508,136 +598,164 @@ class QuickTabSwitcher extends HookConsumerWidget {
|
||||
isPinned: false,
|
||||
url: url,
|
||||
color: null,
|
||||
tabState: null,
|
||||
avatar: UrlIcon([url], iconSize: 20),
|
||||
);
|
||||
}),
|
||||
)
|
||||
.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) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final chipScrollController = useScrollController();
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||
child: SizedBox(
|
||||
height: 48,
|
||||
width: double.maxFinite,
|
||||
child: SelectableChips<_QuickTabItem, _QuickTabItem, String>(
|
||||
enableDelete: false,
|
||||
scrollController: chipScrollController,
|
||||
itemId: (item) => item.id,
|
||||
labelPadding: (item) =>
|
||||
(!showTitles &&
|
||||
!item.isHistory &&
|
||||
!item.isPinned &&
|
||||
item.tabMode is! PrivateTabMode &&
|
||||
item.tabMode is! IsolatedTabMode)
|
||||
? EdgeInsets.zero
|
||||
: null,
|
||||
itemLabel: (item) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (item.isHistory || showTitles)
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 64),
|
||||
child: Text(item.title),
|
||||
),
|
||||
if (showIsolatedTabUi && item.tabMode is IsolatedTabMode)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0),
|
||||
child: Icon(
|
||||
MdiIcons.snowflake,
|
||||
color: appColors.isolatedTabTeal,
|
||||
size: 20,
|
||||
),
|
||||
)
|
||||
else if (item.tabMode is PrivateTabMode)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0),
|
||||
child: Icon(
|
||||
MdiIcons.dominoMask,
|
||||
color: appColors.privateTabPurple,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
if (item.isPinned)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0),
|
||||
child: Icon(
|
||||
MdiIcons.pin,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
if (item.isHistory)
|
||||
const Padding(
|
||||
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,
|
||||
child:
|
||||
SelectableChips<QuickTabSwitcherItem, QuickTabSwitcherItem, String>(
|
||||
enableDelete: false,
|
||||
scrollController: scrollController,
|
||||
itemId: (item) => item.id,
|
||||
labelPadding: (item) =>
|
||||
(!showTitles &&
|
||||
!item.isHistory &&
|
||||
!item.isPinned &&
|
||||
item.tabMode is! PrivateTabMode &&
|
||||
item.tabMode is! IsolatedTabMode)
|
||||
? EdgeInsets.zero
|
||||
: null,
|
||||
itemLabel: (item) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (item.isHistory || showTitles)
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 64),
|
||||
child: Text(item.title),
|
||||
),
|
||||
if (showIsolatedTabUi && item.tabMode is IsolatedTabMode)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0),
|
||||
child: Icon(
|
||||
MdiIcons.snowflake,
|
||||
color: appColors.isolatedTabTeal,
|
||||
size: 20,
|
||||
),
|
||||
)
|
||||
else if (item.tabMode is PrivateTabMode)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0),
|
||||
child: Icon(
|
||||
MdiIcons.dominoMask,
|
||||
color: appColors.privateTabPurple,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
if (item.isPinned)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0),
|
||||
child: Icon(
|
||||
MdiIcons.pin,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
if (item.isHistory)
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(left: 8.0),
|
||||
child: Icon(MdiIcons.history, size: 20),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
availableItems: availableItems,
|
||||
),
|
||||
itemAvatar: (item) => item.avatar,
|
||||
itemBackgroundColor: (item) => item.color != null
|
||||
? ContainerColors.forChip(item.color!)
|
||||
: null,
|
||||
onSelected: onSelected,
|
||||
itemWrap: itemWrapBuilder,
|
||||
availableItems: availableItems,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -650,18 +768,28 @@ class ShareMenuButton extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return IconButton(
|
||||
return ShareMenuButtonView(
|
||||
onPressed: () async {
|
||||
final tabId = selectedTabId;
|
||||
if (tabId != null) {
|
||||
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 {
|
||||
final String? selectedTabId;
|
||||
|
||||
@@ -669,15 +797,25 @@ class NavigationMenuButton extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ToolbarButton(
|
||||
return NavigationMenuButtonView(
|
||||
onTap: () async {
|
||||
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 {
|
||||
const AddTabButton({super.key});
|
||||
|
||||
@@ -687,7 +825,7 @@ class AddTabButton extends HookConsumerWidget {
|
||||
|
||||
return TabCreationMenu(
|
||||
controller: tabMenuController,
|
||||
child: IconButton(
|
||||
child: AddTabButtonView(
|
||||
onPressed: () async {
|
||||
final settings = ref.read(generalSettingsWithDefaultsProvider);
|
||||
|
||||
@@ -701,7 +839,6 @@ class AddTabButton extends HookConsumerWidget {
|
||||
const BrowserRoute().go(context);
|
||||
}
|
||||
},
|
||||
icon: const Icon(MdiIcons.tabPlus),
|
||||
onLongPress: () {
|
||||
if (tabMenuController.isOpen) {
|
||||
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 {
|
||||
const TabsCountButton({
|
||||
super.key,
|
||||
@@ -732,7 +916,7 @@ class TabsCountButton extends HookConsumerWidget {
|
||||
|
||||
return TabCreationMenu(
|
||||
controller: tabMenuController,
|
||||
child: TabsActionButton(
|
||||
child: TabsCountButtonView(
|
||||
isActive: displayedSheet is ViewTabsSheet,
|
||||
onTap: () async {
|
||||
final tabViewBottomSheet = ref
|
||||
|
||||
+88
-51
@@ -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/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 {
|
||||
const NavigateForwardButton({
|
||||
super.key,
|
||||
@@ -45,25 +91,21 @@ class NavigateForwardButton extends HookConsumerWidget {
|
||||
selectedTabId: selectedTabId,
|
||||
controller: historyMenuController,
|
||||
direction: HistoryMenuDirection.forward,
|
||||
child: IconButton(
|
||||
onPressed: canGoForward
|
||||
? () async {
|
||||
final controller = ref.read(
|
||||
tabSessionProvider(tabId: selectedTabId).notifier,
|
||||
);
|
||||
child: NavigateForwardButtonView(
|
||||
canGoForward: canGoForward,
|
||||
onPressed: () async {
|
||||
final controller = ref.read(
|
||||
tabSessionProvider(tabId: selectedTabId).notifier,
|
||||
);
|
||||
|
||||
await controller.goForward();
|
||||
menuControllerToClose?.close();
|
||||
}
|
||||
: null,
|
||||
onLongPress: canGoForward
|
||||
? () {
|
||||
if (!historyMenuController.isOpen) {
|
||||
historyMenuController.open();
|
||||
}
|
||||
}
|
||||
: null,
|
||||
icon: const Icon(Icons.arrow_forward),
|
||||
await controller.goForward();
|
||||
menuControllerToClose?.close();
|
||||
},
|
||||
onLongPress: () {
|
||||
if (!historyMenuController.isOpen) {
|
||||
historyMenuController.open();
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -91,42 +133,37 @@ class NavigateBackButton extends HookConsumerWidget {
|
||||
selectedTabId: selectedTabId,
|
||||
controller: historyMenuController,
|
||||
direction: HistoryMenuDirection.back,
|
||||
child: IconButton(
|
||||
onPressed: (canGoBack || isLoading)
|
||||
? () async {
|
||||
final controller = ref.read(
|
||||
tabSessionProvider(tabId: selectedTabId).notifier,
|
||||
);
|
||||
child: NavigateBackButtonView(
|
||||
canGoBack: canGoBack,
|
||||
isLoading: isLoading,
|
||||
onPressed: () async {
|
||||
final controller = ref.read(
|
||||
tabSessionProvider(tabId: selectedTabId).notifier,
|
||||
);
|
||||
|
||||
final isReaderActive = ref.read(
|
||||
selectedTabStateProvider.select(
|
||||
(state) => state?.readerableState.active ?? false,
|
||||
),
|
||||
);
|
||||
final isReaderActive = ref.read(
|
||||
selectedTabStateProvider.select(
|
||||
(state) => state?.readerableState.active ?? false,
|
||||
),
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
await controller.stopLoading();
|
||||
} else if (isReaderActive) {
|
||||
await ref
|
||||
.read(readerableScreenControllerProvider.notifier)
|
||||
.toggleReaderView(false);
|
||||
} else {
|
||||
await controller.goBack();
|
||||
}
|
||||
if (isLoading) {
|
||||
await controller.stopLoading();
|
||||
} else if (isReaderActive) {
|
||||
await ref
|
||||
.read(readerableScreenControllerProvider.notifier)
|
||||
.toggleReaderView(false);
|
||||
} else {
|
||||
await controller.goBack();
|
||||
}
|
||||
|
||||
menuControllerToClose?.close();
|
||||
}
|
||||
: null,
|
||||
onLongPress: (canGoBack && !isLoading)
|
||||
? () {
|
||||
if (!historyMenuController.isOpen) {
|
||||
historyMenuController.open();
|
||||
}
|
||||
}
|
||||
: null,
|
||||
icon: isLoading
|
||||
? const Icon(Icons.close)
|
||||
: const Icon(Icons.arrow_back),
|
||||
menuControllerToClose?.close();
|
||||
},
|
||||
onLongPress: () {
|
||||
if (!historyMenuController.isOpen) {
|
||||
historyMenuController.open();
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
+67
-63
@@ -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/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 {
|
||||
final bool isActive;
|
||||
final VoidCallback? onTap;
|
||||
@@ -42,78 +92,32 @@ class TabsActionButton extends HookConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
final tabCount = isActive
|
||||
// ignore: provider_parameters
|
||||
? ref.watch(containerTabCountProvider(ContainerFilterDisabled()))
|
||||
: ref.watch(selectedContainerTabCountProvider);
|
||||
|
||||
final iconColor = isActive
|
||||
? theme.colorScheme.primary
|
||||
: theme.colorScheme.onSurfaceVariant;
|
||||
final tabCountText = tabCount.when(
|
||||
skipLoadingOnReload: true,
|
||||
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,
|
||||
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: 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,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user