merge address bar into search screen
This commit is contained in:
@@ -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),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
+11
-2
@@ -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(
|
||||
|
||||
+10
-1
@@ -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(),
|
||||
);
|
||||
}
|
||||
|
||||
+6
-1
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user