diff --git a/app/lib/extensions/uri.dart b/app/lib/extensions/uri.dart index 8308f37b..90739a93 100644 --- a/app/lib/extensions/uri.dart +++ b/app/lib/extensions/uri.dart @@ -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; + } } diff --git a/app/lib/features/geckoview/features/search/presentation/screens/search.dart b/app/lib/features/geckoview/features/search/presentation/screens/search.dart index b19d4509..c7718945 100644 --- a/app/lib/features/geckoview/features/search/presentation/screens/search.dart +++ b/app/lib/features/geckoview/features/search/presentation/screens/search.dart @@ -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.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.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), ], ], ), diff --git a/app/lib/features/geckoview/features/top_sites/data/database/daos/top_site.dart b/app/lib/features/geckoview/features/top_sites/data/database/daos/top_site.dart index f61310a0..aa7d36ce 100644 --- a/app/lib/features/geckoview/features/top_sites/data/database/daos/top_site.dart +++ b/app/lib/features/geckoview/features/top_sites/data/database/daos/top_site.dart @@ -18,6 +18,7 @@ * along with this program. If not, see . */ 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 } Future 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 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 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)), ); } diff --git a/app/lib/features/geckoview/features/top_sites/domain/repositories/top_site_repository.dart b/app/lib/features/geckoview/features/top_sites/domain/repositories/top_site_repository.dart index fb7fbd1f..9b9f6d99 100644 --- a/app/lib/features/geckoview/features/top_sites/domain/repositories/top_site_repository.dart +++ b/app/lib/features/geckoview/features/top_sites/domain/repositories/top_site_repository.dart @@ -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()