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 isHttps => isScheme('https');
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) {
hasUserModifiedInput.value = text != initialSearchText;
}
isUrlInput.value = text.isNotEmpty &&
isUrlInput.value =
text.isNotEmpty &&
classifyAddressBarInput(text) is NavigateInputClassification;
},
);
@@ -371,26 +372,18 @@ class SearchScreen extends HookConsumerWidget {
}
final emptyStateWidgets = <SearchModuleType, Widget>{
SearchModuleType.topSites: TopSitesSection(
onUriSelected: openUriInTab,
),
SearchModuleType.topSites: TopSitesSection(onUriSelected: openUriInTab),
SearchModuleType.recentArticles: RecentFeedArticlesSection(
onArticleSelected: (article) {
unawaited(
FeedArticleRoute(articleId: article.id).push(context),
);
FeedArticleRoute(articleId: article.id).pushReplacement(context);
},
),
SearchModuleType.recentTabs: RecentTabsSection(
onTabSelected: (tabId) async {
await ref
.read(tabRepositoryProvider.notifier)
.selectTab(tabId);
await ref.read(tabRepositoryProvider.notifier).selectTab(tabId);
if (context.mounted) {
ref
.read(bottomSheetControllerProvider.notifier)
.requestDismiss();
ref.read(bottomSheetControllerProvider.notifier).requestDismiss();
const BrowserRoute().go(context);
}
},
@@ -436,9 +429,7 @@ class SearchScreen extends HookConsumerWidget {
};
final searchWidgets = <SearchModuleType, Widget>{
SearchModuleType.tabs: TabSearch(
searchTextListenable: sampledSearchText,
),
SearchModuleType.tabs: TabSearch(searchTextListenable: sampledSearchText),
SearchModuleType.bookmarks: BookmarkSearch(
searchTextListenable: sampledSearchText,
onUriSelected: openUriInTab,
@@ -671,9 +662,7 @@ class SearchScreen extends HookConsumerWidget {
(!isUrlInput.value ||
entry.type != SearchModuleType.articles))
searchWidgets[entry.type]!,
const _CustomizeSectionsButton(
group: SearchModuleGroup.search,
),
const _CustomizeSectionsButton(group: SearchModuleGroup.search),
],
],
),
@@ -18,6 +18,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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/database.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) {
return (db.topSite.select()..where((t) => t.url.equalsValue(url)))
return (db.topSite.select()
..where((t) => t.url.equalsValue(url.normalized)))
.getSingleOrNull();
}
@@ -63,7 +65,7 @@ class TopSiteDao extends DatabaseAccessor<TopSiteDatabase>
TopSiteCompanion.insert(
id: id,
title: title,
url: url,
url: url.normalized,
source: StoredTopSiteSource.pinned,
orderKey: orderKey,
createdAt: DateTime.now(),
@@ -83,7 +85,7 @@ class TopSiteDao extends DatabaseAccessor<TopSiteDatabase>
required Uri url,
}) {
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:riverpod_annotation/riverpod_annotation.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/top_sites/data/database/definitions.drift.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(
id: uuid.v7(),
title: seeds[i].title,
url: seeds[i].url,
url: seeds[i].url.normalized,
source: StoredTopSiteSource.seeded,
orderKey: orderKey,
createdAt: now,
@@ -80,7 +81,9 @@ class TopSiteRepository extends _$TopSiteRepository {
final remaining = targetCount - persisted.length;
final historyItems = await _getHistoryItems(
limit: remaining,
excludeUrls: persisted.map((s) => s.url.toString()).toSet(),
excludeUrls: persisted
.map((s) => s.url.normalized.toString())
.toSet(),
);
return [...persisted, ...historyItems];
@@ -103,7 +106,9 @@ class TopSiteRepository extends _$TopSiteRepository {
final remaining = targetCount - persistedItems.length;
final historyItems = await _getHistoryItems(
limit: remaining,
excludeUrls: persistedItems.map((s) => s.url.toString()).toSet(),
excludeUrls: persistedItems
.map((s) => s.url.normalized.toString())
.toSet(),
);
return [...persistedItems, ...historyItems];
@@ -259,7 +264,7 @@ class TopSiteRepository extends _$TopSiteRepository {
final uri = Uri.tryParse(site.url);
if (uri == null) continue;
if (excludeUrls.contains(uri.toString())) continue;
if (excludeUrls.contains(uri.normalized.toString())) continue;
final title = (site.title?.trim().isNotEmpty == true)
? site.title!.trim()