diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/app_bar_title.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/app_bar_title.dart index c483bc4f..6c7ec183 100644 --- a/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/app_bar_title.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/app_bar_title.dart @@ -35,6 +35,141 @@ 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 { + 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( + showSiteSettingsBadgeProvider.select((value) => value.value == true), + ); + + if (tabState == null) { + 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 Row( + children: [ + ToolbarButton( + onTap: () { + ref + .read(bottomSheetControllerProvider.notifier) + .show(SiteSettingsSheet(tabState: tabState)); + }, + child: Padding( + padding: const EdgeInsets.only(right: 4.0), + child: Stack( + clipBehavior: Clip.none, + children: [ + TabIcon(tabState: tabState, iconSize: 24), + Positioned( + top: -4, + right: -4, + child: Icon( + MdiIcons.shieldHalfFull, + size: 10, + color: showSiteSettingsBadge + ? appColors.warningAmber + : Colors.green, + ), + ), + ], + ), + ), + ), + 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); + }, + child: Container( + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12), + decoration: BoxDecoration( + color: theme.colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular(24), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + if (tabState.tabMode is PrivateTabMode) ...[ + Icon( + MdiIcons.dominoMask, + color: appColors.privateTabPurple, + size: 16, + ), + const SizedBox(width: 4), + ] else if (tabState.tabMode is IsolatedTabMode) ...[ + Icon( + MdiIcons.snowflake, + color: appColors.isolatedTabTeal, + size: 16, + ), + const SizedBox(width: 4), + ], + if (isTabTuneledAsync.hasValue && + isTabTuneledAsync.value == true) ...[ + const Icon(MdiIcons.tunnelOutline, size: 16), + const SizedBox(width: 4), + ], + icon, + const SizedBox(width: 6), + Flexible( + child: UriBreadcrumb( + uri: tabState.url, + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurface, + ), + ), + ), + ], + ), + ), + ), + ), + const SizedBox(width: 8.0), + ], + ); + } +} + class AppBarTitle extends HookConsumerWidget { const AppBarTitle({super.key}); diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/bottom_app_bar.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/bottom_app_bar.dart index 62b1c875..9bf51a50 100644 --- a/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/bottom_app_bar.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/bottom_app_bar.dart @@ -291,7 +291,9 @@ class BrowserTabBar extends HookConsumerWidget { : null, title: (selectedTabId != null && displayedSheet is! ViewTabsSheet) - ? const AppBarTitle() + ? settings.tabBarLayout == TabBarLayout.compact + ? const CompactAppBarTitle() + : const AppBarTitle() : null, actions: [ if (selectedTabId != null && displayedSheet is! ViewTabsSheet) diff --git a/app/lib/features/settings/presentation/screens/appearance_display_settings.dart b/app/lib/features/settings/presentation/screens/appearance_display_settings.dart index 7b399a61..1a26ad68 100644 --- a/app/lib/features/settings/presentation/screens/appearance_display_settings.dart +++ b/app/lib/features/settings/presentation/screens/appearance_display_settings.dart @@ -76,6 +76,7 @@ class _TabBarLayoutSection extends StatelessWidget { children: [ SettingSection(name: 'Tab Bar Layout'), _TabBarPositionSection(), + _TabBarLayoutModeSection(), _ShowContextualTabBarTile(), _AutoHideTabBarTile(), _BottomSheetTabViewTile(), @@ -287,6 +288,59 @@ class _TabBarPositionSection extends HookConsumerWidget { } } +class _TabBarLayoutModeSection extends HookConsumerWidget { + const _TabBarLayoutModeSection(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final tabBarLayout = ref.watch( + generalSettingsWithDefaultsProvider.select((s) => s.tabBarLayout), + ); + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const ListTile( + title: Text('Tab Bar Style'), + leading: Icon(MdiIcons.tabUnselected), + contentPadding: EdgeInsets.zero, + ), + RadioGroup( + groupValue: tabBarLayout, + onChanged: (value) async { + if (value != null) { + await ref + .read(saveGeneralSettingsControllerProvider.notifier) + .save( + (currentSettings) => + currentSettings.copyWith.tabBarLayout(value), + ); + } + }, + child: const Column( + children: [ + RadioListTile.adaptive( + value: TabBarLayout.withTitle, + title: Text('With Title'), + subtitle: Text('Shows page title and URL breadcrumb'), + ), + RadioListTile.adaptive( + value: TabBarLayout.compact, + title: Text('Compact'), + subtitle: Text('Centered URL pill without page title'), + ), + ], + ), + ), + ], + ), + ); + } +} + class _ShowContextualTabBarTile extends HookConsumerWidget { const _ShowContextualTabBarTile(); diff --git a/app/lib/features/user/data/models/general_settings.dart b/app/lib/features/user/data/models/general_settings.dart index adb0c777..d7f8b949 100644 --- a/app/lib/features/user/data/models/general_settings.dart +++ b/app/lib/features/user/data/models/general_settings.dart @@ -42,6 +42,8 @@ enum TabIntentOpenSetting { regular, private, ask } enum TabBarPosition { top, bottom } +enum TabBarLayout { withTitle, compact } + enum DeleteBrowsingDataType { tabs('Open tabs'), history('Browsing history'), @@ -82,6 +84,7 @@ class GeneralSettings with FastEquatable { final bool tabBarShowContextualBar; final bool tabBarShowQuickTabSwitcherBar; final TabBarPosition tabBarPosition; + final TabBarLayout tabBarLayout; final QuickTabSwitcherMode quickTabSwitcherMode; final bool pullToRefreshEnabled; final bool useExternalDownloadManager; @@ -127,6 +130,7 @@ class GeneralSettings with FastEquatable { required this.tabBarShowContextualBar, required this.tabBarShowQuickTabSwitcherBar, required this.tabBarPosition, + required this.tabBarLayout, required this.quickTabSwitcherMode, required this.pullToRefreshEnabled, required this.useExternalDownloadManager, @@ -173,6 +177,7 @@ class GeneralSettings with FastEquatable { bool? tabBarShowContextualBar, bool? tabBarShowQuickTabSwitcherBar, TabBarPosition? tabBarPosition, + TabBarLayout? tabBarLayout, QuickTabSwitcherMode? quickTabSwitcherMode, bool? pullToRefreshEnabled, bool? useExternalDownloadManager, @@ -219,6 +224,7 @@ class GeneralSettings with FastEquatable { tabBarShowContextualBar = tabBarShowContextualBar ?? true, tabBarShowQuickTabSwitcherBar = tabBarShowQuickTabSwitcherBar ?? true, tabBarPosition = tabBarPosition ?? TabBarPosition.bottom, + tabBarLayout = tabBarLayout ?? TabBarLayout.withTitle, quickTabSwitcherMode = quickTabSwitcherMode ?? QuickTabSwitcherMode.lastUsedTabs, pullToRefreshEnabled = pullToRefreshEnabled ?? true, @@ -292,6 +298,7 @@ class GeneralSettings with FastEquatable { tabBarShowContextualBar, tabBarShowQuickTabSwitcherBar, tabBarPosition, + tabBarLayout, quickTabSwitcherMode, pullToRefreshEnabled, useExternalDownloadManager, diff --git a/app/lib/features/user/data/models/general_settings.g.dart b/app/lib/features/user/data/models/general_settings.g.dart index b8e15542..7dd0d31d 100644 --- a/app/lib/features/user/data/models/general_settings.g.dart +++ b/app/lib/features/user/data/models/general_settings.g.dart @@ -59,6 +59,8 @@ abstract class _$GeneralSettingsCWProxy { GeneralSettings tabBarPosition(TabBarPosition tabBarPosition); + GeneralSettings tabBarLayout(TabBarLayout tabBarLayout); + GeneralSettings quickTabSwitcherMode( QuickTabSwitcherMode quickTabSwitcherMode, ); @@ -140,6 +142,7 @@ abstract class _$GeneralSettingsCWProxy { bool tabBarShowContextualBar, bool tabBarShowQuickTabSwitcherBar, TabBarPosition tabBarPosition, + TabBarLayout tabBarLayout, QuickTabSwitcherMode quickTabSwitcherMode, bool pullToRefreshEnabled, bool useExternalDownloadManager, @@ -260,6 +263,10 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy { GeneralSettings tabBarPosition(TabBarPosition tabBarPosition) => call(tabBarPosition: tabBarPosition); + @override + GeneralSettings tabBarLayout(TabBarLayout tabBarLayout) => + call(tabBarLayout: tabBarLayout); + @override GeneralSettings quickTabSwitcherMode( QuickTabSwitcherMode quickTabSwitcherMode, @@ -386,6 +393,7 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy { Object? tabBarShowContextualBar = const $CopyWithPlaceholder(), Object? tabBarShowQuickTabSwitcherBar = const $CopyWithPlaceholder(), Object? tabBarPosition = const $CopyWithPlaceholder(), + Object? tabBarLayout = const $CopyWithPlaceholder(), Object? quickTabSwitcherMode = const $CopyWithPlaceholder(), Object? pullToRefreshEnabled = const $CopyWithPlaceholder(), Object? useExternalDownloadManager = const $CopyWithPlaceholder(), @@ -533,6 +541,11 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy { ? _value.tabBarPosition // ignore: cast_nullable_to_non_nullable : tabBarPosition as TabBarPosition, + tabBarLayout: + tabBarLayout == const $CopyWithPlaceholder() || tabBarLayout == null + ? _value.tabBarLayout + // ignore: cast_nullable_to_non_nullable + : tabBarLayout as TabBarLayout, quickTabSwitcherMode: quickTabSwitcherMode == const $CopyWithPlaceholder() || quickTabSwitcherMode == null @@ -727,6 +740,10 @@ GeneralSettings _$GeneralSettingsFromJson( _$TabBarPositionEnumMap, json['tabBarPosition'], ), + tabBarLayout: $enumDecodeNullable( + _$TabBarLayoutEnumMap, + json['tabBarLayout'], + ), quickTabSwitcherMode: $enumDecodeNullable( _$QuickTabSwitcherModeEnumMap, json['quickTabSwitcherMode'], @@ -795,6 +812,7 @@ Map _$GeneralSettingsToJson( 'tabBarShowContextualBar': instance.tabBarShowContextualBar, 'tabBarShowQuickTabSwitcherBar': instance.tabBarShowQuickTabSwitcherBar, 'tabBarPosition': _$TabBarPositionEnumMap[instance.tabBarPosition]!, + 'tabBarLayout': _$TabBarLayoutEnumMap[instance.tabBarLayout]!, 'quickTabSwitcherMode': _$QuickTabSwitcherModeEnumMap[instance.quickTabSwitcherMode]!, 'pullToRefreshEnabled': instance.pullToRefreshEnabled, @@ -868,6 +886,11 @@ const _$TabBarPositionEnumMap = { TabBarPosition.bottom: 'bottom', }; +const _$TabBarLayoutEnumMap = { + TabBarLayout.withTitle: 'withTitle', + TabBarLayout.compact: 'compact', +}; + const _$QuickTabSwitcherModeEnumMap = { QuickTabSwitcherMode.lastUsedTabs: 'lastUsedTabs', QuickTabSwitcherMode.containerTabs: 'containerTabs', diff --git a/app/lib/features/user/domain/repositories/general_settings.dart b/app/lib/features/user/domain/repositories/general_settings.dart index ecae9cf1..4a13efa7 100644 --- a/app/lib/features/user/domain/repositories/general_settings.dart +++ b/app/lib/features/user/domain/repositories/general_settings.dart @@ -126,6 +126,10 @@ class GeneralSettingsRepository extends _$GeneralSettingsRepository { DriftSqlType.string, db.typeMapping, ), + 'tabBarLayout': settings['tabBarLayout']?.readAs( + DriftSqlType.string, + db.typeMapping, + ), 'quickTabSwitcherMode': settings['quickTabSwitcherMode']?.readAs( DriftSqlType.string, db.typeMapping, diff --git a/app/lib/features/user/domain/repositories/general_settings.g.dart b/app/lib/features/user/domain/repositories/general_settings.g.dart index c7972911..63cf633c 100644 --- a/app/lib/features/user/domain/repositories/general_settings.g.dart +++ b/app/lib/features/user/domain/repositories/general_settings.g.dart @@ -35,7 +35,7 @@ final class GeneralSettingsRepositoryProvider } String _$generalSettingsRepositoryHash() => - r'2e987b58279ea638157324a03403dfac8437b888'; + r'23b15af79f0f41771c20f7c0f7e94f93659ee1e8'; abstract class _$GeneralSettingsRepository extends $StreamNotifier {