tab edit initial
This commit is contained in:
@@ -136,10 +136,16 @@ class SearchRoute extends GoRouteData with $SearchRoute {
|
|||||||
|
|
||||||
final bool launchedFromIntent;
|
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({
|
const SearchRoute({
|
||||||
required this.tabType,
|
required this.tabType,
|
||||||
this.searchText = SearchRoute.emptySearchText,
|
this.searchText = SearchRoute.emptySearchText,
|
||||||
this.launchedFromIntent = false,
|
this.launchedFromIntent = false,
|
||||||
|
this.tabId,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -150,6 +156,7 @@ class SearchRoute extends GoRouteData with $SearchRoute {
|
|||||||
? null
|
? null
|
||||||
: searchText,
|
: searchText,
|
||||||
launchedFromIntent: launchedFromIntent,
|
launchedFromIntent: launchedFromIntent,
|
||||||
|
tabId: tabId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -637,6 +637,7 @@ mixin $SearchRoute on GoRouteData {
|
|||||||
_$boolConverter,
|
_$boolConverter,
|
||||||
) ??
|
) ??
|
||||||
false,
|
false,
|
||||||
|
tabId: state.uri.queryParameters['tab-id'],
|
||||||
);
|
);
|
||||||
|
|
||||||
SearchRoute get _self => this as SearchRoute;
|
SearchRoute get _self => this as SearchRoute;
|
||||||
@@ -647,6 +648,7 @@ mixin $SearchRoute on GoRouteData {
|
|||||||
queryParams: {
|
queryParams: {
|
||||||
if (_self.launchedFromIntent != false)
|
if (_self.launchedFromIntent != false)
|
||||||
'launched-from-intent': _self.launchedFromIntent.toString(),
|
'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];
|
List<Object?> get hashParameters => [null];
|
||||||
}
|
}
|
||||||
|
|
||||||
final class EditUrlSheet extends Sheet {
|
final class SiteSettingsSheet extends Sheet {
|
||||||
final TabState tabState;
|
final TabState tabState;
|
||||||
|
|
||||||
EditUrlSheet({required this.tabState});
|
SiteSettingsSheet({required this.tabState});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get hashParameters => [tabState];
|
List<Object?> get hashParameters => [tabState];
|
||||||
|
|||||||
@@ -591,7 +591,7 @@ class _SheetContainer extends HookConsumerWidget {
|
|||||||
onNotification: dismissOnThreshold,
|
onNotification: dismissOnThreshold,
|
||||||
child: _ViewTabsSheet(maxChildSize: stableMaxChildSize),
|
child: _ViewTabsSheet(maxChildSize: stableMaxChildSize),
|
||||||
),
|
),
|
||||||
final EditUrlSheet parameter =>
|
final SiteSettingsSheet parameter =>
|
||||||
NotificationListener<DraggableScrollableNotification>(
|
NotificationListener<DraggableScrollableNotification>(
|
||||||
onNotification: dismissOnThreshold,
|
onNotification: dismissOnThreshold,
|
||||||
child: _ViewUrlSheet(
|
child: _ViewUrlSheet(
|
||||||
|
|||||||
+81
-53
@@ -24,7 +24,10 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:skeletonizer/skeletonizer.dart';
|
import 'package:skeletonizer/skeletonizer.dart';
|
||||||
import 'package:text_scroll/text_scroll.dart';
|
import 'package:text_scroll/text_scroll.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/features/geckoview/domain/controllers/bottom_sheet.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/presentation/widgets/tab_icon.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_icon.dart';
|
||||||
import 'package:weblibre/presentation/widgets/uri_breadcrumb.dart';
|
import 'package:weblibre/presentation/widgets/uri_breadcrumb.dart';
|
||||||
|
|
||||||
@@ -67,66 +70,91 @@ class AppBarTitle extends HookConsumerWidget {
|
|||||||
|
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
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),
|
const SizedBox(width: 8),
|
||||||
|
// Title/URL tap → opens search screen
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: GestureDetector(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
onTap: () async {
|
||||||
mainAxisSize: MainAxisSize.min,
|
// Don't pre-fill for internal URLs
|
||||||
children: [
|
final searchText = tabState.url.scheme == 'about'
|
||||||
Skeletonizer(
|
? ''
|
||||||
enabled: tabState.title.isEmpty,
|
: tabState.url.toString();
|
||||||
child: Skeleton.replace(
|
|
||||||
replacement: const Padding(
|
await SearchRoute(
|
||||||
padding: EdgeInsets.only(right: 4, top: 1, bottom: 1),
|
tabId: tabState.id,
|
||||||
child: Bone.text(),
|
searchText: searchText.isEmpty
|
||||||
),
|
? SearchRoute.emptySearchText
|
||||||
child: TextScroll(
|
: searchText,
|
||||||
key: ValueKey(tabState.title),
|
tabType: tabState.isPrivate ? TabType.private : TabType.regular,
|
||||||
tabState.title,
|
).push(context);
|
||||||
style: theme.textTheme.bodyLarge?.copyWith(
|
},
|
||||||
color: theme.colorScheme.onSurface,
|
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,
|
child: TextScroll(
|
||||||
velocity: const Velocity(pixelsPerSecond: Offset(75, 0)),
|
key: ValueKey(tabState.title),
|
||||||
delayBefore: const Duration(milliseconds: 500),
|
tabState.title,
|
||||||
pauseBetween: const Duration(milliseconds: 5000),
|
style: theme.textTheme.bodyLarge?.copyWith(
|
||||||
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,
|
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());
|
final toolbarHeight = useMemoized(() => getToolbarHeight());
|
||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () {
|
// Tap handling moved to AppBarTitle for split icon/title behavior
|
||||||
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));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onHorizontalDragStart: (details) {
|
onHorizontalDragStart: (details) {
|
||||||
dragStartPosition.value = details.globalPosition;
|
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/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:weblibre/features/bangs/domain/providers/bangs.dart';
|
import 'package:weblibre/core/routing/routes.dart';
|
||||||
import 'package:weblibre/features/bangs/presentation/widgets/site_search.dart';
|
|
||||||
import 'package:weblibre/features/geckoview/domain/entities/states/tab.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/certificate_tile.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/draggable_scrollable_header.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/draggable_scrollable_header.dart';
|
||||||
@@ -64,64 +63,6 @@ class ViewTabSheetWidget extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final headerKey = useMemoized(() => GlobalKey());
|
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);
|
final bottomInsets = useRef(0.0);
|
||||||
useEffect(
|
useEffect(
|
||||||
@@ -164,21 +105,23 @@ class ViewTabSheetWidget extends HookConsumerWidget {
|
|||||||
padding: const EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 0.0),
|
padding: const EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 0.0),
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
if (draggableScrollableController.size > 0.85) {
|
// Dismiss sheet and open search screen with tab context
|
||||||
await draggableScrollableController.animateTo(
|
onClose();
|
||||||
(scrolledTo.value > 0.0)
|
|
||||||
? scrolledTo.value
|
// Don't pre-fill for internal URLs
|
||||||
: initialHeight,
|
final searchText = initialTabState.url.scheme == 'about'
|
||||||
duration: const Duration(milliseconds: 150),
|
? ''
|
||||||
curve: Curves.easeInOut,
|
: initialTabState.url.toString();
|
||||||
);
|
|
||||||
} else {
|
await SearchRoute(
|
||||||
await draggableScrollableController.animateTo(
|
tabId: initialTabState.id,
|
||||||
1.0,
|
searchText: searchText.isEmpty
|
||||||
duration: const Duration(milliseconds: 150),
|
? SearchRoute.emptySearchText
|
||||||
curve: Curves.easeInOut,
|
: searchText,
|
||||||
);
|
tabType: initialTabState.isPrivate
|
||||||
}
|
? TabType.private
|
||||||
|
: TabType.regular,
|
||||||
|
).push(context);
|
||||||
},
|
},
|
||||||
child: WebsiteTitleTile(initialTabState),
|
child: WebsiteTitleTile(initialTabState),
|
||||||
),
|
),
|
||||||
@@ -199,20 +142,8 @@ class ViewTabSheetWidget extends HookConsumerWidget {
|
|||||||
controller: controller,
|
controller: controller,
|
||||||
physics: const ClampingScrollPhysicsWithoutImplicit(),
|
physics: const ClampingScrollPhysicsWithoutImplicit(),
|
||||||
children: [
|
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
|
// Tracking Protection Section
|
||||||
TrackingProtectionSection(
|
TrackingProtectionSection(tabId: initialTabState.id),
|
||||||
tabId: initialTabState.id,
|
|
||||||
),
|
|
||||||
const Divider(),
|
const Divider(),
|
||||||
// Permissions Section
|
// Permissions Section
|
||||||
PermissionsSection(
|
PermissionsSection(
|
||||||
@@ -221,9 +152,7 @@ class ViewTabSheetWidget extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
const Divider(),
|
const Divider(),
|
||||||
// Clear Site Data Section
|
// Clear Site Data Section
|
||||||
ClearSiteDataSection(
|
ClearSiteDataSection(url: initialTabState.url),
|
||||||
url: initialTabState.url,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16.0),
|
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/bangs/domain/providers/search.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/providers/selected_tab.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/providers/tab_state.dart';
|
||||||
import 'package:weblibre/features/geckoview/domain/repositories/tab.dart';
|
import 'package:weblibre/features/geckoview/domain/repositories/tab.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/domain/providers.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/geckoview/features/search/presentation/widgets/search_modules/tab_search.dart';
|
||||||
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
||||||
import 'package:weblibre/presentation/hooks/sampled_value_notifier.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;
|
import 'package:weblibre/utils/uri_parser.dart' as uri_parser;
|
||||||
|
|
||||||
class SearchScreen extends HookConsumerWidget {
|
class SearchScreen extends HookConsumerWidget {
|
||||||
@@ -47,10 +50,15 @@ class SearchScreen extends HookConsumerWidget {
|
|||||||
final TabType tabType;
|
final TabType tabType;
|
||||||
final bool launchedFromIntent;
|
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({
|
const SearchScreen({
|
||||||
required this.initialSearchText,
|
required this.initialSearchText,
|
||||||
required this.tabType,
|
required this.tabType,
|
||||||
this.launchedFromIntent = false,
|
this.launchedFromIntent = false,
|
||||||
|
this.tabId,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -67,11 +75,23 @@ class SearchScreen extends HookConsumerWidget {
|
|||||||
final selectedTabType = useState(tabType);
|
final selectedTabType = useState(tabType);
|
||||||
final currentTabTabType = ref.watch(selectedTabTypeProvider);
|
final currentTabTabType = ref.watch(selectedTabTypeProvider);
|
||||||
|
|
||||||
final privateTabMode = switch (selectedTabType.value) {
|
// When editing an existing tab, get its state
|
||||||
TabType.regular => false,
|
// If tab no longer exists (null), fall back to new tab mode
|
||||||
TabType.private => true,
|
final existingTabState = tabId != null
|
||||||
TabType.child => currentTabTabType == TabType.private,
|
? 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(
|
final searchTextController = useTextEditingController(
|
||||||
text: initialSearchText,
|
text: initialSearchText,
|
||||||
@@ -105,13 +125,16 @@ class SearchScreen extends HookConsumerWidget {
|
|||||||
final activeBang = selectedBang.value ?? defaultSearchBang;
|
final activeBang = selectedBang.value ?? defaultSearchBang;
|
||||||
final showBangIcon = useState(false);
|
final showBangIcon = useState(false);
|
||||||
|
|
||||||
ref.listen(selectedBangDataProvider(), (previous, next) {
|
ref.listen(
|
||||||
if (previous != next) {
|
selectedBangDataProvider(domain: isEditMode ? existingTabState.url.host : null),
|
||||||
showBangIcon.value = true;
|
(previous, next) {
|
||||||
}
|
if (previous != next) {
|
||||||
|
showBangIcon.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
selectedBang.value = next;
|
selectedBang.value = next;
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
Future<void> submitSearch(String query) async {
|
Future<void> submitSearch(String query) async {
|
||||||
if (activeBang != null && (formKey.currentState?.validate() == true)) {
|
if (activeBang != null && (formKey.currentState?.validate() == true)) {
|
||||||
@@ -123,17 +146,25 @@ class SearchScreen extends HookConsumerWidget {
|
|||||||
.triggerBangSearch(activeBang, query);
|
.triggerBangSearch(activeBang, query);
|
||||||
}
|
}
|
||||||
|
|
||||||
await ref
|
if (isEditMode) {
|
||||||
.read(tabRepositoryProvider.notifier)
|
// Load into existing tab
|
||||||
.addTab(
|
await ref
|
||||||
url: searchUri,
|
.read(tabSessionProvider(tabId: tabId).notifier)
|
||||||
private: privateTabMode,
|
.loadUrl(url: searchUri);
|
||||||
parentId: (selectedTabType.value == TabType.child)
|
} else {
|
||||||
? ref.read(selectedTabProvider)
|
// Create new tab
|
||||||
: null,
|
await ref
|
||||||
launchedFromIntent: launchedFromIntent,
|
.read(tabRepositoryProvider.notifier)
|
||||||
selectTab: true,
|
.addTab(
|
||||||
);
|
url: searchUri,
|
||||||
|
private: privateTabMode,
|
||||||
|
parentId: (selectedTabType.value == TabType.child)
|
||||||
|
? ref.read(selectedTabProvider)
|
||||||
|
: null,
|
||||||
|
launchedFromIntent: launchedFromIntent,
|
||||||
|
selectTab: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
ref.read(bottomSheetControllerProvider.notifier).requestDismiss();
|
ref.read(bottomSheetControllerProvider.notifier).requestDismiss();
|
||||||
@@ -156,54 +187,70 @@ class SearchScreen extends HookConsumerWidget {
|
|||||||
floating: true,
|
floating: true,
|
||||||
pinned: true,
|
pinned: true,
|
||||||
automaticallyImplyLeading: false,
|
automaticallyImplyLeading: false,
|
||||||
title: Align(
|
title: isEditMode
|
||||||
child: Focus(
|
? Column(
|
||||||
canRequestFocus: false,
|
mainAxisSize: MainAxisSize.min,
|
||||||
child: SegmentedButton(
|
children: [
|
||||||
showSelectedIcon: false,
|
_SiteBangsSelector(
|
||||||
segments: [
|
tabId: tabId!,
|
||||||
const ButtonSegment(
|
domain: existingTabState.url.host,
|
||||||
value: TabType.regular,
|
),
|
||||||
label: Text('Regular'),
|
Text(
|
||||||
icon: Icon(MdiIcons.tab),
|
'Editing current tab',
|
||||||
),
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||||
const ButtonSegment(
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
value: TabType.private,
|
),
|
||||||
label: Text('Private'),
|
),
|
||||||
icon: Icon(MdiIcons.tabUnselected),
|
],
|
||||||
),
|
)
|
||||||
if (createChildTabsOption)
|
: Align(
|
||||||
const ButtonSegment(
|
child: Focus(
|
||||||
value: TabType.child,
|
canRequestFocus: false,
|
||||||
label: Text('Child'),
|
child: SegmentedButton(
|
||||||
icon: Icon(MdiIcons.fileTree),
|
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(
|
bottom: PreferredSize(
|
||||||
preferredSize: const Size.fromHeight(kToolbarHeight),
|
preferredSize: const Size.fromHeight(kToolbarHeight),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
@@ -223,7 +270,9 @@ class SearchScreen extends HookConsumerWidget {
|
|||||||
|
|
||||||
if (newUrl == null) {
|
if (newUrl == null) {
|
||||||
final bang =
|
final bang =
|
||||||
ref.read(selectedBangDataProvider()) ??
|
ref.read(selectedBangDataProvider(
|
||||||
|
domain: isEditMode ? existingTabState.url.host : null,
|
||||||
|
)) ??
|
||||||
await ref.read(
|
await ref.read(
|
||||||
defaultSearchBangDataProvider.future,
|
defaultSearchBangDataProvider.future,
|
||||||
);
|
);
|
||||||
@@ -240,19 +289,27 @@ class SearchScreen extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (newUrl != null) {
|
if (newUrl != null) {
|
||||||
await ref
|
if (isEditMode) {
|
||||||
.read(tabRepositoryProvider.notifier)
|
// Load into existing tab
|
||||||
.addTab(
|
await ref
|
||||||
url: newUrl,
|
.read(tabSessionProvider(tabId: tabId).notifier)
|
||||||
private: privateTabMode,
|
.loadUrl(url: newUrl);
|
||||||
parentId:
|
} else {
|
||||||
(selectedTabType.value ==
|
// Create new tab
|
||||||
TabType.child)
|
await ref
|
||||||
? ref.read(selectedTabProvider)
|
.read(tabRepositoryProvider.notifier)
|
||||||
: null,
|
.addTab(
|
||||||
launchedFromIntent: launchedFromIntent,
|
url: newUrl,
|
||||||
selectTab: true,
|
private: privateTabMode,
|
||||||
);
|
parentId:
|
||||||
|
(selectedTabType.value ==
|
||||||
|
TabType.child)
|
||||||
|
? ref.read(selectedTabProvider)
|
||||||
|
: null,
|
||||||
|
launchedFromIntent: launchedFromIntent,
|
||||||
|
selectTab: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
ref
|
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