added popular sites module

This commit is contained in:
Fabian Freund
2026-06-29 08:55:43 +02:00
parent da7712bc80
commit a1ed66b392
24 changed files with 1441 additions and 5 deletions
+4
View File
@@ -4,6 +4,9 @@
*.pyc
*.swp
.DS_Store
# Build-time raw inputs for the popular-sites DB (only sites.db is committed)
assets/sites/raw/
.atom/
.buildlog/
.history
@@ -31,6 +34,7 @@ migrate_working_dir/
.pub/
/build/
assets/quotes/quotes.db
assets/sites/sites.db
assets/bangs/*
!assets/bangs/
!assets/bangs/weblibre_bangs.json
+1
View File
@@ -16,6 +16,7 @@ targets:
databases:
bangs: lib/features/bangs/data/database/database.dart
quotes: lib/features/quotes/data/database/database.dart
sites: lib/features/popular_sites/data/database/database.dart
tabs: lib/features/geckoview/features/tabs/data/database/database.dart
user: lib/features/user/data/database/database.dart
web_feed: lib/features/web_feed/data/database/database.dart
@@ -0,0 +1,58 @@
{
"_meta": {
"description": "This file contains a serialized version of schema entities for drift.",
"version": "1.3.0"
},
"options": {
"store_date_time_values_as_text": false
},
"entities": [
{
"id": 0,
"references": [],
"type": "table",
"data": {
"name": "sites",
"was_declared_in_moor": true,
"columns": [
{
"name": "domain",
"getter_name": "domain",
"moor_type": "string",
"nullable": false,
"customConstraints": "NOT NULL PRIMARY KEY",
"default_dart": null,
"default_client_dart": null,
"dsl_features": [
"primary-key"
]
},
{
"name": "rank",
"getter_name": "rank",
"moor_type": "int",
"nullable": false,
"customConstraints": "NOT NULL",
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
}
],
"is_virtual": false,
"without_rowid": false,
"constraints": []
}
}
],
"fixed_sql": [
{
"name": "sites",
"sql": [
{
"dialect": "sqlite",
"sql": "CREATE TABLE IF NOT EXISTS \"sites\" (\"domain\" TEXT NOT NULL PRIMARY KEY, \"rank\" INTEGER NOT NULL);"
}
]
}
]
}
@@ -44,7 +44,7 @@ final class EngineSettingsReplicationServiceProvider
}
String _$engineSettingsReplicationServiceHash() =>
r'00439944023e8336dc879c70bb578120e221676d';
r'a3d7628b7662ca8b828586e3fcf4e8925adbe062';
abstract class _$EngineSettingsReplicationService extends $Notifier<void> {
void build();
@@ -28,6 +28,7 @@ const _$SearchModuleTypeEnumMap = {
SearchModuleType.history: 'history',
SearchModuleType.localHistory: 'localHistory',
SearchModuleType.combinedHistory: 'combinedHistory',
SearchModuleType.popularSites: 'popularSites',
SearchModuleType.historyHighlights: 'historyHighlights',
SearchModuleType.topSites: 'topSites',
SearchModuleType.recentHistory: 'recentHistory',
@@ -49,6 +49,13 @@ enum SearchModuleType {
/// enabling [history] and [localHistory] separately.
combinedHistory,
/// Popular-domain prefix completions from the bundled Tranco-derived
/// `sites.db` asset (filtered against adult/gambling + tracker/CDN lists).
/// Static popularity data, ranked below history and bookmarks so
/// visited/saved sites always win. Domain autocomplete: typing "git"
/// suggests github.com even with no local history.
popularSites,
historyHighlights,
topSites,
recentHistory,
@@ -67,6 +74,7 @@ enum SearchModuleType {
history => 'History (engine)',
localHistory => 'Local content',
combinedHistory => 'History',
popularSites => 'Popular Sites',
historyHighlights => 'History Highlights',
topSites => 'Top Sites',
recentHistory => 'Recent History',
@@ -100,6 +108,7 @@ enum SearchModuleGroup {
SearchModuleType.bookmarks,
SearchModuleType.articles,
SearchModuleType.combinedHistory,
SearchModuleType.popularSites,
],
);
@@ -125,7 +134,8 @@ extension SearchModuleTypeGroup on SearchModuleType {
SearchModuleType.articles ||
SearchModuleType.history ||
SearchModuleType.localHistory ||
SearchModuleType.combinedHistory => SearchModuleGroup.search,
SearchModuleType.combinedHistory ||
SearchModuleType.popularSites => SearchModuleGroup.search,
};
}
@@ -59,6 +59,7 @@ import 'package:weblibre/features/geckoview/features/search/presentation/widgets
import 'package:weblibre/features/geckoview/features/search/presentation/widgets/search_modules/feed_search.dart';
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/local_history_suggestions.dart';
import 'package:weblibre/features/geckoview/features/search/presentation/widgets/search_modules/popular_sites_suggestions.dart';
import 'package:weblibre/features/geckoview/features/search/presentation/widgets/search_modules/search_providers_section.dart';
import 'package:weblibre/features/geckoview/features/search/presentation/widgets/search_modules/search_term_suggestions_section.dart';
import 'package:weblibre/features/geckoview/features/search/presentation/widgets/search_modules/tab_search.dart';
@@ -613,6 +614,10 @@ class SearchScreen extends HookConsumerWidget {
searchTextListenable: sampledSearchText,
onUriSelected: openUriInTab,
),
SearchModuleType.popularSites: PopularSitesSuggestions(
searchTextListenable: sampledSearchText,
onUriSelected: openUriInTab,
),
};
bool canShowSearchModule(SearchModuleType type) {
@@ -0,0 +1,91 @@
/*
* Copyright (c) 2024-2026 Fabian Freund.
*
* This file is part of WebLibre
* (see https://weblibre.eu).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* 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 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:weblibre/features/geckoview/features/search/domain/providers/search_modules_view.dart';
import 'package:weblibre/features/geckoview/features/search/presentation/widgets/search_modules/search_module_section.dart';
import 'package:weblibre/features/popular_sites/domain/providers/popular_sites_search.dart';
import 'package:weblibre/presentation/hooks/on_listenable_change_selector.dart';
import 'package:weblibre/presentation/widgets/url_icon.dart';
/// Omnibar module offering popular-domain completions from the bundled
/// Tranco-derived `sites.db`. Ranked below history and bookmarks so a user's
/// own visited/saved sites always win; this fills the long tail with
/// well-known destinations (typing "git" -> github.com) even with no history.
class PopularSitesSuggestions extends HookConsumerWidget {
final ValueListenable<TextEditingValue> searchTextListenable;
final void Function(Uri uri) onUriSelected;
const PopularSitesSuggestions({
super.key,
required this.searchTextListenable,
required this.onUriSelected,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final results = ref.watch(popularSitesSearchResultsProvider);
useOnListenableChangeSelector(
searchTextListenable,
() => searchTextListenable.value.text,
() async {
await ref
.read(popularSitesSearchResultsProvider.notifier)
.search(searchTextListenable.value.text);
},
);
if (results.isEmpty) {
return const SliverToBoxAdapter(child: SizedBox.shrink());
}
return SearchModuleSection(
title: 'Popular Sites',
moduleType: SearchModuleType.popularSites,
totalCount: results.length,
contentSliverBuilder:
({required bool isCollapsed, required int visibleCount}) => [
if (!isCollapsed)
SliverList.builder(
itemCount: visibleCount,
itemBuilder: (context, index) {
final site = results[index];
final uri = Uri.parse('https://${site.domain}');
return ListTile(
key: ValueKey(site.domain),
leading: RepaintBoundary(
child: UrlIcon([uri], iconSize: 24),
),
title: Text(
site.domain,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
onTap: () => onUriSelected(uri),
);
},
),
],
);
}
}
@@ -0,0 +1,50 @@
/*
* Copyright (c) 2024-2026 Fabian Freund.
*
* This file is part of WebLibre
* (see https://weblibre.eu).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* 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 'package:drift/drift.dart';
import 'package:weblibre/features/popular_sites/data/database/daos/site.drift.dart';
import 'package:weblibre/features/popular_sites/data/database/database.dart';
import 'package:weblibre/features/popular_sites/data/database/definitions.drift.dart';
@DriftAccessor()
class SiteDao extends DatabaseAccessor<SitesDatabase> with $SiteDaoMixin {
SiteDao(super.db);
/// Popular-domain prefix completions for the omnibar, ordered by Tranco
/// popularity (densely re-ranked at build time, so `rank` is a contiguous
/// 1..N order). [prefix] is matched case-insensitively against the start of
/// the registrable domain.
///
/// LIKE metacharacters (`%`, `_`, and the `\` escape char itself) in the
/// user input are escaped so they match literally rather than acting as
/// wildcards — see the `ESCAPE '\'` clause on `searchSitesByPrefix`.
Selectable<Site> searchByPrefix(String prefix, {int limit = 8}) {
final escaped = prefix
.trim()
.toLowerCase()
.replaceAll(r'\', r'\\')
.replaceAll('%', r'\%')
.replaceAll('_', r'\_');
return db.definitionsDrift.searchSitesByPrefix(
pattern: '$escaped%',
limit: limit,
);
}
}
@@ -0,0 +1,14 @@
// dart format width=80
// ignore_for_file: type=lint
import 'package:drift/drift.dart' as i0;
import 'package:weblibre/features/popular_sites/data/database/database.dart'
as i1;
mixin $SiteDaoMixin on i0.DatabaseAccessor<i1.SitesDatabase> {
SiteDaoManager get managers => SiteDaoManager(this);
}
class SiteDaoManager {
final $SiteDaoMixin _db;
SiteDaoManager(this._db);
}
@@ -0,0 +1,41 @@
/*
* Copyright (c) 2024-2026 Fabian Freund.
*
* This file is part of WebLibre
* (see https://weblibre.eu).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* 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 'package:drift/drift.dart';
import 'package:drift_dev/api/migrations_native.dart';
import 'package:flutter/foundation.dart';
import 'package:weblibre/features/popular_sites/data/database/daos/site.dart';
import 'package:weblibre/features/popular_sites/data/database/database.drift.dart';
@DriftDatabase(include: {'definitions.drift'}, daos: [SiteDao])
class SitesDatabase extends $SitesDatabase {
@override
final int schemaVersion = 1;
@override
MigrationStrategy get migration => MigrationStrategy(
beforeOpen: (details) async {
if (kDebugMode) {
await validateDatabaseSchema();
}
},
);
SitesDatabase(super.e);
}
@@ -0,0 +1,122 @@
// dart format width=80
// ignore_for_file: type=lint
import 'package:drift/drift.dart' as i0;
import 'package:weblibre/features/popular_sites/data/database/definitions.drift.dart'
as i1;
import 'package:weblibre/features/popular_sites/data/database/daos/site.dart'
as i2;
import 'package:weblibre/features/popular_sites/data/database/database.dart'
as i3;
import 'package:drift/internal/modular.dart' as i4;
import 'package:sqlite3/common.dart' as i5;
abstract class $SitesDatabase extends i0.GeneratedDatabase {
$SitesDatabase(i0.QueryExecutor e) : super(e);
$SitesDatabaseManager get managers => $SitesDatabaseManager(this);
late final i1.Sites sites = i1.Sites(this);
late final i2.SiteDao siteDao = i2.SiteDao(this as i3.SitesDatabase);
i1.DefinitionsDrift get definitionsDrift => i4.ReadDatabaseContainer(
this,
).accessor<i1.DefinitionsDrift>(i1.DefinitionsDrift.new);
@override
Iterable<i0.TableInfo<i0.Table, Object?>> get allTables =>
allSchemaEntities.whereType<i0.TableInfo<i0.Table, Object?>>();
@override
List<i0.DatabaseSchemaEntity> get allSchemaEntities => [sites];
}
class $SitesDatabaseManager {
final $SitesDatabase _db;
$SitesDatabaseManager(this._db);
i1.$SitesTableManager get sites => i1.$SitesTableManager(_db, _db.sites);
}
extension DefineFunctions on i5.CommonDatabase {
void defineFunctions({
required String Function(int, String?) lexoRankNext,
required String Function(int, String?) lexoRankPrevious,
required String Function(String?, String?) lexoRankReorderAfter,
required String Function(String?, String?) lexoRankReorderBefore,
required int Function() generateContentHash,
required bool Function(String?) urlIndexable,
required String Function(String?) urlCanonical,
required String Function(String?) urlHost,
required String Function(String?) urlPath,
}) {
createFunction(
functionName: 'lexo_rank_next',
argumentCount: const i5.AllowedArgumentCount(2),
function: (args) {
final arg0 = args[0] as int;
final arg1 = args[1] as String?;
return lexoRankNext(arg0, arg1);
},
);
createFunction(
functionName: 'lexo_rank_previous',
argumentCount: const i5.AllowedArgumentCount(2),
function: (args) {
final arg0 = args[0] as int;
final arg1 = args[1] as String?;
return lexoRankPrevious(arg0, arg1);
},
);
createFunction(
functionName: 'lexo_rank_reorder_after',
argumentCount: const i5.AllowedArgumentCount(2),
function: (args) {
final arg0 = args[0] as String?;
final arg1 = args[1] as String?;
return lexoRankReorderAfter(arg0, arg1);
},
);
createFunction(
functionName: 'lexo_rank_reorder_before',
argumentCount: const i5.AllowedArgumentCount(2),
function: (args) {
final arg0 = args[0] as String?;
final arg1 = args[1] as String?;
return lexoRankReorderBefore(arg0, arg1);
},
);
createFunction(
functionName: 'generate_content_hash',
argumentCount: const i5.AllowedArgumentCount(0),
function: (args) {
return generateContentHash();
},
);
createFunction(
functionName: 'url_indexable',
argumentCount: const i5.AllowedArgumentCount(1),
function: (args) {
final arg0 = args[0] as String?;
return urlIndexable(arg0);
},
);
createFunction(
functionName: 'url_canonical',
argumentCount: const i5.AllowedArgumentCount(1),
function: (args) {
final arg0 = args[0] as String?;
return urlCanonical(arg0);
},
);
createFunction(
functionName: 'url_host',
argumentCount: const i5.AllowedArgumentCount(1),
function: (args) {
final arg0 = args[0] as String?;
return urlHost(arg0);
},
);
createFunction(
functionName: 'url_path',
argumentCount: const i5.AllowedArgumentCount(1),
function: (args) {
final arg0 = args[0] as String?;
return urlPath(arg0);
},
);
}
}
@@ -0,0 +1,13 @@
CREATE TABLE sites (
domain TEXT NOT NULL PRIMARY KEY,
rank INTEGER NOT NULL
);
-- Prefix completion ordered by popularity. The caller escapes LIKE
-- metacharacters in the user input and appends '%', so `ESCAPE '\'` makes any
-- literal % / _ the user typed match literally instead of as wildcards.
searchSitesByPrefix:
SELECT * FROM sites
WHERE domain LIKE :pattern ESCAPE '\'
ORDER BY rank ASC
LIMIT :limit;
@@ -0,0 +1,334 @@
// dart format width=80
// ignore_for_file: type=lint
import 'package:drift/drift.dart' as i0;
import 'package:weblibre/features/popular_sites/data/database/definitions.drift.dart'
as i1;
import 'package:drift/internal/modular.dart' as i2;
typedef $SitesCreateCompanionBuilder =
i1.SitesCompanion Function({
required String domain,
required int rank,
i0.Value<int> rowid,
});
typedef $SitesUpdateCompanionBuilder =
i1.SitesCompanion Function({
i0.Value<String> domain,
i0.Value<int> rank,
i0.Value<int> rowid,
});
class $SitesFilterComposer extends i0.Composer<i0.GeneratedDatabase, i1.Sites> {
$SitesFilterComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.ColumnFilters<String> get domain => $composableBuilder(
column: $table.domain,
builder: (column) => i0.ColumnFilters(column),
);
i0.ColumnFilters<int> get rank => $composableBuilder(
column: $table.rank,
builder: (column) => i0.ColumnFilters(column),
);
}
class $SitesOrderingComposer
extends i0.Composer<i0.GeneratedDatabase, i1.Sites> {
$SitesOrderingComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.ColumnOrderings<String> get domain => $composableBuilder(
column: $table.domain,
builder: (column) => i0.ColumnOrderings(column),
);
i0.ColumnOrderings<int> get rank => $composableBuilder(
column: $table.rank,
builder: (column) => i0.ColumnOrderings(column),
);
}
class $SitesAnnotationComposer
extends i0.Composer<i0.GeneratedDatabase, i1.Sites> {
$SitesAnnotationComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.GeneratedColumn<String> get domain =>
$composableBuilder(column: $table.domain, builder: (column) => column);
i0.GeneratedColumn<int> get rank =>
$composableBuilder(column: $table.rank, builder: (column) => column);
}
class $SitesTableManager
extends
i0.RootTableManager<
i0.GeneratedDatabase,
i1.Sites,
i1.Site,
i1.$SitesFilterComposer,
i1.$SitesOrderingComposer,
i1.$SitesAnnotationComposer,
$SitesCreateCompanionBuilder,
$SitesUpdateCompanionBuilder,
(i1.Site, i0.BaseReferences<i0.GeneratedDatabase, i1.Sites, i1.Site>),
i1.Site,
i0.PrefetchHooks Function()
> {
$SitesTableManager(i0.GeneratedDatabase db, i1.Sites table)
: super(
i0.TableManagerState(
db: db,
table: table,
createFilteringComposer: () =>
i1.$SitesFilterComposer($db: db, $table: table),
createOrderingComposer: () =>
i1.$SitesOrderingComposer($db: db, $table: table),
createComputedFieldComposer: () =>
i1.$SitesAnnotationComposer($db: db, $table: table),
updateCompanionCallback:
({
i0.Value<String> domain = const i0.Value.absent(),
i0.Value<int> rank = const i0.Value.absent(),
i0.Value<int> rowid = const i0.Value.absent(),
}) => i1.SitesCompanion(domain: domain, rank: rank, rowid: rowid),
createCompanionCallback:
({
required String domain,
required int rank,
i0.Value<int> rowid = const i0.Value.absent(),
}) => i1.SitesCompanion.insert(
domain: domain,
rank: rank,
rowid: rowid,
),
withReferenceMapper: (p0) => p0
.map((e) => (e.readTable(table), i0.BaseReferences(db, table, e)))
.toList(),
prefetchHooksCallback: null,
),
);
}
typedef $SitesProcessedTableManager =
i0.ProcessedTableManager<
i0.GeneratedDatabase,
i1.Sites,
i1.Site,
i1.$SitesFilterComposer,
i1.$SitesOrderingComposer,
i1.$SitesAnnotationComposer,
$SitesCreateCompanionBuilder,
$SitesUpdateCompanionBuilder,
(i1.Site, i0.BaseReferences<i0.GeneratedDatabase, i1.Sites, i1.Site>),
i1.Site,
i0.PrefetchHooks Function()
>;
class Sites extends i0.Table with i0.TableInfo<Sites, i1.Site> {
@override
final i0.GeneratedDatabase attachedDatabase;
final String? _alias;
Sites(this.attachedDatabase, [this._alias]);
late final i0.GeneratedColumn<String> domain = i0.GeneratedColumn<String>(
'domain',
aliasedName,
false,
type: i0.DriftSqlType.string,
requiredDuringInsert: true,
$customConstraints: 'NOT NULL PRIMARY KEY',
);
late final i0.GeneratedColumn<int> rank = i0.GeneratedColumn<int>(
'rank',
aliasedName,
false,
type: i0.DriftSqlType.int,
requiredDuringInsert: true,
$customConstraints: 'NOT NULL',
);
@override
List<i0.GeneratedColumn> get $columns => [domain, rank];
@override
String get aliasedName => _alias ?? actualTableName;
@override
String get actualTableName => $name;
static const String $name = 'sites';
@override
Set<i0.GeneratedColumn> get $primaryKey => {domain};
@override
i1.Site map(Map<String, dynamic> data, {String? tablePrefix}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
return i1.Site(
domain: attachedDatabase.typeMapping.read(
i0.DriftSqlType.string,
data['${effectivePrefix}domain'],
)!,
rank: attachedDatabase.typeMapping.read(
i0.DriftSqlType.int,
data['${effectivePrefix}rank'],
)!,
);
}
@override
Sites createAlias(String alias) {
return Sites(attachedDatabase, alias);
}
@override
bool get dontWriteConstraints => true;
}
class Site extends i0.DataClass implements i0.Insertable<i1.Site> {
final String domain;
final int rank;
const Site({required this.domain, required this.rank});
@override
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{};
map['domain'] = i0.Variable<String>(domain);
map['rank'] = i0.Variable<int>(rank);
return map;
}
factory Site.fromJson(
Map<String, dynamic> json, {
i0.ValueSerializer? serializer,
}) {
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return Site(
domain: serializer.fromJson<String>(json['domain']),
rank: serializer.fromJson<int>(json['rank']),
);
}
@override
Map<String, dynamic> toJson({i0.ValueSerializer? serializer}) {
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'domain': serializer.toJson<String>(domain),
'rank': serializer.toJson<int>(rank),
};
}
i1.Site copyWith({String? domain, int? rank}) =>
i1.Site(domain: domain ?? this.domain, rank: rank ?? this.rank);
Site copyWithCompanion(i1.SitesCompanion data) {
return Site(
domain: data.domain.present ? data.domain.value : this.domain,
rank: data.rank.present ? data.rank.value : this.rank,
);
}
@override
String toString() {
return (StringBuffer('Site(')
..write('domain: $domain, ')
..write('rank: $rank')
..write(')'))
.toString();
}
@override
int get hashCode => Object.hash(domain, rank);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is i1.Site &&
other.domain == this.domain &&
other.rank == this.rank);
}
class SitesCompanion extends i0.UpdateCompanion<i1.Site> {
final i0.Value<String> domain;
final i0.Value<int> rank;
final i0.Value<int> rowid;
const SitesCompanion({
this.domain = const i0.Value.absent(),
this.rank = const i0.Value.absent(),
this.rowid = const i0.Value.absent(),
});
SitesCompanion.insert({
required String domain,
required int rank,
this.rowid = const i0.Value.absent(),
}) : domain = i0.Value(domain),
rank = i0.Value(rank);
static i0.Insertable<i1.Site> custom({
i0.Expression<String>? domain,
i0.Expression<int>? rank,
i0.Expression<int>? rowid,
}) {
return i0.RawValuesInsertable({
if (domain != null) 'domain': domain,
if (rank != null) 'rank': rank,
if (rowid != null) 'rowid': rowid,
});
}
i1.SitesCompanion copyWith({
i0.Value<String>? domain,
i0.Value<int>? rank,
i0.Value<int>? rowid,
}) {
return i1.SitesCompanion(
domain: domain ?? this.domain,
rank: rank ?? this.rank,
rowid: rowid ?? this.rowid,
);
}
@override
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{};
if (domain.present) {
map['domain'] = i0.Variable<String>(domain.value);
}
if (rank.present) {
map['rank'] = i0.Variable<int>(rank.value);
}
if (rowid.present) {
map['rowid'] = i0.Variable<int>(rowid.value);
}
return map;
}
@override
String toString() {
return (StringBuffer('SitesCompanion(')
..write('domain: $domain, ')
..write('rank: $rank, ')
..write('rowid: $rowid')
..write(')'))
.toString();
}
}
class DefinitionsDrift extends i2.ModularAccessor {
DefinitionsDrift(i0.GeneratedDatabase db) : super(db);
i0.Selectable<i1.Site> searchSitesByPrefix({
required String pattern,
required int limit,
}) {
return customSelect(
'SELECT * FROM sites WHERE domain LIKE ?1 ESCAPE \'\\\' ORDER BY rank ASC LIMIT ?2',
variables: [i0.Variable<String>(pattern), i0.Variable<int>(limit)],
readsFrom: {sites},
).asyncMap(sites.mapFromRow);
}
i1.Sites get sites =>
i2.ReadDatabaseContainer(attachedDatabase).resultSet<i1.Sites>('sites');
}
@@ -0,0 +1,64 @@
/*
* Copyright (c) 2024-2026 Fabian Freund.
*
* This file is part of WebLibre
* (see https://weblibre.eu).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* 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:io';
import 'package:drift/drift.dart';
import 'package:drift/native.dart';
import 'package:flutter/services.dart' show rootBundle;
import 'package:path/path.dart' as p;
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:weblibre/core/database_registry.dart';
import 'package:weblibre/core/filesystem.dart';
import 'package:weblibre/features/popular_sites/data/database/database.dart';
part 'providers.g.dart';
const _sitesAssetPath = 'assets/sites/sites.db';
const _sitesDbFileName = 'sites.db';
@Riverpod(keepAlive: true)
SitesDatabase sitesDatabase(Ref ref) {
final db = SitesDatabase(
LazyDatabase(() async {
final file = File(
p.join(filesystem.profileDatabasesDir.path, _sitesDbFileName),
);
await file.parent.create(recursive: true);
final blob = await rootBundle.load(_sitesAssetPath);
final buffer = blob.buffer;
await file.writeAsBytes(
buffer.asUint8List(blob.offsetInBytes, blob.lengthInBytes),
flush: true,
);
return NativeDatabase.createInBackground(file);
}),
);
DatabaseRegistry.instance.register('sites', db);
ref.onDispose(() async {
await db.close();
});
return db;
}
@@ -0,0 +1,51 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'providers.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(sitesDatabase)
final sitesDatabaseProvider = SitesDatabaseProvider._();
final class SitesDatabaseProvider
extends $FunctionalProvider<SitesDatabase, SitesDatabase, SitesDatabase>
with $Provider<SitesDatabase> {
SitesDatabaseProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'sitesDatabaseProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$sitesDatabaseHash();
@$internal
@override
$ProviderElement<SitesDatabase> $createElement($ProviderPointer pointer) =>
$ProviderElement(pointer);
@override
SitesDatabase create(Ref ref) {
return sitesDatabase(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(SitesDatabase value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<SitesDatabase>(value),
);
}
}
String _$sitesDatabaseHash() => r'ac11ab9c1da876c18460cd762237698c01b0ec62';
@@ -0,0 +1,49 @@
/*
* Copyright (c) 2024-2026 Fabian Freund.
*
* This file is part of WebLibre
* (see https://weblibre.eu).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* 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 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:weblibre/features/popular_sites/data/database/definitions.drift.dart';
import 'package:weblibre/features/popular_sites/domain/repositories/popular_sites.dart';
part 'popular_sites_search.g.dart';
/// Holds the current popular-domain prefix completions for the omnibar
/// "Popular Sites" module. Mirrors the bookmark/tab search-result notifiers:
/// the widget watches the state list and pushes new queries through [search].
@Riverpod()
class PopularSitesSearchResults extends _$PopularSitesSearchResults {
Future<void> search(String query, {int limit = 8}) async {
if (query.trim().isEmpty) {
state = const [];
return;
}
final results = await ref
.read(popularSitesRepositoryProvider.notifier)
.searchByPrefix(query, limit: limit);
if (!ref.mounted) return;
state = results;
}
@override
List<Site> build() {
return const [];
}
}
@@ -0,0 +1,76 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'popular_sites_search.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
/// Holds the current popular-domain prefix completions for the omnibar
/// "Popular Sites" module. Mirrors the bookmark/tab search-result notifiers:
/// the widget watches the state list and pushes new queries through [search].
@ProviderFor(PopularSitesSearchResults)
final popularSitesSearchResultsProvider = PopularSitesSearchResultsProvider._();
/// Holds the current popular-domain prefix completions for the omnibar
/// "Popular Sites" module. Mirrors the bookmark/tab search-result notifiers:
/// the widget watches the state list and pushes new queries through [search].
final class PopularSitesSearchResultsProvider
extends $NotifierProvider<PopularSitesSearchResults, List<Site>> {
/// Holds the current popular-domain prefix completions for the omnibar
/// "Popular Sites" module. Mirrors the bookmark/tab search-result notifiers:
/// the widget watches the state list and pushes new queries through [search].
PopularSitesSearchResultsProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'popularSitesSearchResultsProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$popularSitesSearchResultsHash();
@$internal
@override
PopularSitesSearchResults create() => PopularSitesSearchResults();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(List<Site> value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<List<Site>>(value),
);
}
}
String _$popularSitesSearchResultsHash() =>
r'107396362aa70acf898b75137df42717ac52f1b3';
/// Holds the current popular-domain prefix completions for the omnibar
/// "Popular Sites" module. Mirrors the bookmark/tab search-result notifiers:
/// the widget watches the state list and pushes new queries through [search].
abstract class _$PopularSitesSearchResults extends $Notifier<List<Site>> {
List<Site> build();
@$mustCallSuper
@override
WhenComplete runBuild() {
final ref = this.ref as $Ref<List<Site>, List<Site>>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<List<Site>, List<Site>>,
List<Site>,
Object?,
Object?
>;
return element.handleCreate(ref, build);
}
}
@@ -0,0 +1,45 @@
/*
* Copyright (c) 2024-2026 Fabian Freund.
*
* This file is part of WebLibre
* (see https://weblibre.eu).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* 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 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:weblibre/features/popular_sites/data/database/definitions.drift.dart';
import 'package:weblibre/features/popular_sites/data/providers.dart';
part 'popular_sites.g.dart';
@Riverpod(keepAlive: true)
class PopularSitesRepository extends _$PopularSitesRepository {
/// Popular registrable domains whose start matches [prefix], ordered by
/// popularity. Returns empty for a blank prefix so the omnibar suggestion
/// module stays quiet until the user types.
Future<List<Site>> searchByPrefix(String prefix, {int limit = 8}) {
if (prefix.trim().isEmpty) {
return Future.value(const []);
}
return ref
.read(sitesDatabaseProvider)
.siteDao
.searchByPrefix(prefix, limit: limit)
.get();
}
@override
void build() {}
}
@@ -0,0 +1,63 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'popular_sites.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(PopularSitesRepository)
final popularSitesRepositoryProvider = PopularSitesRepositoryProvider._();
final class PopularSitesRepositoryProvider
extends $NotifierProvider<PopularSitesRepository, void> {
PopularSitesRepositoryProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'popularSitesRepositoryProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$popularSitesRepositoryHash();
@$internal
@override
PopularSitesRepository create() => PopularSitesRepository();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(void value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<void>(value),
);
}
}
String _$popularSitesRepositoryHash() =>
r'9825dc634b2ae9ea48a33eac8387a3e138c17b9f';
abstract class _$PopularSitesRepository extends $Notifier<void> {
void build();
@$mustCallSuper
@override
WhenComplete runBuild() {
final ref = this.ref as $Ref<void, void>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<void, void>,
void,
Object?,
Object?
>;
return element.handleCreate(ref, build);
}
}
+2 -1
View File
@@ -25,7 +25,7 @@ dependencies:
fading_scroll: ^0.9.4
fancy_password_field: ^2.0.8
fast_equatable: ^1.3.1
file_picker: ^12.0.0-beta.7
file_picker: 12.0.0-beta.5
flutter:
sdk: flutter
flutter_auto_size_text: ^5.0.0
@@ -141,6 +141,7 @@ flutter:
- assets/preferences/
- assets/legal/
- assets/quotes/
- assets/sites/
- assets/small_web/
- assets/ublock/
fonts: