prepare for multiple apps

This commit is contained in:
Fabian Freund
2026-04-06 12:23:11 +02:00
parent bd1600e8dc
commit 5afc323f04
904 changed files with 29 additions and 29 deletions
@@ -0,0 +1,54 @@
/*
* 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/extensions/uri.dart';
import 'package:weblibre/features/geckoview/features/top_sites/data/database/daos/hidden_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';
@DriftAccessor()
class HiddenTopSiteDao extends DatabaseAccessor<TopSiteDatabase>
with $HiddenTopSiteDaoMixin {
HiddenTopSiteDao(super.db);
Future<Set<String>> getHiddenUrls() async {
final rows = await db.hiddenTopSite.select().get();
return rows.map((r) => r.url.normalized.toString()).toSet();
}
Stream<Set<String>> watchHiddenUrls() {
return db.hiddenTopSite.select().watch().map(
(rows) => rows.map((r) => r.url.normalized.toString()).toSet(),
);
}
Future<void> hideUrl(Uri url) {
return db.hiddenTopSite.insertOne(
HiddenTopSiteCompanion.insert(url: url.normalized),
mode: InsertMode.insertOrIgnore,
);
}
Future<void> unhideUrl(Uri url) {
return (db.hiddenTopSite.delete()
..where((t) => t.url.equalsValue(url.normalized)))
.go();
}
}
@@ -0,0 +1,14 @@
// dart format width=80
// ignore_for_file: type=lint
import 'package:drift/drift.dart' as i0;
import 'package:weblibre/features/geckoview/features/top_sites/data/database/database.dart'
as i1;
mixin $HiddenTopSiteDaoMixin on i0.DatabaseAccessor<i1.TopSiteDatabase> {
HiddenTopSiteDaoManager get managers => HiddenTopSiteDaoManager(this);
}
class HiddenTopSiteDaoManager {
final $HiddenTopSiteDaoMixin _db;
HiddenTopSiteDaoManager(this._db);
}
@@ -0,0 +1,116 @@
/*
* 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/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';
import 'package:weblibre/features/geckoview/features/top_sites/data/entities/stored_top_site_source.dart';
@DriftAccessor()
class TopSiteDao extends DatabaseAccessor<TopSiteDatabase>
with $TopSiteDaoMixin {
TopSiteDao(super.db);
Selectable<TopSiteData> selectAllTopSites() {
return db.topSite.select()..orderBy([(t) => OrderingTerm.asc(t.orderKey)]);
}
Future<List<TopSiteData>> getAllTopSites() {
return selectAllTopSites().get();
}
Future<TopSiteData?> getTopSiteById(String id) {
return (db.topSite.select()..where((t) => t.id.equals(id)))
.getSingleOrNull();
}
Future<TopSiteData?> getTopSiteByUrl(Uri url) {
return (db.topSite.select()
..where((t) => t.url.equalsValue(url.normalized)))
.getSingleOrNull();
}
Future<int> insertSite({
required String id,
required String title,
required Uri url,
required StoredTopSiteSource source,
required String orderKey,
}) {
return db.topSite.insertOne(
TopSiteCompanion.insert(
id: id,
title: title,
url: url.normalized,
source: source,
orderKey: orderKey,
createdAt: DateTime.now(),
),
);
}
Future<int> updateSite(String id, {required String title, required Uri url}) {
return (db.topSite.update()..where((t) => t.id.equals(id))).write(
TopSiteCompanion(title: Value(title), url: Value(url.normalized)),
);
}
Future<int> deleteSite(String id) {
return (db.topSite.delete()..where((t) => t.id.equals(id))).go();
}
Future<void> assignOrderKey(String id, {required String orderKey}) {
return (db.topSite.update()..where((t) => t.id.equals(id))).write(
TopSiteCompanion(orderKey: Value(orderKey)),
);
}
Future<void> promoteToSource(
String id, {
required StoredTopSiteSource source,
required String title,
required String orderKey,
}) {
return (db.topSite.update()..where((t) => t.id.equals(id))).write(
TopSiteCompanion(
source: Value(source),
title: Value(title),
orderKey: Value(orderKey),
),
);
}
SingleSelectable<String> generateLeadingOrderKey({int bucket = 0}) {
return db.definitionsDrift.leadingOrderKey(bucket: bucket);
}
SingleSelectable<String> generateTrailingOrderKey({int bucket = 0}) {
return db.definitionsDrift.trailingOrderKey(bucket: bucket);
}
SingleOrNullSelectable<String> generateOrderKeyAfterSiteId(String id) {
return db.definitionsDrift.orderKeyAfterSite(siteId: id);
}
SingleSelectable<String> generateOrderKeyBeforeSiteId(String id) {
return db.definitionsDrift.orderKeyBeforeSite(siteId: id);
}
}
@@ -0,0 +1,14 @@
// dart format width=80
// ignore_for_file: type=lint
import 'package:drift/drift.dart' as i0;
import 'package:weblibre/features/geckoview/features/top_sites/data/database/database.dart'
as i1;
mixin $TopSiteDaoMixin on i0.DatabaseAccessor<i1.TopSiteDatabase> {
TopSiteDaoManager get managers => TopSiteDaoManager(this);
}
class TopSiteDaoManager {
final $TopSiteDaoMixin _db;
TopSiteDaoManager(this._db);
}
@@ -0,0 +1,47 @@
/*
* 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/geckoview/features/top_sites/data/database/daos/hidden_top_site.dart';
import 'package:weblibre/features/geckoview/features/top_sites/data/database/daos/top_site.dart';
import 'package:weblibre/features/geckoview/features/top_sites/data/database/database.drift.dart';
@DriftDatabase(
include: {'definitions.drift'},
daos: [TopSiteDao, HiddenTopSiteDao],
)
class TopSiteDatabase extends $TopSiteDatabase {
@override
final int schemaVersion = 1;
@override
MigrationStrategy get migration => MigrationStrategy(
beforeOpen: (details) async {
if (kDebugMode) {
await validateDatabaseSchema();
}
await customStatement('PRAGMA foreign_keys = ON;');
},
);
TopSiteDatabase(super.e);
}
@@ -0,0 +1,93 @@
// dart format width=80
// ignore_for_file: type=lint
import 'package:drift/drift.dart' as i0;
import 'package:weblibre/features/geckoview/features/top_sites/data/database/definitions.drift.dart'
as i1;
import 'package:weblibre/features/geckoview/features/top_sites/data/database/daos/top_site.dart'
as i2;
import 'package:weblibre/features/geckoview/features/top_sites/data/database/database.dart'
as i3;
import 'package:weblibre/features/geckoview/features/top_sites/data/database/daos/hidden_top_site.dart'
as i4;
import 'package:drift/internal/modular.dart' as i5;
import 'package:sqlite3/common.dart' as i6;
abstract class $TopSiteDatabase extends i0.GeneratedDatabase {
$TopSiteDatabase(i0.QueryExecutor e) : super(e);
$TopSiteDatabaseManager get managers => $TopSiteDatabaseManager(this);
late final i1.TopSite topSite = i1.TopSite(this);
late final i1.HiddenTopSite hiddenTopSite = i1.HiddenTopSite(this);
late final i2.TopSiteDao topSiteDao = i2.TopSiteDao(
this as i3.TopSiteDatabase,
);
late final i4.HiddenTopSiteDao hiddenTopSiteDao = i4.HiddenTopSiteDao(
this as i3.TopSiteDatabase,
);
i1.DefinitionsDrift get definitionsDrift => i5.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 => [
topSite,
i1.idxTopSiteOrderKey,
hiddenTopSite,
];
}
class $TopSiteDatabaseManager {
final $TopSiteDatabase _db;
$TopSiteDatabaseManager(this._db);
i1.$TopSiteTableManager get topSite =>
i1.$TopSiteTableManager(_db, _db.topSite);
i1.$HiddenTopSiteTableManager get hiddenTopSite =>
i1.$HiddenTopSiteTableManager(_db, _db.hiddenTopSite);
}
extension DefineFunctions on i6.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,
}) {
createFunction(
functionName: 'lexo_rank_next',
argumentCount: const i6.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 i6.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 i6.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 i6.AllowedArgumentCount(2),
function: (args) {
final arg0 = args[0] as String?;
final arg1 = args[1] as String?;
return lexoRankReorderBefore(arg0, arg1);
},
);
}
}
@@ -0,0 +1,64 @@
import 'package:weblibre/data/database/converters/uri.dart';
import 'package:weblibre/features/geckoview/features/top_sites/data/entities/stored_top_site_source.dart';
CREATE TABLE top_site (
id TEXT PRIMARY KEY NOT NULL,
title TEXT NOT NULL,
url TEXT NOT NULL MAPPED BY `const UriConverter()`,
source ENUM(StoredTopSiteSource) NOT NULL,
order_key TEXT NOT NULL,
created_at DATETIME NOT NULL,
UNIQUE(url)
);
CREATE INDEX idx_top_site_order_key ON top_site(order_key);
CREATE TABLE hidden_top_site (
url TEXT PRIMARY KEY NOT NULL MAPPED BY `const UriConverter()`
);
leadingOrderKey(:bucket AS INTEGER):
SELECT lexo_rank_previous(
:bucket,
(
SELECT order_key
FROM top_site
ORDER BY order_key
LIMIT 1
)
);
trailingOrderKey(:bucket AS INTEGER):
SELECT lexo_rank_next(
:bucket,
(
SELECT order_key
FROM top_site
ORDER BY order_key DESC
LIMIT 1
)
);
orderKeyAfterSite(:site_id AS TEXT):
WITH ordered_table AS (
SELECT
id,
order_key,
LEAD(order_key) OVER (ORDER BY order_key) AS next_order_key
FROM top_site
)
SELECT lexo_rank_reorder_after(order_key, next_order_key)
FROM ordered_table
WHERE id = :site_id;
orderKeyBeforeSite(:site_id AS TEXT):
WITH ordered_table AS (
SELECT
id,
order_key,
LAG(order_key) OVER (ORDER BY order_key) AS prev_order_key
FROM top_site
)
SELECT lexo_rank_reorder_before(order_key, prev_order_key)
FROM ordered_table
WHERE id = :site_id;
@@ -0,0 +1,918 @@
// dart format width=80
// ignore_for_file: type=lint
import 'package:drift/drift.dart' as i0;
import 'package:weblibre/features/geckoview/features/top_sites/data/database/definitions.drift.dart'
as i1;
import 'package:weblibre/features/geckoview/features/top_sites/data/entities/stored_top_site_source.dart'
as i2;
import 'package:weblibre/data/database/converters/uri.dart' as i3;
import 'package:drift/internal/modular.dart' as i4;
typedef $TopSiteCreateCompanionBuilder =
i1.TopSiteCompanion Function({
required String id,
required String title,
required Uri url,
required i2.StoredTopSiteSource source,
required String orderKey,
required DateTime createdAt,
i0.Value<int> rowid,
});
typedef $TopSiteUpdateCompanionBuilder =
i1.TopSiteCompanion Function({
i0.Value<String> id,
i0.Value<String> title,
i0.Value<Uri> url,
i0.Value<i2.StoredTopSiteSource> source,
i0.Value<String> orderKey,
i0.Value<DateTime> createdAt,
i0.Value<int> rowid,
});
class $TopSiteFilterComposer
extends i0.Composer<i0.GeneratedDatabase, i1.TopSite> {
$TopSiteFilterComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.ColumnFilters<String> get id => $composableBuilder(
column: $table.id,
builder: (column) => i0.ColumnFilters(column),
);
i0.ColumnFilters<String> get title => $composableBuilder(
column: $table.title,
builder: (column) => i0.ColumnFilters(column),
);
i0.ColumnWithTypeConverterFilters<Uri, Uri, String> get url =>
$composableBuilder(
column: $table.url,
builder: (column) => i0.ColumnWithTypeConverterFilters(column),
);
i0.ColumnWithTypeConverterFilters<
i2.StoredTopSiteSource,
i2.StoredTopSiteSource,
int
>
get source => $composableBuilder(
column: $table.source,
builder: (column) => i0.ColumnWithTypeConverterFilters(column),
);
i0.ColumnFilters<String> get orderKey => $composableBuilder(
column: $table.orderKey,
builder: (column) => i0.ColumnFilters(column),
);
i0.ColumnFilters<DateTime> get createdAt => $composableBuilder(
column: $table.createdAt,
builder: (column) => i0.ColumnFilters(column),
);
}
class $TopSiteOrderingComposer
extends i0.Composer<i0.GeneratedDatabase, i1.TopSite> {
$TopSiteOrderingComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.ColumnOrderings<String> get id => $composableBuilder(
column: $table.id,
builder: (column) => i0.ColumnOrderings(column),
);
i0.ColumnOrderings<String> get title => $composableBuilder(
column: $table.title,
builder: (column) => i0.ColumnOrderings(column),
);
i0.ColumnOrderings<String> get url => $composableBuilder(
column: $table.url,
builder: (column) => i0.ColumnOrderings(column),
);
i0.ColumnOrderings<int> get source => $composableBuilder(
column: $table.source,
builder: (column) => i0.ColumnOrderings(column),
);
i0.ColumnOrderings<String> get orderKey => $composableBuilder(
column: $table.orderKey,
builder: (column) => i0.ColumnOrderings(column),
);
i0.ColumnOrderings<DateTime> get createdAt => $composableBuilder(
column: $table.createdAt,
builder: (column) => i0.ColumnOrderings(column),
);
}
class $TopSiteAnnotationComposer
extends i0.Composer<i0.GeneratedDatabase, i1.TopSite> {
$TopSiteAnnotationComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.GeneratedColumn<String> get id =>
$composableBuilder(column: $table.id, builder: (column) => column);
i0.GeneratedColumn<String> get title =>
$composableBuilder(column: $table.title, builder: (column) => column);
i0.GeneratedColumnWithTypeConverter<Uri, String> get url =>
$composableBuilder(column: $table.url, builder: (column) => column);
i0.GeneratedColumnWithTypeConverter<i2.StoredTopSiteSource, int> get source =>
$composableBuilder(column: $table.source, builder: (column) => column);
i0.GeneratedColumn<String> get orderKey =>
$composableBuilder(column: $table.orderKey, builder: (column) => column);
i0.GeneratedColumn<DateTime> get createdAt =>
$composableBuilder(column: $table.createdAt, builder: (column) => column);
}
class $TopSiteTableManager
extends
i0.RootTableManager<
i0.GeneratedDatabase,
i1.TopSite,
i1.TopSiteData,
i1.$TopSiteFilterComposer,
i1.$TopSiteOrderingComposer,
i1.$TopSiteAnnotationComposer,
$TopSiteCreateCompanionBuilder,
$TopSiteUpdateCompanionBuilder,
(
i1.TopSiteData,
i0.BaseReferences<i0.GeneratedDatabase, i1.TopSite, i1.TopSiteData>,
),
i1.TopSiteData,
i0.PrefetchHooks Function()
> {
$TopSiteTableManager(i0.GeneratedDatabase db, i1.TopSite table)
: super(
i0.TableManagerState(
db: db,
table: table,
createFilteringComposer: () =>
i1.$TopSiteFilterComposer($db: db, $table: table),
createOrderingComposer: () =>
i1.$TopSiteOrderingComposer($db: db, $table: table),
createComputedFieldComposer: () =>
i1.$TopSiteAnnotationComposer($db: db, $table: table),
updateCompanionCallback:
({
i0.Value<String> id = const i0.Value.absent(),
i0.Value<String> title = const i0.Value.absent(),
i0.Value<Uri> url = const i0.Value.absent(),
i0.Value<i2.StoredTopSiteSource> source =
const i0.Value.absent(),
i0.Value<String> orderKey = const i0.Value.absent(),
i0.Value<DateTime> createdAt = const i0.Value.absent(),
i0.Value<int> rowid = const i0.Value.absent(),
}) => i1.TopSiteCompanion(
id: id,
title: title,
url: url,
source: source,
orderKey: orderKey,
createdAt: createdAt,
rowid: rowid,
),
createCompanionCallback:
({
required String id,
required String title,
required Uri url,
required i2.StoredTopSiteSource source,
required String orderKey,
required DateTime createdAt,
i0.Value<int> rowid = const i0.Value.absent(),
}) => i1.TopSiteCompanion.insert(
id: id,
title: title,
url: url,
source: source,
orderKey: orderKey,
createdAt: createdAt,
rowid: rowid,
),
withReferenceMapper: (p0) => p0
.map((e) => (e.readTable(table), i0.BaseReferences(db, table, e)))
.toList(),
prefetchHooksCallback: null,
),
);
}
typedef $TopSiteProcessedTableManager =
i0.ProcessedTableManager<
i0.GeneratedDatabase,
i1.TopSite,
i1.TopSiteData,
i1.$TopSiteFilterComposer,
i1.$TopSiteOrderingComposer,
i1.$TopSiteAnnotationComposer,
$TopSiteCreateCompanionBuilder,
$TopSiteUpdateCompanionBuilder,
(
i1.TopSiteData,
i0.BaseReferences<i0.GeneratedDatabase, i1.TopSite, i1.TopSiteData>,
),
i1.TopSiteData,
i0.PrefetchHooks Function()
>;
typedef $HiddenTopSiteCreateCompanionBuilder =
i1.HiddenTopSiteCompanion Function({required Uri url, i0.Value<int> rowid});
typedef $HiddenTopSiteUpdateCompanionBuilder =
i1.HiddenTopSiteCompanion Function({
i0.Value<Uri> url,
i0.Value<int> rowid,
});
class $HiddenTopSiteFilterComposer
extends i0.Composer<i0.GeneratedDatabase, i1.HiddenTopSite> {
$HiddenTopSiteFilterComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.ColumnWithTypeConverterFilters<Uri, Uri, String> get url =>
$composableBuilder(
column: $table.url,
builder: (column) => i0.ColumnWithTypeConverterFilters(column),
);
}
class $HiddenTopSiteOrderingComposer
extends i0.Composer<i0.GeneratedDatabase, i1.HiddenTopSite> {
$HiddenTopSiteOrderingComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.ColumnOrderings<String> get url => $composableBuilder(
column: $table.url,
builder: (column) => i0.ColumnOrderings(column),
);
}
class $HiddenTopSiteAnnotationComposer
extends i0.Composer<i0.GeneratedDatabase, i1.HiddenTopSite> {
$HiddenTopSiteAnnotationComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.GeneratedColumnWithTypeConverter<Uri, String> get url =>
$composableBuilder(column: $table.url, builder: (column) => column);
}
class $HiddenTopSiteTableManager
extends
i0.RootTableManager<
i0.GeneratedDatabase,
i1.HiddenTopSite,
i1.HiddenTopSiteData,
i1.$HiddenTopSiteFilterComposer,
i1.$HiddenTopSiteOrderingComposer,
i1.$HiddenTopSiteAnnotationComposer,
$HiddenTopSiteCreateCompanionBuilder,
$HiddenTopSiteUpdateCompanionBuilder,
(
i1.HiddenTopSiteData,
i0.BaseReferences<
i0.GeneratedDatabase,
i1.HiddenTopSite,
i1.HiddenTopSiteData
>,
),
i1.HiddenTopSiteData,
i0.PrefetchHooks Function()
> {
$HiddenTopSiteTableManager(i0.GeneratedDatabase db, i1.HiddenTopSite table)
: super(
i0.TableManagerState(
db: db,
table: table,
createFilteringComposer: () =>
i1.$HiddenTopSiteFilterComposer($db: db, $table: table),
createOrderingComposer: () =>
i1.$HiddenTopSiteOrderingComposer($db: db, $table: table),
createComputedFieldComposer: () =>
i1.$HiddenTopSiteAnnotationComposer($db: db, $table: table),
updateCompanionCallback:
({
i0.Value<Uri> url = const i0.Value.absent(),
i0.Value<int> rowid = const i0.Value.absent(),
}) => i1.HiddenTopSiteCompanion(url: url, rowid: rowid),
createCompanionCallback:
({
required Uri url,
i0.Value<int> rowid = const i0.Value.absent(),
}) => i1.HiddenTopSiteCompanion.insert(url: url, rowid: rowid),
withReferenceMapper: (p0) => p0
.map((e) => (e.readTable(table), i0.BaseReferences(db, table, e)))
.toList(),
prefetchHooksCallback: null,
),
);
}
typedef $HiddenTopSiteProcessedTableManager =
i0.ProcessedTableManager<
i0.GeneratedDatabase,
i1.HiddenTopSite,
i1.HiddenTopSiteData,
i1.$HiddenTopSiteFilterComposer,
i1.$HiddenTopSiteOrderingComposer,
i1.$HiddenTopSiteAnnotationComposer,
$HiddenTopSiteCreateCompanionBuilder,
$HiddenTopSiteUpdateCompanionBuilder,
(
i1.HiddenTopSiteData,
i0.BaseReferences<
i0.GeneratedDatabase,
i1.HiddenTopSite,
i1.HiddenTopSiteData
>,
),
i1.HiddenTopSiteData,
i0.PrefetchHooks Function()
>;
class TopSite extends i0.Table with i0.TableInfo<TopSite, i1.TopSiteData> {
@override
final i0.GeneratedDatabase attachedDatabase;
final String? _alias;
TopSite(this.attachedDatabase, [this._alias]);
late final i0.GeneratedColumn<String> id = i0.GeneratedColumn<String>(
'id',
aliasedName,
false,
type: i0.DriftSqlType.string,
requiredDuringInsert: true,
$customConstraints: 'PRIMARY KEY NOT NULL',
);
late final i0.GeneratedColumn<String> title = i0.GeneratedColumn<String>(
'title',
aliasedName,
false,
type: i0.DriftSqlType.string,
requiredDuringInsert: true,
$customConstraints: 'NOT NULL',
);
late final i0.GeneratedColumnWithTypeConverter<Uri, String> url =
i0.GeneratedColumn<String>(
'url',
aliasedName,
false,
type: i0.DriftSqlType.string,
requiredDuringInsert: true,
$customConstraints: 'NOT NULL',
).withConverter<Uri>(i1.TopSite.$converterurl);
late final i0.GeneratedColumnWithTypeConverter<i2.StoredTopSiteSource, int>
source = i0.GeneratedColumn<int>(
'source',
aliasedName,
false,
type: i0.DriftSqlType.int,
requiredDuringInsert: true,
$customConstraints: 'NOT NULL',
).withConverter<i2.StoredTopSiteSource>(i1.TopSite.$convertersource);
late final i0.GeneratedColumn<String> orderKey = i0.GeneratedColumn<String>(
'order_key',
aliasedName,
false,
type: i0.DriftSqlType.string,
requiredDuringInsert: true,
$customConstraints: 'NOT NULL',
);
late final i0.GeneratedColumn<DateTime> createdAt =
i0.GeneratedColumn<DateTime>(
'created_at',
aliasedName,
false,
type: i0.DriftSqlType.dateTime,
requiredDuringInsert: true,
$customConstraints: 'NOT NULL',
);
@override
List<i0.GeneratedColumn> get $columns => [
id,
title,
url,
source,
orderKey,
createdAt,
];
@override
String get aliasedName => _alias ?? actualTableName;
@override
String get actualTableName => $name;
static const String $name = 'top_site';
@override
Set<i0.GeneratedColumn> get $primaryKey => {id};
@override
List<Set<i0.GeneratedColumn>> get uniqueKeys => [
{url},
];
@override
i1.TopSiteData map(Map<String, dynamic> data, {String? tablePrefix}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
return i1.TopSiteData(
id: attachedDatabase.typeMapping.read(
i0.DriftSqlType.string,
data['${effectivePrefix}id'],
)!,
title: attachedDatabase.typeMapping.read(
i0.DriftSqlType.string,
data['${effectivePrefix}title'],
)!,
url: i1.TopSite.$converterurl.fromSql(
attachedDatabase.typeMapping.read(
i0.DriftSqlType.string,
data['${effectivePrefix}url'],
)!,
),
source: i1.TopSite.$convertersource.fromSql(
attachedDatabase.typeMapping.read(
i0.DriftSqlType.int,
data['${effectivePrefix}source'],
)!,
),
orderKey: attachedDatabase.typeMapping.read(
i0.DriftSqlType.string,
data['${effectivePrefix}order_key'],
)!,
createdAt: attachedDatabase.typeMapping.read(
i0.DriftSqlType.dateTime,
data['${effectivePrefix}created_at'],
)!,
);
}
@override
TopSite createAlias(String alias) {
return TopSite(attachedDatabase, alias);
}
static i0.TypeConverter<Uri, String> $converterurl = const i3.UriConverter();
static i0.JsonTypeConverter2<i2.StoredTopSiteSource, int, int>
$convertersource = const i0.EnumIndexConverter<i2.StoredTopSiteSource>(
i2.StoredTopSiteSource.values,
);
@override
List<String> get customConstraints => const ['UNIQUE(url)'];
@override
bool get dontWriteConstraints => true;
}
class TopSiteData extends i0.DataClass
implements i0.Insertable<i1.TopSiteData> {
final String id;
final String title;
final Uri url;
final i2.StoredTopSiteSource source;
final String orderKey;
final DateTime createdAt;
const TopSiteData({
required this.id,
required this.title,
required this.url,
required this.source,
required this.orderKey,
required this.createdAt,
});
@override
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{};
map['id'] = i0.Variable<String>(id);
map['title'] = i0.Variable<String>(title);
{
map['url'] = i0.Variable<String>(i1.TopSite.$converterurl.toSql(url));
}
{
map['source'] = i0.Variable<int>(
i1.TopSite.$convertersource.toSql(source),
);
}
map['order_key'] = i0.Variable<String>(orderKey);
map['created_at'] = i0.Variable<DateTime>(createdAt);
return map;
}
factory TopSiteData.fromJson(
Map<String, dynamic> json, {
i0.ValueSerializer? serializer,
}) {
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return TopSiteData(
id: serializer.fromJson<String>(json['id']),
title: serializer.fromJson<String>(json['title']),
url: serializer.fromJson<Uri>(json['url']),
source: i1.TopSite.$convertersource.fromJson(
serializer.fromJson<int>(json['source']),
),
orderKey: serializer.fromJson<String>(json['order_key']),
createdAt: serializer.fromJson<DateTime>(json['created_at']),
);
}
@override
Map<String, dynamic> toJson({i0.ValueSerializer? serializer}) {
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'id': serializer.toJson<String>(id),
'title': serializer.toJson<String>(title),
'url': serializer.toJson<Uri>(url),
'source': serializer.toJson<int>(
i1.TopSite.$convertersource.toJson(source),
),
'order_key': serializer.toJson<String>(orderKey),
'created_at': serializer.toJson<DateTime>(createdAt),
};
}
i1.TopSiteData copyWith({
String? id,
String? title,
Uri? url,
i2.StoredTopSiteSource? source,
String? orderKey,
DateTime? createdAt,
}) => i1.TopSiteData(
id: id ?? this.id,
title: title ?? this.title,
url: url ?? this.url,
source: source ?? this.source,
orderKey: orderKey ?? this.orderKey,
createdAt: createdAt ?? this.createdAt,
);
TopSiteData copyWithCompanion(i1.TopSiteCompanion data) {
return TopSiteData(
id: data.id.present ? data.id.value : this.id,
title: data.title.present ? data.title.value : this.title,
url: data.url.present ? data.url.value : this.url,
source: data.source.present ? data.source.value : this.source,
orderKey: data.orderKey.present ? data.orderKey.value : this.orderKey,
createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt,
);
}
@override
String toString() {
return (StringBuffer('TopSiteData(')
..write('id: $id, ')
..write('title: $title, ')
..write('url: $url, ')
..write('source: $source, ')
..write('orderKey: $orderKey, ')
..write('createdAt: $createdAt')
..write(')'))
.toString();
}
@override
int get hashCode => Object.hash(id, title, url, source, orderKey, createdAt);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is i1.TopSiteData &&
other.id == this.id &&
other.title == this.title &&
other.url == this.url &&
other.source == this.source &&
other.orderKey == this.orderKey &&
other.createdAt == this.createdAt);
}
class TopSiteCompanion extends i0.UpdateCompanion<i1.TopSiteData> {
final i0.Value<String> id;
final i0.Value<String> title;
final i0.Value<Uri> url;
final i0.Value<i2.StoredTopSiteSource> source;
final i0.Value<String> orderKey;
final i0.Value<DateTime> createdAt;
final i0.Value<int> rowid;
const TopSiteCompanion({
this.id = const i0.Value.absent(),
this.title = const i0.Value.absent(),
this.url = const i0.Value.absent(),
this.source = const i0.Value.absent(),
this.orderKey = const i0.Value.absent(),
this.createdAt = const i0.Value.absent(),
this.rowid = const i0.Value.absent(),
});
TopSiteCompanion.insert({
required String id,
required String title,
required Uri url,
required i2.StoredTopSiteSource source,
required String orderKey,
required DateTime createdAt,
this.rowid = const i0.Value.absent(),
}) : id = i0.Value(id),
title = i0.Value(title),
url = i0.Value(url),
source = i0.Value(source),
orderKey = i0.Value(orderKey),
createdAt = i0.Value(createdAt);
static i0.Insertable<i1.TopSiteData> custom({
i0.Expression<String>? id,
i0.Expression<String>? title,
i0.Expression<String>? url,
i0.Expression<int>? source,
i0.Expression<String>? orderKey,
i0.Expression<DateTime>? createdAt,
i0.Expression<int>? rowid,
}) {
return i0.RawValuesInsertable({
if (id != null) 'id': id,
if (title != null) 'title': title,
if (url != null) 'url': url,
if (source != null) 'source': source,
if (orderKey != null) 'order_key': orderKey,
if (createdAt != null) 'created_at': createdAt,
if (rowid != null) 'rowid': rowid,
});
}
i1.TopSiteCompanion copyWith({
i0.Value<String>? id,
i0.Value<String>? title,
i0.Value<Uri>? url,
i0.Value<i2.StoredTopSiteSource>? source,
i0.Value<String>? orderKey,
i0.Value<DateTime>? createdAt,
i0.Value<int>? rowid,
}) {
return i1.TopSiteCompanion(
id: id ?? this.id,
title: title ?? this.title,
url: url ?? this.url,
source: source ?? this.source,
orderKey: orderKey ?? this.orderKey,
createdAt: createdAt ?? this.createdAt,
rowid: rowid ?? this.rowid,
);
}
@override
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{};
if (id.present) {
map['id'] = i0.Variable<String>(id.value);
}
if (title.present) {
map['title'] = i0.Variable<String>(title.value);
}
if (url.present) {
map['url'] = i0.Variable<String>(
i1.TopSite.$converterurl.toSql(url.value),
);
}
if (source.present) {
map['source'] = i0.Variable<int>(
i1.TopSite.$convertersource.toSql(source.value),
);
}
if (orderKey.present) {
map['order_key'] = i0.Variable<String>(orderKey.value);
}
if (createdAt.present) {
map['created_at'] = i0.Variable<DateTime>(createdAt.value);
}
if (rowid.present) {
map['rowid'] = i0.Variable<int>(rowid.value);
}
return map;
}
@override
String toString() {
return (StringBuffer('TopSiteCompanion(')
..write('id: $id, ')
..write('title: $title, ')
..write('url: $url, ')
..write('source: $source, ')
..write('orderKey: $orderKey, ')
..write('createdAt: $createdAt, ')
..write('rowid: $rowid')
..write(')'))
.toString();
}
}
i0.Index get idxTopSiteOrderKey => i0.Index(
'idx_top_site_order_key',
'CREATE INDEX idx_top_site_order_key ON top_site (order_key)',
);
class HiddenTopSite extends i0.Table
with i0.TableInfo<HiddenTopSite, i1.HiddenTopSiteData> {
@override
final i0.GeneratedDatabase attachedDatabase;
final String? _alias;
HiddenTopSite(this.attachedDatabase, [this._alias]);
late final i0.GeneratedColumnWithTypeConverter<Uri, String> url =
i0.GeneratedColumn<String>(
'url',
aliasedName,
false,
type: i0.DriftSqlType.string,
requiredDuringInsert: true,
$customConstraints: 'PRIMARY KEY NOT NULL',
).withConverter<Uri>(i1.HiddenTopSite.$converterurl);
@override
List<i0.GeneratedColumn> get $columns => [url];
@override
String get aliasedName => _alias ?? actualTableName;
@override
String get actualTableName => $name;
static const String $name = 'hidden_top_site';
@override
Set<i0.GeneratedColumn> get $primaryKey => {url};
@override
i1.HiddenTopSiteData map(Map<String, dynamic> data, {String? tablePrefix}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
return i1.HiddenTopSiteData(
url: i1.HiddenTopSite.$converterurl.fromSql(
attachedDatabase.typeMapping.read(
i0.DriftSqlType.string,
data['${effectivePrefix}url'],
)!,
),
);
}
@override
HiddenTopSite createAlias(String alias) {
return HiddenTopSite(attachedDatabase, alias);
}
static i0.TypeConverter<Uri, String> $converterurl = const i3.UriConverter();
@override
bool get dontWriteConstraints => true;
}
class HiddenTopSiteData extends i0.DataClass
implements i0.Insertable<i1.HiddenTopSiteData> {
final Uri url;
const HiddenTopSiteData({required this.url});
@override
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{};
{
map['url'] = i0.Variable<String>(
i1.HiddenTopSite.$converterurl.toSql(url),
);
}
return map;
}
factory HiddenTopSiteData.fromJson(
Map<String, dynamic> json, {
i0.ValueSerializer? serializer,
}) {
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return HiddenTopSiteData(url: serializer.fromJson<Uri>(json['url']));
}
@override
Map<String, dynamic> toJson({i0.ValueSerializer? serializer}) {
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{'url': serializer.toJson<Uri>(url)};
}
i1.HiddenTopSiteData copyWith({Uri? url}) =>
i1.HiddenTopSiteData(url: url ?? this.url);
HiddenTopSiteData copyWithCompanion(i1.HiddenTopSiteCompanion data) {
return HiddenTopSiteData(url: data.url.present ? data.url.value : this.url);
}
@override
String toString() {
return (StringBuffer('HiddenTopSiteData(')
..write('url: $url')
..write(')'))
.toString();
}
@override
int get hashCode => url.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is i1.HiddenTopSiteData && other.url == this.url);
}
class HiddenTopSiteCompanion extends i0.UpdateCompanion<i1.HiddenTopSiteData> {
final i0.Value<Uri> url;
final i0.Value<int> rowid;
const HiddenTopSiteCompanion({
this.url = const i0.Value.absent(),
this.rowid = const i0.Value.absent(),
});
HiddenTopSiteCompanion.insert({
required Uri url,
this.rowid = const i0.Value.absent(),
}) : url = i0.Value(url);
static i0.Insertable<i1.HiddenTopSiteData> custom({
i0.Expression<String>? url,
i0.Expression<int>? rowid,
}) {
return i0.RawValuesInsertable({
if (url != null) 'url': url,
if (rowid != null) 'rowid': rowid,
});
}
i1.HiddenTopSiteCompanion copyWith({
i0.Value<Uri>? url,
i0.Value<int>? rowid,
}) {
return i1.HiddenTopSiteCompanion(
url: url ?? this.url,
rowid: rowid ?? this.rowid,
);
}
@override
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{};
if (url.present) {
map['url'] = i0.Variable<String>(
i1.HiddenTopSite.$converterurl.toSql(url.value),
);
}
if (rowid.present) {
map['rowid'] = i0.Variable<int>(rowid.value);
}
return map;
}
@override
String toString() {
return (StringBuffer('HiddenTopSiteCompanion(')
..write('url: $url, ')
..write('rowid: $rowid')
..write(')'))
.toString();
}
}
class DefinitionsDrift extends i4.ModularAccessor {
DefinitionsDrift(i0.GeneratedDatabase db) : super(db);
i0.Selectable<String> leadingOrderKey({required int bucket}) {
return customSelect(
'SELECT lexo_rank_previous(?1, (SELECT order_key FROM top_site ORDER BY order_key LIMIT 1)) AS _c0',
variables: [i0.Variable<int>(bucket)],
readsFrom: {topSite},
).map((i0.QueryRow row) => row.read<String>('_c0'));
}
i0.Selectable<String> trailingOrderKey({required int bucket}) {
return customSelect(
'SELECT lexo_rank_next(?1, (SELECT order_key FROM top_site ORDER BY order_key DESC LIMIT 1)) AS _c0',
variables: [i0.Variable<int>(bucket)],
readsFrom: {topSite},
).map((i0.QueryRow row) => row.read<String>('_c0'));
}
i0.Selectable<String> orderKeyAfterSite({required String siteId}) {
return customSelect(
'WITH ordered_table AS (SELECT id, order_key, LEAD(order_key)OVER (ORDER BY order_key RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW EXCLUDE NO OTHERS) AS next_order_key FROM top_site) SELECT lexo_rank_reorder_after(order_key, next_order_key) AS _c0 FROM ordered_table WHERE id = ?1',
variables: [i0.Variable<String>(siteId)],
readsFrom: {topSite},
).map((i0.QueryRow row) => row.read<String>('_c0'));
}
i0.Selectable<String> orderKeyBeforeSite({required String siteId}) {
return customSelect(
'WITH ordered_table AS (SELECT id, order_key, LAG(order_key)OVER (ORDER BY order_key RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW EXCLUDE NO OTHERS) AS prev_order_key FROM top_site) SELECT lexo_rank_reorder_before(order_key, prev_order_key) AS _c0 FROM ordered_table WHERE id = ?1',
variables: [i0.Variable<String>(siteId)],
readsFrom: {topSite},
).map((i0.QueryRow row) => row.read<String>('_c0'));
}
i1.TopSite get topSite => i4.ReadDatabaseContainer(
attachedDatabase,
).resultSet<i1.TopSite>('top_site');
}
@@ -0,0 +1,20 @@
/*
* 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/>.
*/
enum StoredTopSiteSource { defaultSite, pinned }
@@ -0,0 +1,62 @@
/*
* 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:path/path.dart' as p;
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:sqlite3_flutter_libs/sqlite3_flutter_libs.dart';
import 'package:weblibre/core/database_registry.dart';
import 'package:weblibre/core/filesystem.dart';
import 'package:weblibre/data/database/functions/lexo_rank_functions.dart';
import 'package:weblibre/features/geckoview/features/top_sites/data/database/database.dart';
part 'providers.g.dart';
@Riverpod(keepAlive: true)
TopSiteDatabase topSiteDatabase(Ref ref) {
final db = TopSiteDatabase(
LazyDatabase(() async {
final file = File(
p.join(filesystem.profileDatabasesDir.path, 'top_site.db'),
);
if (Platform.isAndroid) {
await applyWorkaroundToOpenSqlite3OnOldAndroidVersions();
}
return NativeDatabase.createInBackground(
file,
setup: (database) {
registerLexorankFunctions(database);
},
);
}),
);
DatabaseRegistry.instance.register('topSite', db);
ref.onDispose(() async {
await db.close();
});
return db;
}
@@ -0,0 +1,52 @@
// 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(topSiteDatabase)
final topSiteDatabaseProvider = TopSiteDatabaseProvider._();
final class TopSiteDatabaseProvider
extends
$FunctionalProvider<TopSiteDatabase, TopSiteDatabase, TopSiteDatabase>
with $Provider<TopSiteDatabase> {
TopSiteDatabaseProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'topSiteDatabaseProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$topSiteDatabaseHash();
@$internal
@override
$ProviderElement<TopSiteDatabase> $createElement($ProviderPointer pointer) =>
$ProviderElement(pointer);
@override
TopSiteDatabase create(Ref ref) {
return topSiteDatabase(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(TopSiteDatabase value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<TopSiteDatabase>(value),
);
}
}
String _$topSiteDatabaseHash() => r'a5afe9a807174ae3382931612b25f959efccdded';
@@ -0,0 +1,72 @@
/*
* 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:fast_equatable/fast_equatable.dart';
import 'package:weblibre/features/geckoview/features/top_sites/domain/entities/top_site_source.dart';
class TopSiteItem with FastEquatable {
final String? id;
final String title;
final Uri url;
final TopSiteSource source;
final String? orderKey;
final DateTime? createdAt;
final String? previewImageUrl;
final double? historyScore;
final int? historyPlaceId;
TopSiteItem({
this.id,
required this.title,
required this.url,
required this.source,
this.orderKey,
this.createdAt,
this.previewImageUrl,
this.historyScore,
this.historyPlaceId,
});
bool get isPinned => source == TopSiteSource.pinned;
bool get isDefault => source == TopSiteSource.defaultSite;
bool get isHistory => source == TopSiteSource.history;
bool get isPersisted => isPinned || isDefault;
bool get isReorderable => isPersisted;
bool get isEditable => isPersisted;
bool get isRemovable => isPersisted;
@override
List<Object?> get hashParameters => [
id,
title,
url,
source,
orderKey,
createdAt,
previewImageUrl,
historyScore,
historyPlaceId,
];
}
@@ -0,0 +1,20 @@
/*
* 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/>.
*/
enum TopSiteSource { defaultSite, pinned, history }
@@ -0,0 +1,51 @@
/*
* 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/geckoview/features/top_sites/domain/entities/top_site_item.dart';
import 'package:weblibre/features/geckoview/features/top_sites/domain/repositories/top_site_repository.dart';
part 'providers.g.dart';
const defaultTopSites = [
(title: 'Wikipedia', url: 'https://wikipedia.org/'),
(title: 'OpenStreetMap', url: 'https://www.openstreetmap.org/'),
(
title:
'Internet Archive: Digital Library of Free & Borrowable Texts, Movies, Music & Wayback Machine',
url: 'https://archive.org/',
),
(
title: 'Mozilla - Internet for people, not profit',
url: 'https://www.mozilla.org/',
),
(title: 'Tor Project | Anonymity Online', url: 'https://www.torproject.org/'),
];
@Riverpod()
Stream<List<TopSiteItem>> topSiteList(Ref ref, {int limit = 8}) {
return ref
.watch(topSiteRepositoryProvider.notifier)
.watchTopSites(limit: limit);
}
@Riverpod()
Stream<List<TopSiteItem>> pinnedTopSiteList(Ref ref) {
return ref.watch(topSiteRepositoryProvider.notifier).watchPinnedTopSites();
}
@@ -0,0 +1,128 @@
// 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(topSiteList)
final topSiteListProvider = TopSiteListFamily._();
final class TopSiteListProvider
extends
$FunctionalProvider<
AsyncValue<List<TopSiteItem>>,
List<TopSiteItem>,
Stream<List<TopSiteItem>>
>
with
$FutureModifier<List<TopSiteItem>>,
$StreamProvider<List<TopSiteItem>> {
TopSiteListProvider._({
required TopSiteListFamily super.from,
required int super.argument,
}) : super(
retry: null,
name: r'topSiteListProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$topSiteListHash();
@override
String toString() {
return r'topSiteListProvider'
''
'($argument)';
}
@$internal
@override
$StreamProviderElement<List<TopSiteItem>> $createElement(
$ProviderPointer pointer,
) => $StreamProviderElement(pointer);
@override
Stream<List<TopSiteItem>> create(Ref ref) {
final argument = this.argument as int;
return topSiteList(ref, limit: argument);
}
@override
bool operator ==(Object other) {
return other is TopSiteListProvider && other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$topSiteListHash() => r'c9cb226f7a4368b907b85230ef2e8a4e0c73a199';
final class TopSiteListFamily extends $Family
with $FunctionalFamilyOverride<Stream<List<TopSiteItem>>, int> {
TopSiteListFamily._()
: super(
retry: null,
name: r'topSiteListProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
TopSiteListProvider call({int limit = 8}) =>
TopSiteListProvider._(argument: limit, from: this);
@override
String toString() => r'topSiteListProvider';
}
@ProviderFor(pinnedTopSiteList)
final pinnedTopSiteListProvider = PinnedTopSiteListProvider._();
final class PinnedTopSiteListProvider
extends
$FunctionalProvider<
AsyncValue<List<TopSiteItem>>,
List<TopSiteItem>,
Stream<List<TopSiteItem>>
>
with
$FutureModifier<List<TopSiteItem>>,
$StreamProvider<List<TopSiteItem>> {
PinnedTopSiteListProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'pinnedTopSiteListProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$pinnedTopSiteListHash();
@$internal
@override
$StreamProviderElement<List<TopSiteItem>> $createElement(
$ProviderPointer pointer,
) => $StreamProviderElement(pointer);
@override
Stream<List<TopSiteItem>> create(Ref ref) {
return pinnedTopSiteList(ref);
}
}
String _$pinnedTopSiteListHash() => r'83adb6584ef738be14a2e0b55b33986ebb4520a9';
@@ -0,0 +1,362 @@
/*
* 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:async';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:rxdart/rxdart.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';
import 'package:weblibre/features/geckoview/features/top_sites/data/providers.dart';
import 'package:weblibre/features/geckoview/features/top_sites/domain/entities/top_site_item.dart';
import 'package:weblibre/features/geckoview/features/top_sites/domain/entities/top_site_source.dart';
import 'package:weblibre/features/geckoview/features/top_sites/domain/providers.dart';
import 'package:weblibre/utils/uri_parser.dart' as uri_parser;
part 'top_site_repository.g.dart';
@Riverpod(keepAlive: true)
class TopSiteRepository extends _$TopSiteRepository {
Stream<List<TopSiteItem>> watchTopSites({int limit = 8}) {
final db = ref.read(topSiteDatabaseProvider);
return CombineLatestStream.combine2(
db.topSiteDao.selectAllTopSites().watch(),
db.hiddenTopSiteDao.watchHiddenUrls(),
(List<TopSiteData> rows, Set<String> hiddenUrls) => (rows, hiddenUrls),
).asyncMap((record) async {
final (rows, hiddenUrls) = record;
final persistedItems = rows.map(_mapRow).toList();
final persistedUrls = persistedItems
.map((s) => s.url.normalized.toString())
.toSet();
final defaultItems = _getVisibleDefaults(
persistedUrls: persistedUrls,
hiddenUrls: hiddenUrls,
);
final combined = [...persistedItems, ...defaultItems];
final targetCount = limit < 0 ? 0 : limit;
if (combined.length >= targetCount) {
return combined;
}
final remaining = targetCount - combined.length;
final excludeUrls = {
...persistedUrls,
...defaultItems.map((s) => s.url.normalized.toString()),
};
final historyItems = await _getHistoryItems(
limit: remaining,
excludeUrls: excludeUrls,
);
return [...combined, ...historyItems];
});
}
Future<List<TopSiteItem>> getTopSites({int limit = 8}) async {
final db = ref.read(topSiteDatabaseProvider);
final rows = await db.topSiteDao.getAllTopSites();
final hiddenUrls = await db.hiddenTopSiteDao.getHiddenUrls();
final persistedItems = rows.map(_mapRow).toList();
final persistedUrls = persistedItems
.map((s) => s.url.normalized.toString())
.toSet();
final defaultItems = _getVisibleDefaults(
persistedUrls: persistedUrls,
hiddenUrls: hiddenUrls,
);
final combined = [...persistedItems, ...defaultItems];
final targetCount = limit < 0 ? 0 : limit;
if (combined.length >= targetCount) {
return combined;
}
final remaining = targetCount - combined.length;
final excludeUrls = {
...persistedUrls,
...defaultItems.map((s) => s.url.normalized.toString()),
};
final historyItems = await _getHistoryItems(
limit: remaining,
excludeUrls: excludeUrls,
);
return [...combined, ...historyItems];
}
Stream<List<TopSiteItem>> watchPinnedTopSites() {
final db = ref.read(topSiteDatabaseProvider);
return db.topSiteDao.selectAllTopSites().watch().map(
(rows) => rows
.where((r) => r.source == StoredTopSiteSource.pinned)
.map(_mapRow)
.toList(),
);
}
static Uri _validateUrl(Uri url) {
final normalized = url.normalized;
final parsed = uri_parser.tryParseUrl(
normalized.toString(),
eagerParsing: true,
);
if (parsed == null) {
throw ArgumentError.value(url.toString(), 'url', 'Invalid URL');
}
return normalized;
}
/// Persists a default site to the database so it can be reordered.
/// Returns the database ID.
Future<String> _persistDefault({
required String title,
required Uri url,
required String orderKey,
}) async {
final db = ref.read(topSiteDatabaseProvider);
final id = uuid.v7();
await db.topSiteDao.insertSite(
id: id,
title: title,
url: url,
source: StoredTopSiteSource.defaultSite,
orderKey: orderKey,
);
return id;
}
/// Ensures a default site is persisted in the database. If it already exists,
/// returns its existing ID. Otherwise inserts it with a trailing order key.
Future<String> ensureDefaultPersisted({
required String title,
required Uri url,
}) async {
final db = ref.read(topSiteDatabaseProvider);
final existing = await db.topSiteDao.getTopSiteByUrl(url);
if (existing != null) return existing.id;
final orderKey = await db.topSiteDao.generateTrailingOrderKey().getSingle();
return _persistDefault(title: title, url: url, orderKey: orderKey);
}
Future<String> addPinnedSite({
required String title,
required Uri url,
}) async {
_validateUrl(url);
final db = ref.read(topSiteDatabaseProvider);
// If it was a hidden default, unhide it
await db.hiddenTopSiteDao.unhideUrl(url);
// Check if URL already exists
final existing = await db.topSiteDao.getTopSiteByUrl(url);
if (existing != null) {
final leadingKey = await db.topSiteDao
.generateLeadingOrderKey()
.getSingle();
await db.topSiteDao.promoteToSource(
existing.id,
source: StoredTopSiteSource.pinned,
title: title,
orderKey: leadingKey,
);
return existing.id;
}
final id = uuid.v7();
final orderKey = await db.topSiteDao.generateLeadingOrderKey().getSingle();
await db.topSiteDao.insertSite(
id: id,
title: title,
url: url,
source: StoredTopSiteSource.pinned,
orderKey: orderKey,
);
return id;
}
Future<void> updateSite({
required String id,
required String title,
required Uri url,
}) {
_validateUrl(url);
return ref
.read(topSiteDatabaseProvider)
.topSiteDao
.updateSite(id, title: title, url: url);
}
Future<void> removeSite(String id) {
return ref.read(topSiteDatabaseProvider).topSiteDao.deleteSite(id);
}
Future<void> hideDefaultSite(Uri url) {
return ref.read(topSiteDatabaseProvider).hiddenTopSiteDao.hideUrl(url);
}
Future<bool> isPinnedTopSiteUrl(Uri url) async {
final row = await ref
.read(topSiteDatabaseProvider)
.topSiteDao
.getTopSiteByUrl(url);
return row != null && row.source == StoredTopSiteSource.pinned;
}
Future<bool> unpinSiteByUrl(Uri url) async {
final db = ref.read(topSiteDatabaseProvider);
final row = await db.topSiteDao.getTopSiteByUrl(url);
if (row == null) return false;
// If it was a pinned default, demote back to defaultSite source
final isDefaultUrl = defaultTopSites.any(
(d) => Uri.parse(d.url).normalized == url.normalized,
);
if (isDefaultUrl) {
final trailingKey = await db.topSiteDao
.generateTrailingOrderKey()
.getSingle();
await db.topSiteDao.promoteToSource(
row.id,
source: StoredTopSiteSource.defaultSite,
title: row.title,
orderKey: trailingKey,
);
} else {
await db.topSiteDao.deleteSite(row.id);
}
return true;
}
Future<void> assignOrderKey(String id, String orderKey) {
return ref
.read(topSiteDatabaseProvider)
.topSiteDao
.assignOrderKey(id, orderKey: orderKey);
}
Future<String> getLeadingOrderKey() {
return ref
.read(topSiteDatabaseProvider)
.topSiteDao
.generateLeadingOrderKey()
.getSingle();
}
Future<String> getTrailingOrderKey() {
return ref
.read(topSiteDatabaseProvider)
.topSiteDao
.generateTrailingOrderKey()
.getSingle();
}
Future<String?> getOrderKeyAfterSite(String id) {
return ref
.read(topSiteDatabaseProvider)
.topSiteDao
.generateOrderKeyAfterSiteId(id)
.getSingleOrNull();
}
Future<String> getOrderKeyBeforeSite(String id) {
return ref
.read(topSiteDatabaseProvider)
.topSiteDao
.generateOrderKeyBeforeSiteId(id)
.getSingle();
}
List<TopSiteItem> _getVisibleDefaults({
required Set<String> persistedUrls,
required Set<String> hiddenUrls,
}) {
return defaultTopSites
.where((seed) {
final normalized = Uri.parse(seed.url).normalized.toString();
return !persistedUrls.contains(normalized) &&
!hiddenUrls.contains(normalized);
})
.map(
(seed) => TopSiteItem(
title: seed.title,
url: Uri.parse(seed.url),
source: TopSiteSource.defaultSite,
),
)
.toList();
}
Future<List<TopSiteItem>> _getHistoryItems({
required int limit,
required Set<String> excludeUrls,
}) async {
final frecentSites = await ref
.read(historyRepositoryProvider.notifier)
.getTopFrecentSites(limit: limit + excludeUrls.length);
final items = <TopSiteItem>[];
for (final site in frecentSites) {
if (items.length >= limit) break;
final uri = Uri.tryParse(site.url);
if (uri == null) continue;
if (excludeUrls.contains(uri.normalized.toString())) continue;
final title = (site.title?.trim().isNotEmpty == true)
? site.title!.trim()
: uri.host;
items.add(
TopSiteItem(title: title, url: uri, source: TopSiteSource.history),
);
}
return items;
}
TopSiteItem _mapRow(TopSiteData row) {
return TopSiteItem(
id: row.id,
title: row.title,
url: row.url,
source: row.source == StoredTopSiteSource.pinned
? TopSiteSource.pinned
: TopSiteSource.defaultSite,
orderKey: row.orderKey,
createdAt: row.createdAt,
);
}
@override
void build() {}
}
@@ -0,0 +1,62 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'top_site_repository.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(TopSiteRepository)
final topSiteRepositoryProvider = TopSiteRepositoryProvider._();
final class TopSiteRepositoryProvider
extends $NotifierProvider<TopSiteRepository, void> {
TopSiteRepositoryProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'topSiteRepositoryProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$topSiteRepositoryHash();
@$internal
@override
TopSiteRepository create() => TopSiteRepository();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(void value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<void>(value),
);
}
}
String _$topSiteRepositoryHash() => r'43c0495dfb3044dc9bb2f420524b45afb5735a0b';
abstract class _$TopSiteRepository extends $Notifier<void> {
void build();
@$mustCallSuper
@override
void runBuild() {
final ref = this.ref as $Ref<void, void>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<void, void>,
void,
Object?,
Object?
>;
element.handleCreate(ref, build);
}
}