tab edit initial
This commit is contained in:
@@ -136,10 +136,16 @@ class SearchRoute extends GoRouteData with $SearchRoute {
|
||||
|
||||
final bool launchedFromIntent;
|
||||
|
||||
/// When provided, the search screen will load URLs into this existing tab
|
||||
/// instead of creating a new tab. This also changes the UI to show
|
||||
/// site-specific bangs instead of the tab type selector.
|
||||
final String? tabId;
|
||||
|
||||
const SearchRoute({
|
||||
required this.tabType,
|
||||
this.searchText = SearchRoute.emptySearchText,
|
||||
this.launchedFromIntent = false,
|
||||
this.tabId,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -150,6 +156,7 @@ class SearchRoute extends GoRouteData with $SearchRoute {
|
||||
? null
|
||||
: searchText,
|
||||
launchedFromIntent: launchedFromIntent,
|
||||
tabId: tabId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -637,6 +637,7 @@ mixin $SearchRoute on GoRouteData {
|
||||
_$boolConverter,
|
||||
) ??
|
||||
false,
|
||||
tabId: state.uri.queryParameters['tab-id'],
|
||||
);
|
||||
|
||||
SearchRoute get _self => this as SearchRoute;
|
||||
@@ -647,6 +648,7 @@ mixin $SearchRoute on GoRouteData {
|
||||
queryParams: {
|
||||
if (_self.launchedFromIntent != false)
|
||||
'launched-from-intent': _self.launchedFromIntent.toString(),
|
||||
if (_self.tabId != null) 'tab-id': _self.tabId,
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -27,10 +27,10 @@ final class ViewTabsSheet extends Sheet {
|
||||
List<Object?> get hashParameters => [null];
|
||||
}
|
||||
|
||||
final class EditUrlSheet extends Sheet {
|
||||
final class SiteSettingsSheet extends Sheet {
|
||||
final TabState tabState;
|
||||
|
||||
EditUrlSheet({required this.tabState});
|
||||
SiteSettingsSheet({required this.tabState});
|
||||
|
||||
@override
|
||||
List<Object?> get hashParameters => [tabState];
|
||||
|
||||
@@ -591,7 +591,7 @@ class _SheetContainer extends HookConsumerWidget {
|
||||
onNotification: dismissOnThreshold,
|
||||
child: _ViewTabsSheet(maxChildSize: stableMaxChildSize),
|
||||
),
|
||||
final EditUrlSheet parameter =>
|
||||
final SiteSettingsSheet parameter =>
|
||||
NotificationListener<DraggableScrollableNotification>(
|
||||
onNotification: dismissOnThreshold,
|
||||
child: _ViewUrlSheet(
|
||||
|
||||
+81
-53
@@ -24,7 +24,10 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
import 'package:text_scroll/text_scroll.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/providers/tab_state.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/domain/entities/sheet.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_icon.dart';
|
||||
import 'package:weblibre/presentation/widgets/uri_breadcrumb.dart';
|
||||
|
||||
@@ -67,66 +70,91 @@ class AppBarTitle extends HookConsumerWidget {
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
TabIcon(tabState: tabState),
|
||||
// Icon tap → opens site settings sheet
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
ref
|
||||
.read(bottomSheetControllerProvider.notifier)
|
||||
.show(SiteSettingsSheet(tabState: tabState));
|
||||
},
|
||||
child: TabIcon(tabState: tabState),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
// Title/URL tap → opens search screen
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Skeletonizer(
|
||||
enabled: tabState.title.isEmpty,
|
||||
child: Skeleton.replace(
|
||||
replacement: const Padding(
|
||||
padding: EdgeInsets.only(right: 4, top: 1, bottom: 1),
|
||||
child: Bone.text(),
|
||||
),
|
||||
child: TextScroll(
|
||||
key: ValueKey(tabState.title),
|
||||
tabState.title,
|
||||
style: theme.textTheme.bodyLarge?.copyWith(
|
||||
color: theme.colorScheme.onSurface,
|
||||
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.isPrivate ? TabType.private : TabType.regular,
|
||||
).push(context);
|
||||
},
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Skeletonizer(
|
||||
enabled: tabState.title.isEmpty,
|
||||
child: Skeleton.replace(
|
||||
replacement: const Padding(
|
||||
padding: EdgeInsets.only(right: 4, top: 1, bottom: 1),
|
||||
child: Bone.text(),
|
||||
),
|
||||
// mode: TextScrollMode.bouncing,
|
||||
velocity: const Velocity(pixelsPerSecond: Offset(75, 0)),
|
||||
delayBefore: const Duration(milliseconds: 500),
|
||||
pauseBetween: const Duration(milliseconds: 5000),
|
||||
fadedBorder: true,
|
||||
fadeBorderSide: FadeBorderSide.right,
|
||||
fadedBorderWidth: 0.05,
|
||||
intervalSpaces: 4,
|
||||
numberOfReps: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
if (isTabTuneledAsync.hasValue &&
|
||||
isTabTuneledAsync.value == true) ...[
|
||||
const Icon(MdiIcons.tunnelOutline, size: 14),
|
||||
const SizedBox(width: 4),
|
||||
],
|
||||
icon,
|
||||
const SizedBox(width: 4),
|
||||
if (tabState.isPrivate) ...[
|
||||
Icon(
|
||||
MdiIcons.dominoMask,
|
||||
color: appColors.privateTabPurple,
|
||||
size: 14,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
],
|
||||
Expanded(
|
||||
child: UriBreadcrumb(
|
||||
uri: tabState.url,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
child: TextScroll(
|
||||
key: ValueKey(tabState.title),
|
||||
tabState.title,
|
||||
style: theme.textTheme.bodyLarge?.copyWith(
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
// mode: TextScrollMode.bouncing,
|
||||
velocity: const Velocity(pixelsPerSecond: Offset(75, 0)),
|
||||
delayBefore: const Duration(milliseconds: 500),
|
||||
pauseBetween: const Duration(milliseconds: 5000),
|
||||
fadedBorder: true,
|
||||
fadeBorderSide: FadeBorderSide.right,
|
||||
fadedBorderWidth: 0.05,
|
||||
intervalSpaces: 4,
|
||||
numberOfReps: 2,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
if (isTabTuneledAsync.hasValue &&
|
||||
isTabTuneledAsync.value == true) ...[
|
||||
const Icon(MdiIcons.tunnelOutline, size: 14),
|
||||
const SizedBox(width: 4),
|
||||
],
|
||||
icon,
|
||||
const SizedBox(width: 4),
|
||||
if (tabState.isPrivate) ...[
|
||||
Icon(
|
||||
MdiIcons.dominoMask,
|
||||
color: appColors.privateTabPurple,
|
||||
size: 14,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
],
|
||||
Expanded(
|
||||
child: UriBreadcrumb(
|
||||
uri: tabState.url,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
+1
-14
@@ -218,20 +218,7 @@ class BrowserTabBar extends HookConsumerWidget {
|
||||
final toolbarHeight = useMemoized(() => getToolbarHeight());
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
if (displayedSheet case EditUrlSheet()) {
|
||||
ref.read(bottomSheetControllerProvider.notifier).requestDismiss();
|
||||
return;
|
||||
}
|
||||
|
||||
final tabState = ref.read(selectedTabStateProvider);
|
||||
|
||||
if (tabState != null) {
|
||||
ref
|
||||
.read(bottomSheetControllerProvider.notifier)
|
||||
.show(EditUrlSheet(tabState: tabState));
|
||||
}
|
||||
},
|
||||
// Tap handling moved to AppBarTitle for split icon/title behavior
|
||||
onHorizontalDragStart: (details) {
|
||||
dragStartPosition.value = details.globalPosition;
|
||||
},
|
||||
|
||||
+20
-91
@@ -22,8 +22,7 @@ import 'package:fading_scroll/fading_scroll.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:weblibre/features/bangs/domain/providers/bangs.dart';
|
||||
import 'package:weblibre/features/bangs/presentation/widgets/site_search.dart';
|
||||
import 'package:weblibre/core/routing/routes.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/entities/states/tab.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/certificate_tile.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/draggable_scrollable_header.dart';
|
||||
@@ -64,64 +63,6 @@ class ViewTabSheetWidget extends HookConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final headerKey = useMemoized(() => GlobalKey());
|
||||
final textFieldKey = useMemoized(() => GlobalKey());
|
||||
|
||||
final scrolledTo = useRef(0.0);
|
||||
|
||||
final searchTextController = useTextEditingController(
|
||||
text: initialTabState.url.toString(),
|
||||
);
|
||||
|
||||
Future<void> scroll() async {
|
||||
if (!context.mounted) return;
|
||||
|
||||
final header = headerKey.currentContext?.findRenderObject();
|
||||
final text = textFieldKey.currentContext?.findRenderObject();
|
||||
|
||||
if (header case final RenderBox headerBox) {
|
||||
if (text case final RenderBox textBox) {
|
||||
final totalHeight =
|
||||
headerBox.size.height +
|
||||
textBox.size.height +
|
||||
bottomAppBarHeight +
|
||||
MediaQuery.of(context).viewInsets.bottom;
|
||||
|
||||
final relative = (totalHeight / MediaQuery.of(context).size.height)
|
||||
.clamp(0.0, 1.0);
|
||||
|
||||
if (draggableScrollableController.size < relative &&
|
||||
relative > scrolledTo.value) {
|
||||
await draggableScrollableController.animateTo(
|
||||
relative,
|
||||
duration: const Duration(milliseconds: 150),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
scrolledTo.value = relative;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
useOnListenableChange(searchTextController, () {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
await scroll();
|
||||
});
|
||||
});
|
||||
|
||||
final availableBangs = ref.watch(
|
||||
bangListProvider(
|
||||
domain: initialTabState.url.host,
|
||||
orderMostFrequentFirst: true,
|
||||
).select((value) => value.value ?? const []),
|
||||
);
|
||||
|
||||
useEffect(() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
await scroll();
|
||||
});
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
final bottomInsets = useRef(0.0);
|
||||
useEffect(
|
||||
@@ -164,21 +105,23 @@ class ViewTabSheetWidget extends HookConsumerWidget {
|
||||
padding: const EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 0.0),
|
||||
child: GestureDetector(
|
||||
onTap: () async {
|
||||
if (draggableScrollableController.size > 0.85) {
|
||||
await draggableScrollableController.animateTo(
|
||||
(scrolledTo.value > 0.0)
|
||||
? scrolledTo.value
|
||||
: initialHeight,
|
||||
duration: const Duration(milliseconds: 150),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
} else {
|
||||
await draggableScrollableController.animateTo(
|
||||
1.0,
|
||||
duration: const Duration(milliseconds: 150),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
}
|
||||
// Dismiss sheet and open search screen with tab context
|
||||
onClose();
|
||||
|
||||
// Don't pre-fill for internal URLs
|
||||
final searchText = initialTabState.url.scheme == 'about'
|
||||
? ''
|
||||
: initialTabState.url.toString();
|
||||
|
||||
await SearchRoute(
|
||||
tabId: initialTabState.id,
|
||||
searchText: searchText.isEmpty
|
||||
? SearchRoute.emptySearchText
|
||||
: searchText,
|
||||
tabType: initialTabState.isPrivate
|
||||
? TabType.private
|
||||
: TabType.regular,
|
||||
).push(context);
|
||||
},
|
||||
child: WebsiteTitleTile(initialTabState),
|
||||
),
|
||||
@@ -199,20 +142,8 @@ class ViewTabSheetWidget extends HookConsumerWidget {
|
||||
controller: controller,
|
||||
physics: const ClampingScrollPhysicsWithoutImplicit(),
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: SiteSearch(
|
||||
key: textFieldKey,
|
||||
domain: initialTabState.url.host,
|
||||
availableBangs: availableBangs,
|
||||
controller: searchTextController,
|
||||
),
|
||||
),
|
||||
const Divider(),
|
||||
// Tracking Protection Section
|
||||
TrackingProtectionSection(
|
||||
tabId: initialTabState.id,
|
||||
),
|
||||
TrackingProtectionSection(tabId: initialTabState.id),
|
||||
const Divider(),
|
||||
// Permissions Section
|
||||
PermissionsSection(
|
||||
@@ -221,9 +152,7 @@ class ViewTabSheetWidget extends HookConsumerWidget {
|
||||
),
|
||||
const Divider(),
|
||||
// Clear Site Data Section
|
||||
ClearSiteDataSection(
|
||||
url: initialTabState.url,
|
||||
),
|
||||
ClearSiteDataSection(url: initialTabState.url),
|
||||
const SizedBox(height: 16.0),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -29,6 +29,7 @@ import 'package:weblibre/features/bangs/domain/providers/bangs.dart';
|
||||
import 'package:weblibre/features/bangs/domain/providers/search.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/controllers/bottom_sheet.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/providers/selected_tab.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/providers/tab_session.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/features/browser/domain/providers.dart';
|
||||
@@ -40,6 +41,8 @@ import 'package:weblibre/features/geckoview/features/search/presentation/widgets
|
||||
import 'package:weblibre/features/geckoview/features/search/presentation/widgets/search_modules/tab_search.dart';
|
||||
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
||||
import 'package:weblibre/presentation/hooks/sampled_value_notifier.dart';
|
||||
import 'package:weblibre/presentation/widgets/selectable_chips.dart';
|
||||
import 'package:weblibre/presentation/widgets/url_icon.dart';
|
||||
import 'package:weblibre/utils/uri_parser.dart' as uri_parser;
|
||||
|
||||
class SearchScreen extends HookConsumerWidget {
|
||||
@@ -47,10 +50,15 @@ class SearchScreen extends HookConsumerWidget {
|
||||
final TabType tabType;
|
||||
final bool launchedFromIntent;
|
||||
|
||||
/// When provided, URLs will be loaded into this existing tab.
|
||||
/// When null, a new tab will be created.
|
||||
final String? tabId;
|
||||
|
||||
const SearchScreen({
|
||||
required this.initialSearchText,
|
||||
required this.tabType,
|
||||
this.launchedFromIntent = false,
|
||||
this.tabId,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -67,11 +75,23 @@ class SearchScreen extends HookConsumerWidget {
|
||||
final selectedTabType = useState(tabType);
|
||||
final currentTabTabType = ref.watch(selectedTabTypeProvider);
|
||||
|
||||
final privateTabMode = switch (selectedTabType.value) {
|
||||
TabType.regular => false,
|
||||
TabType.private => true,
|
||||
TabType.child => currentTabTabType == TabType.private,
|
||||
};
|
||||
// When editing an existing tab, get its state
|
||||
// If tab no longer exists (null), fall back to new tab mode
|
||||
final existingTabState = tabId != null
|
||||
? ref.watch(tabStateProvider(tabId))
|
||||
: null;
|
||||
|
||||
// Determine if we're in edit mode (tabId provided AND tab still exists)
|
||||
final isEditMode = tabId != null && existingTabState != null;
|
||||
|
||||
// Derive private mode from existing tab or from selector
|
||||
final privateTabMode = isEditMode
|
||||
? existingTabState.isPrivate
|
||||
: switch (selectedTabType.value) {
|
||||
TabType.regular => false,
|
||||
TabType.private => true,
|
||||
TabType.child => currentTabTabType == TabType.private,
|
||||
};
|
||||
|
||||
final searchTextController = useTextEditingController(
|
||||
text: initialSearchText,
|
||||
@@ -105,13 +125,16 @@ class SearchScreen extends HookConsumerWidget {
|
||||
final activeBang = selectedBang.value ?? defaultSearchBang;
|
||||
final showBangIcon = useState(false);
|
||||
|
||||
ref.listen(selectedBangDataProvider(), (previous, next) {
|
||||
if (previous != next) {
|
||||
showBangIcon.value = true;
|
||||
}
|
||||
ref.listen(
|
||||
selectedBangDataProvider(domain: isEditMode ? existingTabState.url.host : null),
|
||||
(previous, next) {
|
||||
if (previous != next) {
|
||||
showBangIcon.value = true;
|
||||
}
|
||||
|
||||
selectedBang.value = next;
|
||||
});
|
||||
selectedBang.value = next;
|
||||
},
|
||||
);
|
||||
|
||||
Future<void> submitSearch(String query) async {
|
||||
if (activeBang != null && (formKey.currentState?.validate() == true)) {
|
||||
@@ -123,17 +146,25 @@ class SearchScreen extends HookConsumerWidget {
|
||||
.triggerBangSearch(activeBang, query);
|
||||
}
|
||||
|
||||
await ref
|
||||
.read(tabRepositoryProvider.notifier)
|
||||
.addTab(
|
||||
url: searchUri,
|
||||
private: privateTabMode,
|
||||
parentId: (selectedTabType.value == TabType.child)
|
||||
? ref.read(selectedTabProvider)
|
||||
: null,
|
||||
launchedFromIntent: launchedFromIntent,
|
||||
selectTab: true,
|
||||
);
|
||||
if (isEditMode) {
|
||||
// Load into existing tab
|
||||
await ref
|
||||
.read(tabSessionProvider(tabId: tabId).notifier)
|
||||
.loadUrl(url: searchUri);
|
||||
} else {
|
||||
// Create new tab
|
||||
await ref
|
||||
.read(tabRepositoryProvider.notifier)
|
||||
.addTab(
|
||||
url: searchUri,
|
||||
private: privateTabMode,
|
||||
parentId: (selectedTabType.value == TabType.child)
|
||||
? ref.read(selectedTabProvider)
|
||||
: null,
|
||||
launchedFromIntent: launchedFromIntent,
|
||||
selectTab: true,
|
||||
);
|
||||
}
|
||||
|
||||
if (context.mounted) {
|
||||
ref.read(bottomSheetControllerProvider.notifier).requestDismiss();
|
||||
@@ -156,54 +187,70 @@ class SearchScreen extends HookConsumerWidget {
|
||||
floating: true,
|
||||
pinned: true,
|
||||
automaticallyImplyLeading: false,
|
||||
title: Align(
|
||||
child: Focus(
|
||||
canRequestFocus: false,
|
||||
child: SegmentedButton(
|
||||
showSelectedIcon: false,
|
||||
segments: [
|
||||
const ButtonSegment(
|
||||
value: TabType.regular,
|
||||
label: Text('Regular'),
|
||||
icon: Icon(MdiIcons.tab),
|
||||
),
|
||||
const ButtonSegment(
|
||||
value: TabType.private,
|
||||
label: Text('Private'),
|
||||
icon: Icon(MdiIcons.tabUnselected),
|
||||
),
|
||||
if (createChildTabsOption)
|
||||
const ButtonSegment(
|
||||
value: TabType.child,
|
||||
label: Text('Child'),
|
||||
icon: Icon(MdiIcons.fileTree),
|
||||
title: isEditMode
|
||||
? Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_SiteBangsSelector(
|
||||
tabId: tabId!,
|
||||
domain: existingTabState.url.host,
|
||||
),
|
||||
Text(
|
||||
'Editing current tab',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: Align(
|
||||
child: Focus(
|
||||
canRequestFocus: false,
|
||||
child: SegmentedButton(
|
||||
showSelectedIcon: false,
|
||||
segments: [
|
||||
const ButtonSegment(
|
||||
value: TabType.regular,
|
||||
label: Text('Regular'),
|
||||
icon: Icon(MdiIcons.tab),
|
||||
),
|
||||
const ButtonSegment(
|
||||
value: TabType.private,
|
||||
label: Text('Private'),
|
||||
icon: Icon(MdiIcons.tabUnselected),
|
||||
),
|
||||
if (createChildTabsOption)
|
||||
const ButtonSegment(
|
||||
value: TabType.child,
|
||||
label: Text('Child'),
|
||||
icon: Icon(MdiIcons.fileTree),
|
||||
),
|
||||
],
|
||||
selected: {selectedTabType.value},
|
||||
onSelectionChanged: (value) {
|
||||
selectedTabType.value = value.first;
|
||||
// Restore focus to search field after segment change
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
searchFocusNode.requestFocus();
|
||||
});
|
||||
},
|
||||
style: switch (selectedTabType.value) {
|
||||
TabType.regular => null,
|
||||
TabType.private => SegmentedButton.styleFrom(
|
||||
selectedBackgroundColor:
|
||||
appColors.privateSelectionOverlay,
|
||||
),
|
||||
TabType.child =>
|
||||
(currentTabTabType == TabType.private)
|
||||
? SegmentedButton.styleFrom(
|
||||
selectedBackgroundColor:
|
||||
appColors.privateSelectionOverlay,
|
||||
)
|
||||
: null,
|
||||
},
|
||||
),
|
||||
],
|
||||
selected: {selectedTabType.value},
|
||||
onSelectionChanged: (value) {
|
||||
selectedTabType.value = value.first;
|
||||
// Restore focus to search field after segment change
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
searchFocusNode.requestFocus();
|
||||
});
|
||||
},
|
||||
style: switch (selectedTabType.value) {
|
||||
TabType.regular => null,
|
||||
TabType.private => SegmentedButton.styleFrom(
|
||||
selectedBackgroundColor:
|
||||
appColors.privateSelectionOverlay,
|
||||
),
|
||||
TabType.child =>
|
||||
(currentTabTabType == TabType.private)
|
||||
? SegmentedButton.styleFrom(
|
||||
selectedBackgroundColor:
|
||||
appColors.privateSelectionOverlay,
|
||||
)
|
||||
: null,
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
bottom: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(kToolbarHeight),
|
||||
child: Padding(
|
||||
@@ -223,7 +270,9 @@ class SearchScreen extends HookConsumerWidget {
|
||||
|
||||
if (newUrl == null) {
|
||||
final bang =
|
||||
ref.read(selectedBangDataProvider()) ??
|
||||
ref.read(selectedBangDataProvider(
|
||||
domain: isEditMode ? existingTabState.url.host : null,
|
||||
)) ??
|
||||
await ref.read(
|
||||
defaultSearchBangDataProvider.future,
|
||||
);
|
||||
@@ -240,19 +289,27 @@ class SearchScreen extends HookConsumerWidget {
|
||||
}
|
||||
|
||||
if (newUrl != null) {
|
||||
await ref
|
||||
.read(tabRepositoryProvider.notifier)
|
||||
.addTab(
|
||||
url: newUrl,
|
||||
private: privateTabMode,
|
||||
parentId:
|
||||
(selectedTabType.value ==
|
||||
TabType.child)
|
||||
? ref.read(selectedTabProvider)
|
||||
: null,
|
||||
launchedFromIntent: launchedFromIntent,
|
||||
selectTab: true,
|
||||
);
|
||||
if (isEditMode) {
|
||||
// Load into existing tab
|
||||
await ref
|
||||
.read(tabSessionProvider(tabId: tabId).notifier)
|
||||
.loadUrl(url: newUrl);
|
||||
} else {
|
||||
// Create new tab
|
||||
await ref
|
||||
.read(tabRepositoryProvider.notifier)
|
||||
.addTab(
|
||||
url: newUrl,
|
||||
private: privateTabMode,
|
||||
parentId:
|
||||
(selectedTabType.value ==
|
||||
TabType.child)
|
||||
? ref.read(selectedTabProvider)
|
||||
: null,
|
||||
launchedFromIntent: launchedFromIntent,
|
||||
selectTab: true,
|
||||
);
|
||||
}
|
||||
|
||||
if (context.mounted) {
|
||||
ref
|
||||
@@ -296,3 +353,56 @@ class SearchScreen extends HookConsumerWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SiteBangsSelector extends HookConsumerWidget {
|
||||
final String tabId;
|
||||
final String domain;
|
||||
|
||||
const _SiteBangsSelector({
|
||||
required this.tabId,
|
||||
required this.domain,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final selectedBang = ref.watch(
|
||||
selectedBangDataProvider(domain: domain),
|
||||
);
|
||||
final availableBangs = ref.watch(
|
||||
bangListProvider(
|
||||
domain: domain,
|
||||
orderMostFrequentFirst: true,
|
||||
).select((value) => value.value ?? const []),
|
||||
);
|
||||
|
||||
if (availableBangs.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return SizedBox(
|
||||
height: 48,
|
||||
width: double.maxFinite,
|
||||
child: SelectableChips(
|
||||
itemId: (bang) => bang.trigger,
|
||||
itemAvatar: (bang) => UrlIcon([bang.getDefaultUrl()], iconSize: 20),
|
||||
itemLabel: (bang) => Text(bang.websiteName),
|
||||
itemTooltip: (bang) => bang.trigger,
|
||||
availableItems: availableBangs,
|
||||
selectedItem: selectedBang,
|
||||
onSelected: (bang) {
|
||||
ref
|
||||
.read(selectedBangTriggerProvider(domain: domain).notifier)
|
||||
.setTrigger(bang.toKey());
|
||||
},
|
||||
onDeleted: (bang) {
|
||||
if (ref.read(selectedBangTriggerProvider(domain: domain)) ==
|
||||
bang.toKey()) {
|
||||
ref
|
||||
.read(selectedBangTriggerProvider(domain: domain).notifier)
|
||||
.clearTrigger();
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user