merge address bar into search screen

This commit is contained in:
Fabian Freund
2026-01-19 16:01:44 +01:00
parent 629c6da667
commit cbbbcf01e8
9 changed files with 184 additions and 37 deletions
@@ -594,7 +594,7 @@ class _SheetContainer extends HookConsumerWidget {
final SiteSettingsSheet parameter =>
NotificationListener<DraggableScrollableNotification>(
onNotification: dismissOnThreshold,
child: _ViewUrlSheet(
child: _SiteSettingsSheet(
initialTabState: parameter.tabState,
maxChildSize: stableMaxChildSize,
bottomAppBarHeight: bottomAppBarHeight,
@@ -855,12 +855,14 @@ class _BrowserView extends StatelessWidget {
}
}
class _ViewUrlSheet extends HookConsumerWidget {
class _SiteSettingsSheet extends HookConsumerWidget {
final double maxChildSize;
final TabState initialTabState;
final double bottomAppBarHeight;
const _ViewUrlSheet({
static const initialHeight = 0.8;
const _SiteSettingsSheet({
required this.initialTabState,
required this.bottomAppBarHeight,
this.maxChildSize = 1.0,
@@ -870,7 +872,17 @@ class _ViewUrlSheet extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final draggableScrollableController = useDraggableScrollableController();
final initialHeight = 172.0 / MediaQuery.of(context).size.height;
void handleClearSiteDataExpansion(bool isExpanded) {
if (isExpanded) {
unawaited(
draggableScrollableController.animateTo(
1.0,
duration: const Duration(milliseconds: 300),
curve: Curves.decelerate,
),
);
}
}
return DraggableScrollableSheet(
controller: draggableScrollableController,
@@ -904,6 +916,7 @@ class _ViewUrlSheet extends HookConsumerWidget {
},
initialHeight: initialHeight,
bottomAppBarHeight: bottomAppBarHeight,
onClearSiteDataExpandedChanged: handleClearSiteDataExpansion,
),
);
},
@@ -73,6 +73,7 @@ class CertificateTile extends HookConsumerWidget {
child: ListTile(
leading: const Skeleton.keep(child: Icon(MdiIcons.timerSand)),
title: Text(BoneMock.title),
subtitle: Text(BoneMock.subtitle),
),
);
}
@@ -28,8 +28,13 @@ import 'package:weblibre/utils/ui_helper.dart';
/// Section widget for clearing site data
class ClearSiteDataSection extends HookConsumerWidget {
final Uri url;
final ValueChanged<bool>? onExpandedChanged;
const ClearSiteDataSection({required this.url, super.key});
const ClearSiteDataSection({
required this.url,
this.onExpandedChanged,
super.key,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
@@ -65,7 +70,11 @@ class ClearSiteDataSection extends HookConsumerWidget {
trailing: Icon(
isExpanded.value ? Icons.expand_less : Icons.expand_more,
),
onTap: () => isExpanded.value = !isExpanded.value,
onTap: () {
final newValue = !isExpanded.value;
isExpanded.value = newValue;
onExpandedChanged?.call(newValue);
},
),
if (isExpanded.value) ...[
_DataTypeCheckbox(
@@ -19,6 +19,7 @@
*/
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:skeletonizer/skeletonizer.dart';
import 'package:weblibre/features/geckoview/domain/providers/tab_session.dart';
import 'package:weblibre/features/geckoview/features/browser/domain/repositories/tracking_protection.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/sheets/tracking_protection_provider.dart';
@@ -41,7 +42,15 @@ class TrackingProtectionSection extends HookConsumerWidget {
tabId: tabId,
isEnabled: !hasException, // ETP enabled when NOT in exceptions
),
loading: () => const SizedBox.shrink(),
loading: () => Skeletonizer(
child: SwitchListTile.adaptive(
value: false,
onChanged: null,
title: Text(BoneMock.title),
subtitle: Text(BoneMock.subtitle),
secondary: const Icon(Icons.shield_outlined),
),
),
error: (error, stack) => const SizedBox.shrink(),
);
}
@@ -50,6 +50,7 @@ class ViewTabSheetWidget extends HookConsumerWidget {
final VoidCallback onClose;
final double initialHeight;
final double bottomAppBarHeight;
final ValueChanged<bool>? onClearSiteDataExpandedChanged;
const ViewTabSheetWidget({
required this.initialTabState,
@@ -58,6 +59,7 @@ class ViewTabSheetWidget extends HookConsumerWidget {
required this.onClose,
required this.initialHeight,
required this.bottomAppBarHeight,
this.onClearSiteDataExpandedChanged,
});
@override
@@ -152,7 +154,10 @@ class ViewTabSheetWidget extends HookConsumerWidget {
),
const Divider(),
// Clear Site Data Section
ClearSiteDataSection(url: initialTabState.url),
ClearSiteDataSection(
url: initialTabState.url,
onExpandedChanged: onClearSiteDataExpandedChanged,
),
const SizedBox(height: 16.0),
],
);
@@ -17,13 +17,17 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import 'dart:async';
import 'package:fading_scroll/fading_scroll.dart';
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:nullability/nullability.dart';
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/bangs/data/models/bang_data.dart';
import 'package:weblibre/features/bangs/domain/providers/bangs.dart';
import 'package:weblibre/features/bangs/domain/providers/search.dart';
@@ -40,9 +44,11 @@ import 'package:weblibre/features/geckoview/features/search/presentation/widgets
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/user/domain/repositories/general_settings.dart';
import 'package:weblibre/presentation/hooks/on_listenable_change_selector.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/text_field_line_count.dart';
import 'package:weblibre/utils/uri_parser.dart' as uri_parser;
class SearchScreen extends HookConsumerWidget {
@@ -101,6 +107,74 @@ class SearchScreen extends HookConsumerWidget {
sampleDuration: const Duration(milliseconds: 150),
);
final searchFocusNode = useFocusNode();
final textFieldKey = useMemoized(() => GlobalKey());
final preferredHeight = useState<double>(kToolbarHeight);
useOnListenableChangeSelector(
searchFocusNode,
() => searchFocusNode.hasFocus,
() {
if (searchFocusNode.hasFocus && isEditMode) {
// Select all text when the field is focused
searchTextController.selection = TextSelection(
baseOffset: 0,
extentOffset: searchTextController.text.length,
);
}
},
);
useEffect(() {
if (isEditMode) {
WidgetsBinding.instance.addPostFrameCallback((_) {
searchTextController.selection = TextSelection(
baseOffset: 0,
extentOffset: searchTextController.text.length,
);
});
}
return null;
}, []);
//Request initial focus in a way our useOnListenableChangeSelector is triggered
useEffect(() {
//Wait for first frame then request focus
unawaited(
Future.delayed(const Duration(milliseconds: 1000 ~/ 60)).whenComplete(
() {
WidgetsBinding.instance.addPostFrameCallback((_) {
searchFocusNode.requestFocus();
});
},
),
);
return null;
}, []);
useEffect(() {
void measureHeight() {
final measuredHeight = getTextFieldHeight(textFieldKey);
if (measuredHeight != null) {
preferredHeight.value = measuredHeight;
}
}
WidgetsBinding.instance.addPostFrameCallback((_) {
measureHeight();
});
return null;
}, [textFieldKey, isEditMode]);
useOnListenableChange(isEditMode ? searchTextController : null, () {
final measuredHeight = getTextFieldHeight(textFieldKey);
if (measuredHeight != null && preferredHeight.value != measuredHeight) {
preferredHeight.value = measuredHeight;
}
});
useOnAppLifecycleStateChange((previous, current) {
switch (current) {
@@ -126,7 +200,9 @@ class SearchScreen extends HookConsumerWidget {
final showBangIcon = useState(false);
ref.listen(
selectedBangDataProvider(domain: isEditMode ? existingTabState.url.host : null),
selectedBangDataProvider(
domain: isEditMode ? existingTabState.url.host : null,
),
(previous, next) {
if (previous != next) {
showBangIcon.value = true;
@@ -188,20 +264,10 @@ class SearchScreen extends HookConsumerWidget {
pinned: true,
automaticallyImplyLeading: false,
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,
),
),
],
? _SiteBangsSelector(
tabId: tabId!,
domain: existingTabState.url.host,
searchTextController: searchTextController,
)
: Align(
child: Focus(
@@ -230,7 +296,9 @@ class SearchScreen extends HookConsumerWidget {
onSelectionChanged: (value) {
selectedTabType.value = value.first;
// Restore focus to search field after segment change
WidgetsBinding.instance.addPostFrameCallback((_) {
WidgetsBinding.instance.addPostFrameCallback((
_,
) {
searchFocusNode.requestFocus();
});
},
@@ -243,8 +311,8 @@ class SearchScreen extends HookConsumerWidget {
TabType.child =>
(currentTabTabType == TabType.private)
? SegmentedButton.styleFrom(
selectedBackgroundColor:
appColors.privateSelectionOverlay,
selectedBackgroundColor: appColors
.privateSelectionOverlay,
)
: null,
},
@@ -252,15 +320,20 @@ class SearchScreen extends HookConsumerWidget {
),
),
bottom: PreferredSize(
preferredSize: const Size.fromHeight(kToolbarHeight),
preferredSize: Size.fromHeight(preferredHeight.value),
child: Padding(
padding: const EdgeInsets.only(left: 16.0),
child: SearchField(
textFieldKey: textFieldKey,
showBangIcon: showBangIcon.value,
textEditingController: searchTextController,
focusNode: searchFocusNode,
maxLines: isEditMode ? 3 : 1,
autofocus: true,
label: const Text('Address / Search'),
label: (activeBang != null)
? const Text('Search')
: const Text('Address / Search'),
unfocusOnTapOutside: !isEditMode,
onSubmitted: (value) async {
if (value.isNotEmpty) {
var newUrl = uri_parser.tryParseUrl(
@@ -270,9 +343,13 @@ class SearchScreen extends HookConsumerWidget {
if (newUrl == null) {
final bang =
ref.read(selectedBangDataProvider(
domain: isEditMode ? existingTabState.url.host : null,
)) ??
ref.read(
selectedBangDataProvider(
domain: isEditMode
? existingTabState.url.host
: null,
),
) ??
await ref.read(
defaultSearchBangDataProvider.future,
);
@@ -292,7 +369,11 @@ class SearchScreen extends HookConsumerWidget {
if (isEditMode) {
// Load into existing tab
await ref
.read(tabSessionProvider(tabId: tabId).notifier)
.read(
tabSessionProvider(
tabId: tabId,
).notifier,
)
.loadUrl(url: newUrl);
} else {
// Create new tab
@@ -357,17 +438,17 @@ class SearchScreen extends HookConsumerWidget {
class _SiteBangsSelector extends HookConsumerWidget {
final String tabId;
final String domain;
final TextEditingController searchTextController;
const _SiteBangsSelector({
required this.tabId,
required this.domain,
required this.searchTextController,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final selectedBang = ref.watch(
selectedBangDataProvider(domain: domain),
);
final selectedBang = ref.watch(selectedBangDataProvider(domain: domain));
final availableBangs = ref.watch(
bangListProvider(
domain: domain,
@@ -390,6 +471,18 @@ class _SiteBangsSelector extends HookConsumerWidget {
availableItems: availableBangs,
selectedItem: selectedBang,
onSelected: (bang) {
if (selectedBang == null) {
final hasSupportedScheme =
uri_parser
.tryParseUrl(searchTextController.text)
.mapNotNull((uri) => uri.hasSupportedScheme) ??
false;
if (hasSupportedScheme) {
searchTextController.clear();
}
}
ref
.read(selectedBangTriggerProvider(domain: domain).notifier)
.setTrigger(bang.toKey());
@@ -37,8 +37,10 @@ class SearchField extends HookConsumerWidget {
final void Function(String)? onSubmitted;
final bool showSuggestions;
final int? maxLines;
final int? minLines;
final VoidCallback? onTap;
final bool unfocusOnTapOutside;
final GlobalKey? textFieldKey;
final BangData? activeBang;
final bool showBangIcon;
@@ -52,10 +54,12 @@ class SearchField extends HookConsumerWidget {
this.label,
this.focusNode,
this.maxLines = 1,
this.minLines = 1,
this.onTap,
this.unfocusOnTapOutside = true,
this.showBangIcon = true,
this.autofocus = false,
this.textFieldKey,
});
@override
@@ -100,10 +104,11 @@ class SearchField extends HookConsumerWidget {
enableIMEPersonalizedLearning: !incognitoEnabled,
focusNode: safeFocusNode,
maxLines: maxLines,
//Submit isntead of newline
textFieldKey: textFieldKey,
textInputAction: (maxLines == null || maxLines! > 1)
? TextInputAction.done
: null,
minLines: minLines,
autofocus: autofocus,
decoration: InputDecoration(
border: InputBorder.none,
@@ -50,6 +50,7 @@ class AutoSuggestTextField extends HookWidget {
final TapRegionCallback? onTapOutside;
final VoidCallback? onTap;
final bool autocorrect;
final GlobalKey? textFieldKey;
const AutoSuggestTextField({
super.key,
@@ -79,6 +80,7 @@ class AutoSuggestTextField extends HookWidget {
this.onTapOutside,
this.onTap,
this.autocorrect = false,
this.textFieldKey,
});
bool _suggestionHasMatch() =>
@@ -88,7 +90,8 @@ class AutoSuggestTextField extends HookWidget {
@override
Widget build(BuildContext context) {
final textFieldKey = useMemoized(() => GlobalKey());
final textFieldKey =
this.textFieldKey ?? useMemoized<GlobalKey>(() => GlobalKey());
final showSuggestion = useListenableSelector(controller, () {
if (maxLines != 1) {
+9
View File
@@ -47,3 +47,12 @@ int? getTextFieldLineCount(GlobalKey key, String text, TextStyle style) {
return null;
}
double? getTextFieldHeight(GlobalKey key) {
final box = key.currentContext?.findRenderObject();
if (box case final RenderBox box) {
return box.size.height;
}
return null;
}