add isolated tab feature
This commit is contained in:
@@ -40,6 +40,8 @@ import 'package:weblibre/features/geckoview/features/search/presentation/widgets
|
||||
import 'package:weblibre/features/geckoview/features/search/presentation/widgets/search_modules/full_search_suggestions.dart';
|
||||
import 'package:weblibre/features/geckoview/features/search/presentation/widgets/search_modules/history_suggestions.dart';
|
||||
import 'package:weblibre/features/geckoview/features/search/presentation/widgets/search_modules/tab_search.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/data/entities/isolation_context.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_mode.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/domain/providers/selected_container.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/presentation/widgets/compact_container_selector.dart';
|
||||
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
||||
@@ -92,15 +94,24 @@ class SearchScreen extends HookConsumerWidget {
|
||||
// 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
|
||||
final effectiveTabMode = isEditMode
|
||||
? existingTabState.tabMode
|
||||
: switch (selectedTabType.value) {
|
||||
TabType.regular => false,
|
||||
TabType.private => true,
|
||||
TabType.child => currentTabTabType == TabType.private,
|
||||
TabType.regular => TabMode.regular,
|
||||
TabType.private => TabMode.private,
|
||||
TabType.isolated => TabMode.newIsolated(),
|
||||
TabType.child => switch (currentTabTabType) {
|
||||
TabType.private => TabMode.private,
|
||||
TabType.isolated => TabMode.isolated(
|
||||
ref.watch(selectedTabStateProvider)?.isolationContextId ??
|
||||
newIsolatedContextId(),
|
||||
),
|
||||
_ => TabMode.regular,
|
||||
},
|
||||
};
|
||||
|
||||
final privateTabMode = effectiveTabMode is PrivateTabMode;
|
||||
|
||||
final searchTextController = useTextEditingController(
|
||||
text: initialSearchText,
|
||||
);
|
||||
@@ -240,7 +251,7 @@ class SearchScreen extends HookConsumerWidget {
|
||||
.read(tabRepositoryProvider.notifier)
|
||||
.addTab(
|
||||
url: searchUri,
|
||||
private: privateTabMode,
|
||||
tabMode: effectiveTabMode,
|
||||
parentId: (selectedTabType.value == TabType.child)
|
||||
? ref.read(selectedTabProvider)
|
||||
: null,
|
||||
@@ -279,7 +290,7 @@ class SearchScreen extends HookConsumerWidget {
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 3,
|
||||
flex: 4,
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Focus(
|
||||
@@ -300,18 +311,24 @@ class SearchScreen extends HookConsumerWidget {
|
||||
TabType.regular => null,
|
||||
TabType.private =>
|
||||
appColors.privateSelectionOverlay,
|
||||
TabType.isolated =>
|
||||
appColors.isolatedSelectionOverlay,
|
||||
TabType.child =>
|
||||
(currentTabTabType ==
|
||||
TabType.private)
|
||||
? appColors
|
||||
.privateSelectionOverlay
|
||||
: null,
|
||||
switch (currentTabTabType) {
|
||||
TabType.private =>
|
||||
appColors
|
||||
.privateSelectionOverlay,
|
||||
TabType.isolated =>
|
||||
appColors
|
||||
.isolatedSelectionOverlay,
|
||||
_ => null,
|
||||
},
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
const SizedBox(width: 8),
|
||||
Flexible(
|
||||
flex: 2,
|
||||
child: Align(
|
||||
@@ -390,7 +407,7 @@ class SearchScreen extends HookConsumerWidget {
|
||||
.read(tabRepositoryProvider.notifier)
|
||||
.addTab(
|
||||
url: newUrl,
|
||||
private: privateTabMode,
|
||||
tabMode: effectiveTabMode,
|
||||
parentId:
|
||||
(selectedTabType.value == TabType.child)
|
||||
? ref.read(selectedTabProvider)
|
||||
@@ -447,7 +464,7 @@ class SearchScreen extends HookConsumerWidget {
|
||||
.read(tabRepositoryProvider.notifier)
|
||||
.addTab(
|
||||
url: uri,
|
||||
private: privateTabMode,
|
||||
tabMode: effectiveTabMode,
|
||||
parentId: (selectedTabType.value == TabType.child)
|
||||
? ref.read(selectedTabProvider)
|
||||
: null,
|
||||
|
||||
+9
@@ -72,6 +72,15 @@ class AnimatedTabTypeSwitcher extends StatelessWidget {
|
||||
selectedBackgroundColor: selectedBackgroundColor,
|
||||
onTap: () => onChanged(TabType.private),
|
||||
),
|
||||
_divider(borderColor),
|
||||
_Segment(
|
||||
tabType: TabType.isolated,
|
||||
icon: MdiIcons.shieldLock,
|
||||
label: 'Isolated',
|
||||
isSelected: selected == TabType.isolated,
|
||||
selectedBackgroundColor: selectedBackgroundColor,
|
||||
onTap: () => onChanged(TabType.isolated),
|
||||
),
|
||||
if (showChildOption) ...[
|
||||
_divider(borderColor),
|
||||
_Segment(
|
||||
|
||||
+2
-6
@@ -75,9 +75,7 @@ class FullSearchTermSuggestions extends HookConsumerWidget {
|
||||
: [
|
||||
if (searchTextIsNotEmpty) searchText,
|
||||
if (searchSuggestions.value != null)
|
||||
...searchSuggestions.value!.whereNot(
|
||||
(s) => s == searchText,
|
||||
),
|
||||
...searchSuggestions.value!.whereNot((s) => s == searchText),
|
||||
],
|
||||
[showHistory, searchText, searchHistory.value, searchSuggestions.value],
|
||||
);
|
||||
@@ -115,9 +113,7 @@ class FullSearchTermSuggestions extends HookConsumerWidget {
|
||||
onPressed: ref
|
||||
.read(searchSuggestionsExpandedProvider.notifier)
|
||||
.toggle,
|
||||
icon: Icon(
|
||||
expanded ? Icons.unfold_less : Icons.unfold_more,
|
||||
),
|
||||
icon: Icon(expanded ? Icons.unfold_less : Icons.unfold_more),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
+2
-6
@@ -105,18 +105,14 @@ class SearchModuleHeader extends StatelessWidget {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
isExpanded
|
||||
? 'Show less'
|
||||
: 'Show all $totalCount',
|
||||
isExpanded ? 'Show less' : 'Show all $totalCount',
|
||||
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Icon(
|
||||
isExpanded
|
||||
? Icons.expand_less
|
||||
: Icons.expand_more,
|
||||
isExpanded ? Icons.expand_less : Icons.expand_more,
|
||||
size: 16,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
|
||||
+112
-109
@@ -135,132 +135,135 @@ class TabSearch extends HookConsumerWidget {
|
||||
title: 'Tabs',
|
||||
moduleType: SearchModuleType.tabs,
|
||||
totalCount: filteredResultCount,
|
||||
contentSliverBuilder: ({
|
||||
required bool isCollapsed,
|
||||
required int visibleCount,
|
||||
}) => [
|
||||
if (!isCollapsed)
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ContainerChips(
|
||||
displayMenu: false,
|
||||
selectedContainer: selectedContainer.value,
|
||||
showUnassignedChip: containerIdsWithResults.value
|
||||
.containsKey(null),
|
||||
onSelected: (container) {
|
||||
selectedContainer.value = container;
|
||||
},
|
||||
onDeleted: (container) {
|
||||
selectedContainer.value = null;
|
||||
},
|
||||
containerFilter: (container) =>
|
||||
containerIdsWithResults.value.containsKey(container.id),
|
||||
containerBadgeCount: (container) =>
|
||||
containerIdsWithResults.value[container?.id] ?? 0,
|
||||
searchTextListenable: searchTextListenable,
|
||||
contentSliverBuilder:
|
||||
({required bool isCollapsed, required int visibleCount}) => [
|
||||
if (!isCollapsed)
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ContainerChips(
|
||||
displayMenu: false,
|
||||
selectedContainer: selectedContainer.value,
|
||||
showUnassignedChip: containerIdsWithResults.value
|
||||
.containsKey(null),
|
||||
onSelected: (container) {
|
||||
selectedContainer.value = container;
|
||||
},
|
||||
onDeleted: (container) {
|
||||
selectedContainer.value = null;
|
||||
},
|
||||
containerFilter: (container) => containerIdsWithResults
|
||||
.value
|
||||
.containsKey(container.id),
|
||||
containerBadgeCount: (container) =>
|
||||
containerIdsWithResults.value[container?.id] ?? 0,
|
||||
searchTextListenable: searchTextListenable,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!isCollapsed)
|
||||
SliverList.builder(
|
||||
itemCount: visibleCount,
|
||||
itemBuilder: (context, index) {
|
||||
final result = filteredTabs[index];
|
||||
if (!isCollapsed)
|
||||
SliverList.builder(
|
||||
itemCount: visibleCount,
|
||||
itemBuilder: (context, index) {
|
||||
final result = filteredTabs[index];
|
||||
|
||||
final content =
|
||||
(result.extractedContent?.contains(_matchPrefix) == true)
|
||||
? result.extractedContent
|
||||
: result.fullContent;
|
||||
final content =
|
||||
(result.extractedContent?.contains(_matchPrefix) == true)
|
||||
? result.extractedContent
|
||||
: result.fullContent;
|
||||
|
||||
final titleHasMatch = result.title.contains(_matchPrefix);
|
||||
final urlHasMatch =
|
||||
result.highlightedUrl?.contains(_matchPrefix) ?? false;
|
||||
final bodyHasMatch = content?.contains(_matchPrefix) ?? false;
|
||||
final titleHasMatch = result.title.contains(_matchPrefix);
|
||||
final urlHasMatch =
|
||||
result.highlightedUrl?.contains(_matchPrefix) ?? false;
|
||||
final bodyHasMatch = content?.contains(_matchPrefix) ?? false;
|
||||
|
||||
return ListTile(
|
||||
leading: RepaintBoundary(
|
||||
child:
|
||||
result.icon.mapNotNull(
|
||||
(icon) => SafeRawImage(
|
||||
image: icon,
|
||||
height: 24,
|
||||
width: 24,
|
||||
fallback: UrlIcon([result.url], iconSize: 24),
|
||||
),
|
||||
) ??
|
||||
UrlIcon([result.url], iconSize: 24),
|
||||
),
|
||||
title: result.title.mapNotNull(
|
||||
(title) => Text.rich(
|
||||
buildHighlightedText(
|
||||
title,
|
||||
Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
_matchPrefix,
|
||||
_matchSuffix,
|
||||
return ListTile(
|
||||
leading: RepaintBoundary(
|
||||
child:
|
||||
result.icon.mapNotNull(
|
||||
(icon) => SafeRawImage(
|
||||
image: icon,
|
||||
height: 24,
|
||||
width: 24,
|
||||
fallback: UrlIcon([result.url], iconSize: 24),
|
||||
),
|
||||
) ??
|
||||
UrlIcon([result.url], iconSize: 24),
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
subtitle: (bodyHasMatch || (urlHasMatch && !titleHasMatch))
|
||||
? Text.rich(
|
||||
title: result.title.mapNotNull(
|
||||
(title) => Text.rich(
|
||||
buildHighlightedText(
|
||||
(bodyHasMatch ? content! : result.highlightedUrl!),
|
||||
Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurfaceVariant,
|
||||
title,
|
||||
Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurfaceVariant,
|
||||
Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
_matchPrefix,
|
||||
_matchSuffix,
|
||||
normalizeWhitespaces: true,
|
||||
),
|
||||
maxLines: 3,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
)
|
||||
: UriBreadcrumb(uri: result.url),
|
||||
onTap: () async {
|
||||
await ref
|
||||
.read(tabRepositoryProvider.notifier)
|
||||
.selectTab(result.id);
|
||||
if (result.sourceSearchQuery.isNotEmpty &&
|
||||
ref.read(findInPageControllerProvider(result.id)) ==
|
||||
FindInPageState.hidden()) {
|
||||
await ref
|
||||
.read(findInPageControllerProvider(result.id).notifier)
|
||||
.findAll(text: result.sourceSearchQuery!);
|
||||
}
|
||||
),
|
||||
),
|
||||
subtitle: (bodyHasMatch || (urlHasMatch && !titleHasMatch))
|
||||
? Text.rich(
|
||||
buildHighlightedText(
|
||||
(bodyHasMatch
|
||||
? content!
|
||||
: result.highlightedUrl!),
|
||||
Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurfaceVariant,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
_matchPrefix,
|
||||
_matchSuffix,
|
||||
normalizeWhitespaces: true,
|
||||
),
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
)
|
||||
: UriBreadcrumb(uri: result.url),
|
||||
onTap: () async {
|
||||
await ref
|
||||
.read(tabRepositoryProvider.notifier)
|
||||
.selectTab(result.id);
|
||||
if (result.sourceSearchQuery.isNotEmpty &&
|
||||
ref.read(findInPageControllerProvider(result.id)) ==
|
||||
FindInPageState.hidden()) {
|
||||
await ref
|
||||
.read(
|
||||
findInPageControllerProvider(result.id).notifier,
|
||||
)
|
||||
.findAll(text: result.sourceSearchQuery!);
|
||||
}
|
||||
|
||||
if (context.mounted) {
|
||||
ref
|
||||
.read(bottomSheetControllerProvider.notifier)
|
||||
.requestDismiss();
|
||||
if (context.mounted) {
|
||||
ref
|
||||
.read(bottomSheetControllerProvider.notifier)
|
||||
.requestDismiss();
|
||||
|
||||
const BrowserRoute().go(context);
|
||||
}
|
||||
const BrowserRoute().go(context);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user