diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/browser_menu_sheet.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/browser_menu_sheet.dart index 39093fde..1456de07 100644 --- a/app/lib/features/geckoview/features/browser/presentation/widgets/browser_menu_sheet.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/browser_menu_sheet.dart @@ -590,7 +590,7 @@ class _PinTopSiteTile extends HookConsumerWidget { () => url != null ? ref .read(topSiteRepositoryProvider.notifier) - .isPersistedTopSiteUrl(url) + .isPinnedTopSiteUrl(url) : Future.value(false), [url], ); diff --git a/app/lib/features/geckoview/features/search/presentation/widgets/empty_state/top_sites_section.dart b/app/lib/features/geckoview/features/search/presentation/widgets/empty_state/top_sites_section.dart index 5ac0c380..34e73dfe 100644 --- a/app/lib/features/geckoview/features/search/presentation/widgets/empty_state/top_sites_section.dart +++ b/app/lib/features/geckoview/features/search/presentation/widgets/empty_state/top_sites_section.dart @@ -19,6 +19,8 @@ */ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:flutter_material_design_icons/flutter_material_design_icons.dart' + show MdiIcons; import 'package:flutter_reorderable_grid_view/widgets/custom_draggable.dart'; import 'package:flutter_reorderable_grid_view/widgets/reorderable_builder.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -221,8 +223,7 @@ class _ReorderableTopSitesGrid extends HookConsumerWidget { if (newIndex < 0 || newIndex > localItems.value.length) return; final items = localItems.value.toList(); - final movedItem = items.removeAt(oldIndex); - if (movedItem.id == null) return; + var movedItem = items.removeAt(oldIndex); final targetIndex = newIndex.clamp(0, items.length); @@ -234,6 +235,41 @@ class _ReorderableTopSitesGrid extends HookConsumerWidget { try { final repo = ref.read(topSiteRepositoryProvider.notifier); + // Auto-persist default sites that don't have an ID yet + if (movedItem.id == null && movedItem.isDefault) { + final id = await repo.ensureDefaultPersisted( + title: movedItem.title, + url: movedItem.url, + ); + movedItem = TopSiteItem( + id: id, + title: movedItem.title, + url: movedItem.url, + source: movedItem.source, + orderKey: movedItem.orderKey, + createdAt: movedItem.createdAt, + ); + } + if (movedItem.id == null) return; + + // Ensure neighbors are persisted too + for (var i = 0; i < items.length; i++) { + if (items[i].id == null && items[i].isDefault) { + final id = await repo.ensureDefaultPersisted( + title: items[i].title, + url: items[i].url, + ); + items[i] = TopSiteItem( + id: id, + title: items[i].title, + url: items[i].url, + source: items[i].source, + orderKey: items[i].orderKey, + createdAt: items[i].createdAt, + ); + } + } + final String key; if (targetIndex <= 0) { key = await repo.getLeadingOrderKey(); @@ -483,10 +519,31 @@ class _TopSiteGridTileState extends State<_TopSiteGridTile> { ), ), ), + if (widget.item.source == TopSiteSource.defaultSite) + Positioned( + top: 4, + right: 4, + child: DecoratedBox( + decoration: BoxDecoration( + color: colorScheme.primaryContainer, + borderRadius: const BorderRadius.all( + Radius.circular(10.0), + ), + ), + child: Padding( + padding: const EdgeInsets.all(3.0), + child: Icon( + MdiIcons.crown, + size: 12, + color: colorScheme.onPrimaryContainer, + ), + ), + ), + ), if (widget.showDragHandle) Positioned( top: 2, - right: 2, + left: 2, child: Icon( Icons.drag_indicator, size: 16, @@ -525,8 +582,6 @@ Future _editItem( WidgetRef ref, TopSiteItem item, ) async { - if (item.id == null) return; - final result = await showEditTopSiteDialog( context, initialTitle: item.title, @@ -536,13 +591,25 @@ Future _editItem( if (result == null || !context.mounted) return; try { - await ref - .read(topSiteRepositoryProvider.notifier) - .updatePersistedSite( - id: item.id!, - title: result.title, - url: result.url, - ); + final repo = ref.read(topSiteRepositoryProvider.notifier); + + // If the item has no ID (unpersisted default), persist it first + var id = item.id; + if (id == null && item.isDefault) { + id = await repo.ensureDefaultPersisted( + title: item.title, + url: item.url, + ); + } + if (id == null) return; + + // If the URL changed, hide the original so it doesn't reappear + // from the const defaults list. + if (item.url != result.url) { + await repo.hideDefaultSite(item.url); + } + + await repo.updateSite(id: id, title: result.title, url: result.url); if (context.mounted) { ui_helper.showInfoMessage(context, 'Top site updated'); } @@ -558,12 +625,15 @@ Future _removeItem( WidgetRef ref, TopSiteItem item, ) async { - if (item.id == null) return; + final repo = ref.read(topSiteRepositoryProvider.notifier); try { - await ref - .read(topSiteRepositoryProvider.notifier) - .removePersistedSite(item.id!); + if (item.id != null) { + await repo.removeSite(item.id!); + } + // Hide the URL so it doesn't reappear from the const defaults list + await repo.hideDefaultSite(item.url); + if (context.mounted) { ui_helper.showInfoMessage( context, @@ -572,9 +642,7 @@ Future _removeItem( label: 'Undo', onPressed: () async { try { - await ref - .read(topSiteRepositoryProvider.notifier) - .addPinnedSite(title: item.title, url: item.url); + await repo.addPinnedSite(title: item.title, url: item.url); } catch (_) {} }, ), diff --git a/app/lib/features/geckoview/features/top_sites/data/database/daos/seed_state.dart b/app/lib/features/geckoview/features/top_sites/data/database/daos/hidden_top_site.dart similarity index 55% rename from app/lib/features/geckoview/features/top_sites/data/database/daos/seed_state.dart rename to app/lib/features/geckoview/features/top_sites/data/database/daos/hidden_top_site.dart index 0bc2c977..0aefb60d 100644 --- a/app/lib/features/geckoview/features/top_sites/data/database/daos/seed_state.dart +++ b/app/lib/features/geckoview/features/top_sites/data/database/daos/hidden_top_site.dart @@ -18,29 +18,37 @@ * along with this program. If not, see . */ import 'package:drift/drift.dart'; -import 'package:weblibre/features/geckoview/features/top_sites/data/database/daos/seed_state.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 TopSiteSeedStateDao extends DatabaseAccessor - with $TopSiteSeedStateDaoMixin { - TopSiteSeedStateDao(super.db); +class HiddenTopSiteDao extends DatabaseAccessor + with $HiddenTopSiteDaoMixin { + HiddenTopSiteDao(super.db); - Future hasSeed(String seedId) async { - final row = - await (db.topSiteSeedState.select() - ..where((t) => t.seedId.equals(seedId))) - .getSingleOrNull(); - return row != null; + Future> getHiddenUrls() async { + final rows = await db.hiddenTopSite.select().get(); + return rows.map((r) => r.url.normalized.toString()).toSet(); } - Future markSeedApplied(String seedId) { - return db.topSiteSeedState.insertOne( - TopSiteSeedStateCompanion.insert( - seedId: seedId, - appliedAt: DateTime.now(), - ), + Stream> watchHiddenUrls() { + return db.hiddenTopSite.select().watch().map( + (rows) => rows.map((r) => r.url.normalized.toString()).toSet(), ); } + + Future hideUrl(Uri url) { + return db.hiddenTopSite.insertOne( + HiddenTopSiteCompanion.insert(url: url.normalized), + mode: InsertMode.insertOrIgnore, + ); + } + + Future unhideUrl(Uri url) { + return (db.hiddenTopSite.delete() + ..where((t) => t.url.equalsValue(url.normalized))) + .go(); + } } diff --git a/app/lib/features/geckoview/features/top_sites/data/database/daos/hidden_top_site.drift.dart b/app/lib/features/geckoview/features/top_sites/data/database/daos/hidden_top_site.drift.dart new file mode 100644 index 00000000..897f3918 --- /dev/null +++ b/app/lib/features/geckoview/features/top_sites/data/database/daos/hidden_top_site.drift.dart @@ -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 { + HiddenTopSiteDaoManager get managers => HiddenTopSiteDaoManager(this); +} + +class HiddenTopSiteDaoManager { + final $HiddenTopSiteDaoMixin _db; + HiddenTopSiteDaoManager(this._db); +} diff --git a/app/lib/features/geckoview/features/top_sites/data/database/daos/seed_state.drift.dart b/app/lib/features/geckoview/features/top_sites/data/database/daos/seed_state.drift.dart deleted file mode 100644 index facb8658..00000000 --- a/app/lib/features/geckoview/features/top_sites/data/database/daos/seed_state.drift.dart +++ /dev/null @@ -1,14 +0,0 @@ -// 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 $TopSiteSeedStateDaoMixin on i0.DatabaseAccessor { - TopSiteSeedStateDaoManager get managers => TopSiteSeedStateDaoManager(this); -} - -class TopSiteSeedStateDaoManager { - final $TopSiteSeedStateDaoMixin _db; - TopSiteSeedStateDaoManager(this._db); -} diff --git a/app/lib/features/geckoview/features/top_sites/data/database/daos/top_site.dart b/app/lib/features/geckoview/features/top_sites/data/database/daos/top_site.dart index aa7d36ce..c8a1bcf7 100644 --- a/app/lib/features/geckoview/features/top_sites/data/database/daos/top_site.dart +++ b/app/lib/features/geckoview/features/top_sites/data/database/daos/top_site.dart @@ -29,36 +29,30 @@ class TopSiteDao extends DatabaseAccessor with $TopSiteDaoMixin { TopSiteDao(super.db); - Selectable selectPersistedTopSites() { + Selectable selectAllTopSites() { return db.topSite.select()..orderBy([(t) => OrderingTerm.asc(t.orderKey)]); } - Future> getPersistedTopSites() { - return selectPersistedTopSites().get(); + Future> getAllTopSites() { + return selectAllTopSites().get(); } - Future getPersistedTopSiteById(String id) { + Future getTopSiteById(String id) { return (db.topSite.select()..where((t) => t.id.equals(id))) .getSingleOrNull(); } - Future getPersistedTopSiteByUrl(Uri url) { + Future getTopSiteByUrl(Uri url) { return (db.topSite.select() ..where((t) => t.url.equalsValue(url.normalized))) .getSingleOrNull(); } - Future countPersistedSites() async { - final count = db.topSite.id.count(); - final query = db.selectOnly(db.topSite)..addColumns([count]); - final result = await query.getSingle(); - return result.read(count)!; - } - - Future insertPinnedSite({ + Future insertSite({ required String id, required String title, required Uri url, + required StoredTopSiteSource source, required String orderKey, }) { return db.topSite.insertOne( @@ -66,20 +60,14 @@ class TopSiteDao extends DatabaseAccessor id: id, title: title, url: url.normalized, - source: StoredTopSiteSource.pinned, + source: source, orderKey: orderKey, createdAt: DateTime.now(), ), ); } - Future insertSeededSites(List rows) { - return db.batch((batch) { - batch.insertAll(db.topSite, rows); - }); - } - - Future updatePersistedSite( + Future updateSite( String id, { required String title, required Uri url, @@ -89,7 +77,7 @@ class TopSiteDao extends DatabaseAccessor ); } - Future deletePersistedSite(String id) { + Future deleteSite(String id) { return (db.topSite.delete()..where((t) => t.id.equals(id))).go(); } diff --git a/app/lib/features/geckoview/features/top_sites/data/database/database.dart b/app/lib/features/geckoview/features/top_sites/data/database/database.dart index b661d9ae..9d5992b7 100644 --- a/app/lib/features/geckoview/features/top_sites/data/database/database.dart +++ b/app/lib/features/geckoview/features/top_sites/data/database/database.dart @@ -20,13 +20,13 @@ 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/seed_state.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, TopSiteSeedStateDao], + daos: [TopSiteDao, HiddenTopSiteDao], ) class TopSiteDatabase extends $TopSiteDatabase { @override diff --git a/app/lib/features/geckoview/features/top_sites/data/database/database.drift.dart b/app/lib/features/geckoview/features/top_sites/data/database/database.drift.dart index 32635089..1705cbf6 100644 --- a/app/lib/features/geckoview/features/top_sites/data/database/database.drift.dart +++ b/app/lib/features/geckoview/features/top_sites/data/database/database.drift.dart @@ -7,7 +7,7 @@ import 'package:weblibre/features/geckoview/features/top_sites/data/database/dao 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/seed_state.dart' +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; @@ -16,12 +16,13 @@ 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.TopSiteSeedState topSiteSeedState = i1.TopSiteSeedState(this); + late final i1.HiddenTopSite hiddenTopSite = i1.HiddenTopSite(this); late final i2.TopSiteDao topSiteDao = i2.TopSiteDao( this as i3.TopSiteDatabase, ); - late final i4.TopSiteSeedStateDao topSiteSeedStateDao = - i4.TopSiteSeedStateDao(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.new); @@ -32,7 +33,7 @@ abstract class $TopSiteDatabase extends i0.GeneratedDatabase { List get allSchemaEntities => [ topSite, i1.idxTopSiteOrderKey, - topSiteSeedState, + hiddenTopSite, ]; } @@ -41,8 +42,8 @@ class $TopSiteDatabaseManager { $TopSiteDatabaseManager(this._db); i1.$TopSiteTableManager get topSite => i1.$TopSiteTableManager(_db, _db.topSite); - i1.$TopSiteSeedStateTableManager get topSiteSeedState => - i1.$TopSiteSeedStateTableManager(_db, _db.topSiteSeedState); + i1.$HiddenTopSiteTableManager get hiddenTopSite => + i1.$HiddenTopSiteTableManager(_db, _db.hiddenTopSite); } extension DefineFunctions on i6.CommonDatabase { diff --git a/app/lib/features/geckoview/features/top_sites/data/database/definitions.drift b/app/lib/features/geckoview/features/top_sites/data/database/definitions.drift index ab1ebe9a..f98320b4 100644 --- a/app/lib/features/geckoview/features/top_sites/data/database/definitions.drift +++ b/app/lib/features/geckoview/features/top_sites/data/database/definitions.drift @@ -13,9 +13,8 @@ CREATE TABLE top_site ( CREATE INDEX idx_top_site_order_key ON top_site(order_key); -CREATE TABLE top_site_seed_state ( - seed_id TEXT PRIMARY KEY NOT NULL, - applied_at DATETIME NOT NULL +CREATE TABLE hidden_top_site ( + url TEXT PRIMARY KEY NOT NULL MAPPED BY `const UriConverter()` ); leadingOrderKey(:bucket AS INTEGER): diff --git a/app/lib/features/geckoview/features/top_sites/data/database/definitions.drift.dart b/app/lib/features/geckoview/features/top_sites/data/database/definitions.drift.dart index 75f8cfb0..4f211208 100644 --- a/app/lib/features/geckoview/features/top_sites/data/database/definitions.drift.dart +++ b/app/lib/features/geckoview/features/top_sites/data/database/definitions.drift.dart @@ -234,130 +234,101 @@ typedef $TopSiteProcessedTableManager = i1.TopSiteData, i0.PrefetchHooks Function() >; -typedef $TopSiteSeedStateCreateCompanionBuilder = - i1.TopSiteSeedStateCompanion Function({ - required String seedId, - required DateTime appliedAt, - i0.Value rowid, - }); -typedef $TopSiteSeedStateUpdateCompanionBuilder = - i1.TopSiteSeedStateCompanion Function({ - i0.Value seedId, - i0.Value appliedAt, +typedef $HiddenTopSiteCreateCompanionBuilder = + i1.HiddenTopSiteCompanion Function({required Uri url, i0.Value rowid}); +typedef $HiddenTopSiteUpdateCompanionBuilder = + i1.HiddenTopSiteCompanion Function({ + i0.Value url, i0.Value rowid, }); -class $TopSiteSeedStateFilterComposer - extends i0.Composer { - $TopSiteSeedStateFilterComposer({ +class $HiddenTopSiteFilterComposer + extends i0.Composer { + $HiddenTopSiteFilterComposer({ required super.$db, required super.$table, super.joinBuilder, super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - i0.ColumnFilters get seedId => $composableBuilder( - column: $table.seedId, - builder: (column) => i0.ColumnFilters(column), - ); - - i0.ColumnFilters get appliedAt => $composableBuilder( - column: $table.appliedAt, - builder: (column) => i0.ColumnFilters(column), - ); + i0.ColumnWithTypeConverterFilters get url => + $composableBuilder( + column: $table.url, + builder: (column) => i0.ColumnWithTypeConverterFilters(column), + ); } -class $TopSiteSeedStateOrderingComposer - extends i0.Composer { - $TopSiteSeedStateOrderingComposer({ +class $HiddenTopSiteOrderingComposer + extends i0.Composer { + $HiddenTopSiteOrderingComposer({ required super.$db, required super.$table, super.joinBuilder, super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - i0.ColumnOrderings get seedId => $composableBuilder( - column: $table.seedId, - builder: (column) => i0.ColumnOrderings(column), - ); - - i0.ColumnOrderings get appliedAt => $composableBuilder( - column: $table.appliedAt, + i0.ColumnOrderings get url => $composableBuilder( + column: $table.url, builder: (column) => i0.ColumnOrderings(column), ); } -class $TopSiteSeedStateAnnotationComposer - extends i0.Composer { - $TopSiteSeedStateAnnotationComposer({ +class $HiddenTopSiteAnnotationComposer + extends i0.Composer { + $HiddenTopSiteAnnotationComposer({ required super.$db, required super.$table, super.joinBuilder, super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - i0.GeneratedColumn get seedId => - $composableBuilder(column: $table.seedId, builder: (column) => column); - - i0.GeneratedColumn get appliedAt => - $composableBuilder(column: $table.appliedAt, builder: (column) => column); + i0.GeneratedColumnWithTypeConverter get url => + $composableBuilder(column: $table.url, builder: (column) => column); } -class $TopSiteSeedStateTableManager +class $HiddenTopSiteTableManager extends i0.RootTableManager< i0.GeneratedDatabase, - i1.TopSiteSeedState, - i1.TopSiteSeedStateData, - i1.$TopSiteSeedStateFilterComposer, - i1.$TopSiteSeedStateOrderingComposer, - i1.$TopSiteSeedStateAnnotationComposer, - $TopSiteSeedStateCreateCompanionBuilder, - $TopSiteSeedStateUpdateCompanionBuilder, + i1.HiddenTopSite, + i1.HiddenTopSiteData, + i1.$HiddenTopSiteFilterComposer, + i1.$HiddenTopSiteOrderingComposer, + i1.$HiddenTopSiteAnnotationComposer, + $HiddenTopSiteCreateCompanionBuilder, + $HiddenTopSiteUpdateCompanionBuilder, ( - i1.TopSiteSeedStateData, + i1.HiddenTopSiteData, i0.BaseReferences< i0.GeneratedDatabase, - i1.TopSiteSeedState, - i1.TopSiteSeedStateData + i1.HiddenTopSite, + i1.HiddenTopSiteData >, ), - i1.TopSiteSeedStateData, + i1.HiddenTopSiteData, i0.PrefetchHooks Function() > { - $TopSiteSeedStateTableManager( - i0.GeneratedDatabase db, - i1.TopSiteSeedState table, - ) : super( + $HiddenTopSiteTableManager(i0.GeneratedDatabase db, i1.HiddenTopSite table) + : super( i0.TableManagerState( db: db, table: table, createFilteringComposer: () => - i1.$TopSiteSeedStateFilterComposer($db: db, $table: table), + i1.$HiddenTopSiteFilterComposer($db: db, $table: table), createOrderingComposer: () => - i1.$TopSiteSeedStateOrderingComposer($db: db, $table: table), + i1.$HiddenTopSiteOrderingComposer($db: db, $table: table), createComputedFieldComposer: () => - i1.$TopSiteSeedStateAnnotationComposer($db: db, $table: table), + i1.$HiddenTopSiteAnnotationComposer($db: db, $table: table), updateCompanionCallback: ({ - i0.Value seedId = const i0.Value.absent(), - i0.Value appliedAt = const i0.Value.absent(), + i0.Value url = const i0.Value.absent(), i0.Value rowid = const i0.Value.absent(), - }) => i1.TopSiteSeedStateCompanion( - seedId: seedId, - appliedAt: appliedAt, - rowid: rowid, - ), + }) => i1.HiddenTopSiteCompanion(url: url, rowid: rowid), createCompanionCallback: ({ - required String seedId, - required DateTime appliedAt, + required Uri url, i0.Value rowid = const i0.Value.absent(), - }) => i1.TopSiteSeedStateCompanion.insert( - seedId: seedId, - appliedAt: appliedAt, - rowid: rowid, - ), + }) => i1.HiddenTopSiteCompanion.insert(url: url, rowid: rowid), withReferenceMapper: (p0) => p0 .map((e) => (e.readTable(table), i0.BaseReferences(db, table, e))) .toList(), @@ -366,25 +337,25 @@ class $TopSiteSeedStateTableManager ); } -typedef $TopSiteSeedStateProcessedTableManager = +typedef $HiddenTopSiteProcessedTableManager = i0.ProcessedTableManager< i0.GeneratedDatabase, - i1.TopSiteSeedState, - i1.TopSiteSeedStateData, - i1.$TopSiteSeedStateFilterComposer, - i1.$TopSiteSeedStateOrderingComposer, - i1.$TopSiteSeedStateAnnotationComposer, - $TopSiteSeedStateCreateCompanionBuilder, - $TopSiteSeedStateUpdateCompanionBuilder, + i1.HiddenTopSite, + i1.HiddenTopSiteData, + i1.$HiddenTopSiteFilterComposer, + i1.$HiddenTopSiteOrderingComposer, + i1.$HiddenTopSiteAnnotationComposer, + $HiddenTopSiteCreateCompanionBuilder, + $HiddenTopSiteUpdateCompanionBuilder, ( - i1.TopSiteSeedStateData, + i1.HiddenTopSiteData, i0.BaseReferences< i0.GeneratedDatabase, - i1.TopSiteSeedState, - i1.TopSiteSeedStateData + i1.HiddenTopSite, + i1.HiddenTopSiteData >, ), - i1.TopSiteSeedStateData, + i1.HiddenTopSiteData, i0.PrefetchHooks Function() >; @@ -755,164 +726,130 @@ i0.Index get idxTopSiteOrderKey => i0.Index( 'CREATE INDEX idx_top_site_order_key ON top_site (order_key)', ); -class TopSiteSeedState extends i0.Table - with i0.TableInfo { +class HiddenTopSite extends i0.Table + with i0.TableInfo { @override final i0.GeneratedDatabase attachedDatabase; final String? _alias; - TopSiteSeedState(this.attachedDatabase, [this._alias]); - late final i0.GeneratedColumn seedId = i0.GeneratedColumn( - 'seed_id', - aliasedName, - false, - type: i0.DriftSqlType.string, - requiredDuringInsert: true, - $customConstraints: 'PRIMARY KEY NOT NULL', - ); - late final i0.GeneratedColumn appliedAt = - i0.GeneratedColumn( - 'applied_at', + HiddenTopSite(this.attachedDatabase, [this._alias]); + late final i0.GeneratedColumnWithTypeConverter url = + i0.GeneratedColumn( + 'url', aliasedName, false, - type: i0.DriftSqlType.dateTime, + type: i0.DriftSqlType.string, requiredDuringInsert: true, - $customConstraints: 'NOT NULL', - ); + $customConstraints: 'PRIMARY KEY NOT NULL', + ).withConverter(i1.HiddenTopSite.$converterurl); @override - List get $columns => [seedId, appliedAt]; + List get $columns => [url]; @override String get aliasedName => _alias ?? actualTableName; @override String get actualTableName => $name; - static const String $name = 'top_site_seed_state'; + static const String $name = 'hidden_top_site'; @override - Set get $primaryKey => {seedId}; + Set get $primaryKey => {url}; @override - i1.TopSiteSeedStateData map( - Map data, { - String? tablePrefix, - }) { + i1.HiddenTopSiteData map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return i1.TopSiteSeedStateData( - seedId: attachedDatabase.typeMapping.read( - i0.DriftSqlType.string, - data['${effectivePrefix}seed_id'], - )!, - appliedAt: attachedDatabase.typeMapping.read( - i0.DriftSqlType.dateTime, - data['${effectivePrefix}applied_at'], - )!, + return i1.HiddenTopSiteData( + url: i1.HiddenTopSite.$converterurl.fromSql( + attachedDatabase.typeMapping.read( + i0.DriftSqlType.string, + data['${effectivePrefix}url'], + )!, + ), ); } @override - TopSiteSeedState createAlias(String alias) { - return TopSiteSeedState(attachedDatabase, alias); + HiddenTopSite createAlias(String alias) { + return HiddenTopSite(attachedDatabase, alias); } + static i0.TypeConverter $converterurl = const i3.UriConverter(); @override bool get dontWriteConstraints => true; } -class TopSiteSeedStateData extends i0.DataClass - implements i0.Insertable { - final String seedId; - final DateTime appliedAt; - const TopSiteSeedStateData({required this.seedId, required this.appliedAt}); +class HiddenTopSiteData extends i0.DataClass + implements i0.Insertable { + final Uri url; + const HiddenTopSiteData({required this.url}); @override Map toColumns(bool nullToAbsent) { final map = {}; - map['seed_id'] = i0.Variable(seedId); - map['applied_at'] = i0.Variable(appliedAt); + { + map['url'] = i0.Variable( + i1.HiddenTopSite.$converterurl.toSql(url), + ); + } return map; } - factory TopSiteSeedStateData.fromJson( + factory HiddenTopSiteData.fromJson( Map json, { i0.ValueSerializer? serializer, }) { serializer ??= i0.driftRuntimeOptions.defaultSerializer; - return TopSiteSeedStateData( - seedId: serializer.fromJson(json['seed_id']), - appliedAt: serializer.fromJson(json['applied_at']), - ); + return HiddenTopSiteData(url: serializer.fromJson(json['url'])); } @override Map toJson({i0.ValueSerializer? serializer}) { serializer ??= i0.driftRuntimeOptions.defaultSerializer; - return { - 'seed_id': serializer.toJson(seedId), - 'applied_at': serializer.toJson(appliedAt), - }; + return {'url': serializer.toJson(url)}; } - i1.TopSiteSeedStateData copyWith({String? seedId, DateTime? appliedAt}) => - i1.TopSiteSeedStateData( - seedId: seedId ?? this.seedId, - appliedAt: appliedAt ?? this.appliedAt, - ); - TopSiteSeedStateData copyWithCompanion(i1.TopSiteSeedStateCompanion data) { - return TopSiteSeedStateData( - seedId: data.seedId.present ? data.seedId.value : this.seedId, - appliedAt: data.appliedAt.present ? data.appliedAt.value : this.appliedAt, - ); + 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('TopSiteSeedStateData(') - ..write('seedId: $seedId, ') - ..write('appliedAt: $appliedAt') + return (StringBuffer('HiddenTopSiteData(') + ..write('url: $url') ..write(')')) .toString(); } @override - int get hashCode => Object.hash(seedId, appliedAt); + int get hashCode => url.hashCode; @override bool operator ==(Object other) => identical(this, other) || - (other is i1.TopSiteSeedStateData && - other.seedId == this.seedId && - other.appliedAt == this.appliedAt); + (other is i1.HiddenTopSiteData && other.url == this.url); } -class TopSiteSeedStateCompanion - extends i0.UpdateCompanion { - final i0.Value seedId; - final i0.Value appliedAt; +class HiddenTopSiteCompanion extends i0.UpdateCompanion { + final i0.Value url; final i0.Value rowid; - const TopSiteSeedStateCompanion({ - this.seedId = const i0.Value.absent(), - this.appliedAt = const i0.Value.absent(), + const HiddenTopSiteCompanion({ + this.url = const i0.Value.absent(), this.rowid = const i0.Value.absent(), }); - TopSiteSeedStateCompanion.insert({ - required String seedId, - required DateTime appliedAt, + HiddenTopSiteCompanion.insert({ + required Uri url, this.rowid = const i0.Value.absent(), - }) : seedId = i0.Value(seedId), - appliedAt = i0.Value(appliedAt); - static i0.Insertable custom({ - i0.Expression? seedId, - i0.Expression? appliedAt, + }) : url = i0.Value(url); + static i0.Insertable custom({ + i0.Expression? url, i0.Expression? rowid, }) { return i0.RawValuesInsertable({ - if (seedId != null) 'seed_id': seedId, - if (appliedAt != null) 'applied_at': appliedAt, + if (url != null) 'url': url, if (rowid != null) 'rowid': rowid, }); } - i1.TopSiteSeedStateCompanion copyWith({ - i0.Value? seedId, - i0.Value? appliedAt, + i1.HiddenTopSiteCompanion copyWith({ + i0.Value? url, i0.Value? rowid, }) { - return i1.TopSiteSeedStateCompanion( - seedId: seedId ?? this.seedId, - appliedAt: appliedAt ?? this.appliedAt, + return i1.HiddenTopSiteCompanion( + url: url ?? this.url, rowid: rowid ?? this.rowid, ); } @@ -920,11 +857,10 @@ class TopSiteSeedStateCompanion @override Map toColumns(bool nullToAbsent) { final map = {}; - if (seedId.present) { - map['seed_id'] = i0.Variable(seedId.value); - } - if (appliedAt.present) { - map['applied_at'] = i0.Variable(appliedAt.value); + if (url.present) { + map['url'] = i0.Variable( + i1.HiddenTopSite.$converterurl.toSql(url.value), + ); } if (rowid.present) { map['rowid'] = i0.Variable(rowid.value); @@ -934,9 +870,8 @@ class TopSiteSeedStateCompanion @override String toString() { - return (StringBuffer('TopSiteSeedStateCompanion(') - ..write('seedId: $seedId, ') - ..write('appliedAt: $appliedAt, ') + return (StringBuffer('HiddenTopSiteCompanion(') + ..write('url: $url, ') ..write('rowid: $rowid') ..write(')')) .toString(); diff --git a/app/lib/features/geckoview/features/top_sites/data/entities/stored_top_site_source.dart b/app/lib/features/geckoview/features/top_sites/data/entities/stored_top_site_source.dart index 61f65edd..0fc04d4e 100644 --- a/app/lib/features/geckoview/features/top_sites/data/entities/stored_top_site_source.dart +++ b/app/lib/features/geckoview/features/top_sites/data/entities/stored_top_site_source.dart @@ -17,4 +17,4 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -enum StoredTopSiteSource { seeded, pinned } +enum StoredTopSiteSource { defaultSite, pinned } diff --git a/app/lib/features/geckoview/features/top_sites/domain/entities/top_site_item.dart b/app/lib/features/geckoview/features/top_sites/domain/entities/top_site_item.dart index 64627176..632c9040 100644 --- a/app/lib/features/geckoview/features/top_sites/domain/entities/top_site_item.dart +++ b/app/lib/features/geckoview/features/top_sites/domain/entities/top_site_item.dart @@ -43,8 +43,13 @@ class TopSiteItem with FastEquatable { this.historyPlaceId, }); - bool get isPersisted => - source == TopSiteSource.seeded || source == TopSiteSource.pinned; + 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; diff --git a/app/lib/features/geckoview/features/top_sites/domain/entities/top_site_source.dart b/app/lib/features/geckoview/features/top_sites/domain/entities/top_site_source.dart index 974a6bdf..05774d86 100644 --- a/app/lib/features/geckoview/features/top_sites/domain/entities/top_site_source.dart +++ b/app/lib/features/geckoview/features/top_sites/domain/entities/top_site_source.dart @@ -17,4 +17,4 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -enum TopSiteSource { seeded, pinned, history } +enum TopSiteSource { defaultSite, pinned, history } diff --git a/app/lib/features/geckoview/features/top_sites/domain/providers.dart b/app/lib/features/geckoview/features/top_sites/domain/providers.dart index 12486b03..1518c2a7 100644 --- a/app/lib/features/geckoview/features/top_sites/domain/providers.dart +++ b/app/lib/features/geckoview/features/top_sites/domain/providers.dart @@ -23,14 +23,20 @@ import 'package:weblibre/features/geckoview/features/top_sites/domain/repositori part 'providers.g.dart'; -@Riverpod(keepAlive: true) -List<({String title, Uri url})> topSiteDefaultSeeds(Ref ref) { - return [ - (title: 'Wikipedia', url: Uri.parse('https://wikipedia.org')), - (title: 'OpenStreetMap', url: Uri.parse('https://www.openstreetmap.org')), - (title: 'Project Gutenberg', url: Uri.parse('https://www.gutenberg.org/')), - ]; -} +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> topSiteList(Ref ref, {int limit = 8}) { @@ -40,6 +46,6 @@ Stream> topSiteList(Ref ref, {int limit = 8}) { } @Riverpod() -Stream> persistedTopSiteList(Ref ref) { - return ref.watch(topSiteRepositoryProvider.notifier).watchPersistedTopSites(); +Stream> pinnedTopSiteList(Ref ref) { + return ref.watch(topSiteRepositoryProvider.notifier).watchPinnedTopSites(); } diff --git a/app/lib/features/geckoview/features/top_sites/domain/providers.g.dart b/app/lib/features/geckoview/features/top_sites/domain/providers.g.dart index dced47f5..8ca696fe 100644 --- a/app/lib/features/geckoview/features/top_sites/domain/providers.g.dart +++ b/app/lib/features/geckoview/features/top_sites/domain/providers.g.dart @@ -9,56 +9,6 @@ part of 'providers.dart'; // GENERATED CODE - DO NOT MODIFY BY HAND // ignore_for_file: type=lint, type=warning -@ProviderFor(topSiteDefaultSeeds) -final topSiteDefaultSeedsProvider = TopSiteDefaultSeedsProvider._(); - -final class TopSiteDefaultSeedsProvider - extends - $FunctionalProvider< - List<({String title, Uri url})>, - List<({String title, Uri url})>, - List<({String title, Uri url})> - > - with $Provider> { - TopSiteDefaultSeedsProvider._() - : super( - from: null, - argument: null, - retry: null, - name: r'topSiteDefaultSeedsProvider', - isAutoDispose: false, - dependencies: null, - $allTransitiveDependencies: null, - ); - - @override - String debugGetCreateSourceHash() => _$topSiteDefaultSeedsHash(); - - @$internal - @override - $ProviderElement> $createElement( - $ProviderPointer pointer, - ) => $ProviderElement(pointer); - - @override - List<({String title, Uri url})> create(Ref ref) { - return topSiteDefaultSeeds(ref); - } - - /// {@macro riverpod.override_with_value} - Override overrideWithValue(List<({String title, Uri url})> value) { - return $ProviderOverride( - origin: this, - providerOverride: $SyncValueProvider>( - value, - ), - ); - } -} - -String _$topSiteDefaultSeedsHash() => - r'd15156e1ebe1896a11dc2d983db9fb90a2222380'; - @ProviderFor(topSiteList) final topSiteListProvider = TopSiteListFamily._(); @@ -136,10 +86,10 @@ final class TopSiteListFamily extends $Family String toString() => r'topSiteListProvider'; } -@ProviderFor(persistedTopSiteList) -final persistedTopSiteListProvider = PersistedTopSiteListProvider._(); +@ProviderFor(pinnedTopSiteList) +final pinnedTopSiteListProvider = PinnedTopSiteListProvider._(); -final class PersistedTopSiteListProvider +final class PinnedTopSiteListProvider extends $FunctionalProvider< AsyncValue>, @@ -149,19 +99,19 @@ final class PersistedTopSiteListProvider with $FutureModifier>, $StreamProvider> { - PersistedTopSiteListProvider._() + PinnedTopSiteListProvider._() : super( from: null, argument: null, retry: null, - name: r'persistedTopSiteListProvider', + name: r'pinnedTopSiteListProvider', isAutoDispose: true, dependencies: null, $allTransitiveDependencies: null, ); @override - String debugGetCreateSourceHash() => _$persistedTopSiteListHash(); + String debugGetCreateSourceHash() => _$pinnedTopSiteListHash(); @$internal @override @@ -171,9 +121,8 @@ final class PersistedTopSiteListProvider @override Stream> create(Ref ref) { - return persistedTopSiteList(ref); + return pinnedTopSiteList(ref); } } -String _$persistedTopSiteListHash() => - r'f0f7f087dccc0cea8498811ea2ad2af5e6d3fbb8'; +String _$pinnedTopSiteListHash() => r'83adb6584ef738be14a2e0b55b33986ebb4520a9'; diff --git a/app/lib/features/geckoview/features/top_sites/domain/repositories/top_site_repository.dart b/app/lib/features/geckoview/features/top_sites/domain/repositories/top_site_repository.dart index 3e6360bf..0f6268c8 100644 --- a/app/lib/features/geckoview/features/top_sites/domain/repositories/top_site_repository.dart +++ b/app/lib/features/geckoview/features/top_sites/domain/repositories/top_site_repository.dart @@ -19,8 +19,8 @@ */ import 'dart:async'; -import 'package:drift/drift.dart'; 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'; @@ -36,92 +36,88 @@ part 'top_site_repository.g.dart'; @Riverpod(keepAlive: true) class TopSiteRepository extends _$TopSiteRepository { - Future ensureSeeded() async { + Stream> watchTopSites({int limit = 8}) { final db = ref.read(topSiteDatabaseProvider); - final seeds = ref.read(topSiteDefaultSeedsProvider); - await db.transaction(() async { - final alreadySeeded = await db.topSiteSeedStateDao.hasSeed( - 'initial-defaults-v1', + return CombineLatestStream.combine2( + db.topSiteDao.selectAllTopSites().watch(), + db.hiddenTopSiteDao.watchHiddenUrls(), + (List rows, Set 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, ); - if (alreadySeeded) return; - final now = DateTime.now(); + final combined = [...persistedItems, ...defaultItems]; + final targetCount = limit < 0 ? 0 : limit; - for (var i = 0; i < seeds.length; i++) { - final orderKey = await db.topSiteDao - .generateTrailingOrderKey() - .getSingle(); - // Insert one-by-one so trailing key advances - await db.topSite.insertOne( - TopSiteCompanion.insert( - id: uuid.v7(), - title: seeds[i].title, - url: seeds[i].url.normalized, - source: StoredTopSiteSource.seeded, - orderKey: orderKey, - createdAt: now, - ), - ); + if (combined.length >= targetCount) { + return combined; } - await db.topSiteSeedStateDao.markSeedApplied('initial-defaults-v1'); + 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> getTopSites({int limit = 8}) async { - final persisted = await _getPersistedItems(); - final targetCount = limit < 0 ? 0 : limit; + final db = ref.read(topSiteDatabaseProvider); + final rows = await db.topSiteDao.getAllTopSites(); + final hiddenUrls = await db.hiddenTopSiteDao.getHiddenUrls(); - // Always keep all persisted items. The limit is only used as a history - // padding target. - if (persisted.length >= targetCount) { - return persisted; - } + final persistedItems = rows.map(_mapRow).toList(); + final persistedUrls = persistedItems + .map((s) => s.url.normalized.toString()) + .toSet(); - final remaining = targetCount - persisted.length; - final historyItems = await _getHistoryItems( - limit: remaining, - excludeUrls: persisted.map((s) => s.url.normalized.toString()).toSet(), + final defaultItems = _getVisibleDefaults( + persistedUrls: persistedUrls, + hiddenUrls: hiddenUrls, ); - return [...persisted, ...historyItems]; + 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> watchTopSites({int limit = 8}) { + Stream> watchPinnedTopSites() { final db = ref.read(topSiteDatabaseProvider); - return db.topSiteDao.selectPersistedTopSites().watch().asyncMap(( - persistedRows, - ) async { - final persistedItems = persistedRows.map(_mapPersistedRow).toList(); - final targetCount = limit < 0 ? 0 : limit; - - // Always keep all persisted items. The limit is only used as a history - // padding target. - if (persistedItems.length >= targetCount) { - return persistedItems; - } - - final remaining = targetCount - persistedItems.length; - final historyItems = await _getHistoryItems( - limit: remaining, - excludeUrls: persistedItems - .map((s) => s.url.normalized.toString()) - .toSet(), - ); - - return [...persistedItems, ...historyItems]; - }); - } - - Future> getPersistedTopSites() { - return _getPersistedItems(); - } - - Stream> watchPersistedTopSites() { - final db = ref.read(topSiteDatabaseProvider); - return db.topSiteDao.selectPersistedTopSites().watch().map( - (rows) => rows.map(_mapPersistedRow).toList(), + return db.topSiteDao.selectAllTopSites().watch().map( + (rows) => rows + .where((r) => r.source == StoredTopSiteSource.pinned) + .map(_mapRow) + .toList(), ); } @@ -137,6 +133,39 @@ class TopSiteRepository extends _$TopSiteRepository { return normalized; } + /// Persists a default site to the database so it can be reordered. + /// Returns the database ID. + Future _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 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 addPinnedSite({ required String title, required Uri url, @@ -144,8 +173,11 @@ class TopSiteRepository extends _$TopSiteRepository { _validateUrl(url); final db = ref.read(topSiteDatabaseProvider); - // Check if URL already exists as a persisted site - final existing = await db.topSiteDao.getPersistedTopSiteByUrl(url); + // 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() @@ -161,16 +193,17 @@ class TopSiteRepository extends _$TopSiteRepository { final id = uuid.v7(); final orderKey = await db.topSiteDao.generateLeadingOrderKey().getSingle(); - await db.topSiteDao.insertPinnedSite( + await db.topSiteDao.insertSite( id: id, title: title, url: url, + source: StoredTopSiteSource.pinned, orderKey: orderKey, ); return id; } - Future updatePersistedSite({ + Future updateSite({ required String id, required String title, required Uri url, @@ -179,39 +212,47 @@ class TopSiteRepository extends _$TopSiteRepository { return ref .read(topSiteDatabaseProvider) .topSiteDao - .updatePersistedSite(id, title: title, url: url); + .updateSite(id, title: title, url: url); } - Future removePersistedSite(String id) { - return ref.read(topSiteDatabaseProvider).topSiteDao.deletePersistedSite(id); + Future removeSite(String id) { + return ref.read(topSiteDatabaseProvider).topSiteDao.deleteSite(id); } - Future getPersistedTopSiteByUrl(Uri url) async { + Future hideDefaultSite(Uri url) { + return ref.read(topSiteDatabaseProvider).hiddenTopSiteDao.hideUrl(url); + } + + Future isPinnedTopSiteUrl(Uri url) async { final row = await ref .read(topSiteDatabaseProvider) .topSiteDao - .getPersistedTopSiteByUrl(url); - return row != null ? _mapPersistedRow(row) : null; - } - - Future isPersistedTopSiteUrl(Uri url) async { - final row = await ref - .read(topSiteDatabaseProvider) - .topSiteDao - .getPersistedTopSiteByUrl(url); - return row != null; + .getTopSiteByUrl(url); + return row != null && row.source == StoredTopSiteSource.pinned; } Future unpinSiteByUrl(Uri url) async { - final row = await ref - .read(topSiteDatabaseProvider) - .topSiteDao - .getPersistedTopSiteByUrl(url); + final db = ref.read(topSiteDatabaseProvider); + final row = await db.topSiteDao.getTopSiteByUrl(url); if (row == null) return false; - await ref - .read(topSiteDatabaseProvider) - .topSiteDao - .deletePersistedSite(row.id); + + // 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; } @@ -254,12 +295,24 @@ class TopSiteRepository extends _$TopSiteRepository { .getSingle(); } - Future> _getPersistedItems() async { - final rows = await ref - .read(topSiteDatabaseProvider) - .topSiteDao - .getPersistedTopSites(); - return rows.map(_mapPersistedRow).toList(); + List _getVisibleDefaults({ + required Set persistedUrls, + required Set 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> _getHistoryItems({ @@ -291,21 +344,19 @@ class TopSiteRepository extends _$TopSiteRepository { return items; } - TopSiteItem _mapPersistedRow(TopSiteData row) { + TopSiteItem _mapRow(TopSiteData row) { return TopSiteItem( id: row.id, title: row.title, url: row.url, - source: row.source == StoredTopSiteSource.seeded - ? TopSiteSource.seeded - : TopSiteSource.pinned, + source: row.source == StoredTopSiteSource.pinned + ? TopSiteSource.pinned + : TopSiteSource.defaultSite, orderKey: row.orderKey, createdAt: row.createdAt, ); } @override - void build() { - unawaited(ensureSeeded()); - } + void build() {} } diff --git a/app/lib/features/geckoview/features/top_sites/domain/repositories/top_site_repository.g.dart b/app/lib/features/geckoview/features/top_sites/domain/repositories/top_site_repository.g.dart index ce34d1a4..164473ee 100644 --- a/app/lib/features/geckoview/features/top_sites/domain/repositories/top_site_repository.g.dart +++ b/app/lib/features/geckoview/features/top_sites/domain/repositories/top_site_repository.g.dart @@ -41,7 +41,7 @@ final class TopSiteRepositoryProvider } } -String _$topSiteRepositoryHash() => r'6e954c84ed5916ac0b5a9e251d43cbe1e9ed15c3'; +String _$topSiteRepositoryHash() => r'43c0495dfb3044dc9bb2f420524b45afb5735a0b'; abstract class _$TopSiteRepository extends $Notifier { void build();