add uri normalization

This commit is contained in:
Fabian Freund
2026-03-09 19:24:45 +01:00
parent 4b01155584
commit 1b1ee65fb3
4 changed files with 32 additions and 26 deletions
+10
View File
@@ -29,4 +29,14 @@ extension UriX on Uri {
bool get isHttp => isScheme('http'); bool get isHttp => isScheme('http');
bool get isHttps => isScheme('https'); bool get isHttps => isScheme('https');
bool get isHttpOrHttps => isHttp || isHttps; bool get isHttpOrHttps => isHttp || isHttps;
/// Removes a bare root path (`/`) when there is no query or fragment, so
/// that `https://example.com/` and `https://example.com` are treated as
/// equivalent.
Uri get normalized {
if (path == '/' && !hasQuery && !hasFragment) {
return replace(path: '');
}
return this;
}
} }
@@ -168,7 +168,8 @@ class SearchScreen extends HookConsumerWidget {
if (startedWithUrl) { if (startedWithUrl) {
hasUserModifiedInput.value = text != initialSearchText; hasUserModifiedInput.value = text != initialSearchText;
} }
isUrlInput.value = text.isNotEmpty && isUrlInput.value =
text.isNotEmpty &&
classifyAddressBarInput(text) is NavigateInputClassification; classifyAddressBarInput(text) is NavigateInputClassification;
}, },
); );
@@ -371,26 +372,18 @@ class SearchScreen extends HookConsumerWidget {
} }
final emptyStateWidgets = <SearchModuleType, Widget>{ final emptyStateWidgets = <SearchModuleType, Widget>{
SearchModuleType.topSites: TopSitesSection( SearchModuleType.topSites: TopSitesSection(onUriSelected: openUriInTab),
onUriSelected: openUriInTab,
),
SearchModuleType.recentArticles: RecentFeedArticlesSection( SearchModuleType.recentArticles: RecentFeedArticlesSection(
onArticleSelected: (article) { onArticleSelected: (article) {
unawaited( FeedArticleRoute(articleId: article.id).pushReplacement(context);
FeedArticleRoute(articleId: article.id).push(context),
);
}, },
), ),
SearchModuleType.recentTabs: RecentTabsSection( SearchModuleType.recentTabs: RecentTabsSection(
onTabSelected: (tabId) async { onTabSelected: (tabId) async {
await ref await ref.read(tabRepositoryProvider.notifier).selectTab(tabId);
.read(tabRepositoryProvider.notifier)
.selectTab(tabId);
if (context.mounted) { if (context.mounted) {
ref ref.read(bottomSheetControllerProvider.notifier).requestDismiss();
.read(bottomSheetControllerProvider.notifier)
.requestDismiss();
const BrowserRoute().go(context); const BrowserRoute().go(context);
} }
}, },
@@ -436,9 +429,7 @@ class SearchScreen extends HookConsumerWidget {
}; };
final searchWidgets = <SearchModuleType, Widget>{ final searchWidgets = <SearchModuleType, Widget>{
SearchModuleType.tabs: TabSearch( SearchModuleType.tabs: TabSearch(searchTextListenable: sampledSearchText),
searchTextListenable: sampledSearchText,
),
SearchModuleType.bookmarks: BookmarkSearch( SearchModuleType.bookmarks: BookmarkSearch(
searchTextListenable: sampledSearchText, searchTextListenable: sampledSearchText,
onUriSelected: openUriInTab, onUriSelected: openUriInTab,
@@ -671,9 +662,7 @@ class SearchScreen extends HookConsumerWidget {
(!isUrlInput.value || (!isUrlInput.value ||
entry.type != SearchModuleType.articles)) entry.type != SearchModuleType.articles))
searchWidgets[entry.type]!, searchWidgets[entry.type]!,
const _CustomizeSectionsButton( const _CustomizeSectionsButton(group: SearchModuleGroup.search),
group: SearchModuleGroup.search,
),
], ],
], ],
), ),
@@ -18,6 +18,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:weblibre/extensions/uri.dart';
import 'package:weblibre/features/geckoview/features/top_sites/data/database/daos/top_site.drift.dart'; import 'package:weblibre/features/geckoview/features/top_sites/data/database/daos/top_site.drift.dart';
import 'package:weblibre/features/geckoview/features/top_sites/data/database/database.dart'; import 'package:weblibre/features/geckoview/features/top_sites/data/database/database.dart';
import 'package:weblibre/features/geckoview/features/top_sites/data/database/definitions.drift.dart'; import 'package:weblibre/features/geckoview/features/top_sites/data/database/definitions.drift.dart';
@@ -42,7 +43,8 @@ class TopSiteDao extends DatabaseAccessor<TopSiteDatabase>
} }
Future<TopSiteData?> getPersistedTopSiteByUrl(Uri url) { Future<TopSiteData?> getPersistedTopSiteByUrl(Uri url) {
return (db.topSite.select()..where((t) => t.url.equalsValue(url))) return (db.topSite.select()
..where((t) => t.url.equalsValue(url.normalized)))
.getSingleOrNull(); .getSingleOrNull();
} }
@@ -63,7 +65,7 @@ class TopSiteDao extends DatabaseAccessor<TopSiteDatabase>
TopSiteCompanion.insert( TopSiteCompanion.insert(
id: id, id: id,
title: title, title: title,
url: url, url: url.normalized,
source: StoredTopSiteSource.pinned, source: StoredTopSiteSource.pinned,
orderKey: orderKey, orderKey: orderKey,
createdAt: DateTime.now(), createdAt: DateTime.now(),
@@ -83,7 +85,7 @@ class TopSiteDao extends DatabaseAccessor<TopSiteDatabase>
required Uri url, required Uri url,
}) { }) {
return (db.topSite.update()..where((t) => t.id.equals(id))).write( return (db.topSite.update()..where((t) => t.id.equals(id))).write(
TopSiteCompanion(title: Value(title), url: Value(url)), TopSiteCompanion(title: Value(title), url: Value(url.normalized)),
); );
} }
@@ -22,6 +22,7 @@ import 'dart:async';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:weblibre/core/uuid.dart'; import 'package:weblibre/core/uuid.dart';
import 'package:weblibre/extensions/uri.dart';
import 'package:weblibre/features/geckoview/features/history/domain/repositories/history.dart'; import 'package:weblibre/features/geckoview/features/history/domain/repositories/history.dart';
import 'package:weblibre/features/geckoview/features/top_sites/data/database/definitions.drift.dart'; import 'package:weblibre/features/geckoview/features/top_sites/data/database/definitions.drift.dart';
import 'package:weblibre/features/geckoview/features/top_sites/data/entities/stored_top_site_source.dart'; import 'package:weblibre/features/geckoview/features/top_sites/data/entities/stored_top_site_source.dart';
@@ -55,7 +56,7 @@ class TopSiteRepository extends _$TopSiteRepository {
TopSiteCompanion.insert( TopSiteCompanion.insert(
id: uuid.v7(), id: uuid.v7(),
title: seeds[i].title, title: seeds[i].title,
url: seeds[i].url, url: seeds[i].url.normalized,
source: StoredTopSiteSource.seeded, source: StoredTopSiteSource.seeded,
orderKey: orderKey, orderKey: orderKey,
createdAt: now, createdAt: now,
@@ -80,7 +81,9 @@ class TopSiteRepository extends _$TopSiteRepository {
final remaining = targetCount - persisted.length; final remaining = targetCount - persisted.length;
final historyItems = await _getHistoryItems( final historyItems = await _getHistoryItems(
limit: remaining, limit: remaining,
excludeUrls: persisted.map((s) => s.url.toString()).toSet(), excludeUrls: persisted
.map((s) => s.url.normalized.toString())
.toSet(),
); );
return [...persisted, ...historyItems]; return [...persisted, ...historyItems];
@@ -103,7 +106,9 @@ class TopSiteRepository extends _$TopSiteRepository {
final remaining = targetCount - persistedItems.length; final remaining = targetCount - persistedItems.length;
final historyItems = await _getHistoryItems( final historyItems = await _getHistoryItems(
limit: remaining, limit: remaining,
excludeUrls: persistedItems.map((s) => s.url.toString()).toSet(), excludeUrls: persistedItems
.map((s) => s.url.normalized.toString())
.toSet(),
); );
return [...persistedItems, ...historyItems]; return [...persistedItems, ...historyItems];
@@ -259,7 +264,7 @@ class TopSiteRepository extends _$TopSiteRepository {
final uri = Uri.tryParse(site.url); final uri = Uri.tryParse(site.url);
if (uri == null) continue; if (uri == null) continue;
if (excludeUrls.contains(uri.toString())) continue; if (excludeUrls.contains(uri.normalized.toString())) continue;
final title = (site.title?.trim().isNotEmpty == true) final title = (site.title?.trim().isNotEmpty == true)
? site.title!.trim() ? site.title!.trim()