feat: bookmark the site directory on a first start

This commit is contained in:
randogoth 2026-10-11 22:58:02 +03:00
parent 95602ac623
commit dcb70a93d1
14 changed files with 506 additions and 1 deletions

View file

@ -127,6 +127,16 @@ class ClientCertificates extends Table {
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
}
/// What a fresh install has already been given, so it is given once: what
/// the reader removes afterwards stays removed.
class FirstStart extends Table {
IntColumn get id => integer().withDefault(const Constant(0))();
BoolColumn get directorySeeded => boolean().withDefault(const Constant(false))();
@override
Set<Column> get primaryKey => {id};
}
/// Which certificate the reader presents to one host: a binding the reader
/// made on a certificate request, worn only there.
class SiteCertificates extends Table {
@ -149,6 +159,7 @@ class SiteCertificates extends Table {
KnownHosts,
ClientCertificates,
SiteCertificates,
FirstStart,
])
class MewsDatabase extends _$MewsDatabase {
MewsDatabase() : super(_open());
@ -157,7 +168,7 @@ class MewsDatabase extends _$MewsDatabase {
MewsDatabase.test(super.executor);
@override
int get schemaVersion => 5;
int get schemaVersion => 6;
@override
MigrationStrategy get migration => MigrationStrategy(
@ -183,6 +194,9 @@ class MewsDatabase extends _$MewsDatabase {
await m.createTable(clientCertificates);
await m.createTable(siteCertificates);
}
if (from < 6) {
await m.createTable(firstStart);
}
},
);
@ -309,6 +323,28 @@ class MewsDatabase extends _$MewsDatabase {
await _removeTagsOf(TagKind.bookmark, url);
});
/// Gives a fresh install the project's directory of sites as a bookmark,
/// so the reader starts somewhere. It happens once: removing it is the
/// reader's, and later starts never bring it back.
Future<void> seedDirectoryBookmark(String url, String title) async {
await transaction(() async {
if (await directorySeeded()) return;
await addBookmark(url, title);
await into(firstStart).insert(
FirstStartCompanion.insert(
id: const Value(0),
directorySeeded: const Value(true),
),
mode: InsertMode.insertOrReplace);
});
}
/// Whether the directory bookmark has been given to this install.
Future<bool> directorySeeded() async {
final row = await (select(firstStart)..where((r) => r.id.equals(0))).getSingleOrNull();
return row?.directorySeeded ?? false;
}
// Subscriptions ---------------------------------------------------------
Future<void> addSubscription(String url, String? title, String? atomHref) =>

View file

@ -3376,6 +3376,209 @@ class SiteCertificatesCompanion extends UpdateCompanion<SiteCertificate> {
}
}
class $FirstStartTable extends FirstStart
with TableInfo<$FirstStartTable, FirstStartData> {
@override
final GeneratedDatabase attachedDatabase;
final String? _alias;
$FirstStartTable(this.attachedDatabase, [this._alias]);
static const VerificationMeta _idMeta = const VerificationMeta('id');
@override
late final GeneratedColumn<int> id = GeneratedColumn<int>(
'id',
aliasedName,
false,
type: DriftSqlType.int,
requiredDuringInsert: false,
defaultValue: const Constant(0),
);
static const VerificationMeta _directorySeededMeta = const VerificationMeta(
'directorySeeded',
);
@override
late final GeneratedColumn<bool> directorySeeded = GeneratedColumn<bool>(
'directory_seeded',
aliasedName,
false,
type: DriftSqlType.bool,
requiredDuringInsert: false,
defaultConstraints: GeneratedColumn.constraintIsAlways(
'CHECK ("directory_seeded" IN (0, 1))',
),
defaultValue: const Constant(false),
);
@override
List<GeneratedColumn> get $columns => [id, directorySeeded];
@override
String get aliasedName => _alias ?? actualTableName;
@override
String get actualTableName => $name;
static const String $name = 'first_start';
@override
VerificationContext validateIntegrity(
Insertable<FirstStartData> instance, {
bool isInserting = false,
}) {
final context = VerificationContext();
final data = instance.toColumns(true);
if (data.containsKey('id')) {
context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta));
}
if (data.containsKey('directory_seeded')) {
context.handle(
_directorySeededMeta,
directorySeeded.isAcceptableOrUnknown(
data['directory_seeded']!,
_directorySeededMeta,
),
);
}
return context;
}
@override
Set<GeneratedColumn> get $primaryKey => {id};
@override
FirstStartData map(Map<String, dynamic> data, {String? tablePrefix}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
return FirstStartData(
id: attachedDatabase.typeMapping.read(
DriftSqlType.int,
data['${effectivePrefix}id'],
)!,
directorySeeded: attachedDatabase.typeMapping.read(
DriftSqlType.bool,
data['${effectivePrefix}directory_seeded'],
)!,
);
}
@override
$FirstStartTable createAlias(String alias) {
return $FirstStartTable(attachedDatabase, alias);
}
}
class FirstStartData extends DataClass implements Insertable<FirstStartData> {
final int id;
final bool directorySeeded;
const FirstStartData({required this.id, required this.directorySeeded});
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
map['id'] = Variable<int>(id);
map['directory_seeded'] = Variable<bool>(directorySeeded);
return map;
}
FirstStartCompanion toCompanion(bool nullToAbsent) {
return FirstStartCompanion(
id: Value(id),
directorySeeded: Value(directorySeeded),
);
}
factory FirstStartData.fromJson(
Map<String, dynamic> json, {
ValueSerializer? serializer,
}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return FirstStartData(
id: serializer.fromJson<int>(json['id']),
directorySeeded: serializer.fromJson<bool>(json['directorySeeded']),
);
}
@override
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'id': serializer.toJson<int>(id),
'directorySeeded': serializer.toJson<bool>(directorySeeded),
};
}
FirstStartData copyWith({int? id, bool? directorySeeded}) => FirstStartData(
id: id ?? this.id,
directorySeeded: directorySeeded ?? this.directorySeeded,
);
FirstStartData copyWithCompanion(FirstStartCompanion data) {
return FirstStartData(
id: data.id.present ? data.id.value : this.id,
directorySeeded: data.directorySeeded.present
? data.directorySeeded.value
: this.directorySeeded,
);
}
@override
String toString() {
return (StringBuffer('FirstStartData(')
..write('id: $id, ')
..write('directorySeeded: $directorySeeded')
..write(')'))
.toString();
}
@override
int get hashCode => Object.hash(id, directorySeeded);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is FirstStartData &&
other.id == this.id &&
other.directorySeeded == this.directorySeeded);
}
class FirstStartCompanion extends UpdateCompanion<FirstStartData> {
final Value<int> id;
final Value<bool> directorySeeded;
const FirstStartCompanion({
this.id = const Value.absent(),
this.directorySeeded = const Value.absent(),
});
FirstStartCompanion.insert({
this.id = const Value.absent(),
this.directorySeeded = const Value.absent(),
});
static Insertable<FirstStartData> custom({
Expression<int>? id,
Expression<bool>? directorySeeded,
}) {
return RawValuesInsertable({
if (id != null) 'id': id,
if (directorySeeded != null) 'directory_seeded': directorySeeded,
});
}
FirstStartCompanion copyWith({Value<int>? id, Value<bool>? directorySeeded}) {
return FirstStartCompanion(
id: id ?? this.id,
directorySeeded: directorySeeded ?? this.directorySeeded,
);
}
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
if (id.present) {
map['id'] = Variable<int>(id.value);
}
if (directorySeeded.present) {
map['directory_seeded'] = Variable<bool>(directorySeeded.value);
}
return map;
}
@override
String toString() {
return (StringBuffer('FirstStartCompanion(')
..write('id: $id, ')
..write('directorySeeded: $directorySeeded')
..write(')'))
.toString();
}
}
abstract class _$MewsDatabase extends GeneratedDatabase {
_$MewsDatabase(QueryExecutor e) : super(e);
$MewsDatabaseManager get managers => $MewsDatabaseManager(this);
@ -3392,6 +3595,7 @@ abstract class _$MewsDatabase extends GeneratedDatabase {
late final $SiteCertificatesTable siteCertificates = $SiteCertificatesTable(
this,
);
late final $FirstStartTable firstStart = $FirstStartTable(this);
late final Index historyItemsUrl = Index(
'history_items_url',
'CREATE INDEX history_items_url ON history_items (url)',
@ -3411,6 +3615,7 @@ abstract class _$MewsDatabase extends GeneratedDatabase {
knownHosts,
clientCertificates,
siteCertificates,
firstStart,
historyItemsUrl,
];
}
@ -5333,6 +5538,137 @@ typedef $$SiteCertificatesTableProcessedTableManager =
SiteCertificate,
PrefetchHooks Function()
>;
typedef $$FirstStartTableCreateCompanionBuilder =
FirstStartCompanion Function({Value<int> id, Value<bool> directorySeeded});
typedef $$FirstStartTableUpdateCompanionBuilder =
FirstStartCompanion Function({Value<int> id, Value<bool> directorySeeded});
class $$FirstStartTableFilterComposer
extends Composer<_$MewsDatabase, $FirstStartTable> {
$$FirstStartTableFilterComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
ColumnFilters<int> get id => $composableBuilder(
column: $table.id,
builder: (column) => ColumnFilters(column),
);
ColumnFilters<bool> get directorySeeded => $composableBuilder(
column: $table.directorySeeded,
builder: (column) => ColumnFilters(column),
);
}
class $$FirstStartTableOrderingComposer
extends Composer<_$MewsDatabase, $FirstStartTable> {
$$FirstStartTableOrderingComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
ColumnOrderings<int> get id => $composableBuilder(
column: $table.id,
builder: (column) => ColumnOrderings(column),
);
ColumnOrderings<bool> get directorySeeded => $composableBuilder(
column: $table.directorySeeded,
builder: (column) => ColumnOrderings(column),
);
}
class $$FirstStartTableAnnotationComposer
extends Composer<_$MewsDatabase, $FirstStartTable> {
$$FirstStartTableAnnotationComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
GeneratedColumn<int> get id =>
$composableBuilder(column: $table.id, builder: (column) => column);
GeneratedColumn<bool> get directorySeeded => $composableBuilder(
column: $table.directorySeeded,
builder: (column) => column,
);
}
class $$FirstStartTableTableManager
extends
RootTableManager<
_$MewsDatabase,
$FirstStartTable,
FirstStartData,
$$FirstStartTableFilterComposer,
$$FirstStartTableOrderingComposer,
$$FirstStartTableAnnotationComposer,
$$FirstStartTableCreateCompanionBuilder,
$$FirstStartTableUpdateCompanionBuilder,
(
FirstStartData,
BaseReferences<_$MewsDatabase, $FirstStartTable, FirstStartData>,
),
FirstStartData,
PrefetchHooks Function()
> {
$$FirstStartTableTableManager(_$MewsDatabase db, $FirstStartTable table)
: super(
TableManagerState(
db: db,
table: table,
createFilteringComposer: () =>
$$FirstStartTableFilterComposer($db: db, $table: table),
createOrderingComposer: () =>
$$FirstStartTableOrderingComposer($db: db, $table: table),
createComputedFieldComposer: () =>
$$FirstStartTableAnnotationComposer($db: db, $table: table),
updateCompanionCallback:
({
Value<int> id = const Value.absent(),
Value<bool> directorySeeded = const Value.absent(),
}) =>
FirstStartCompanion(id: id, directorySeeded: directorySeeded),
createCompanionCallback:
({
Value<int> id = const Value.absent(),
Value<bool> directorySeeded = const Value.absent(),
}) => FirstStartCompanion.insert(
id: id,
directorySeeded: directorySeeded,
),
withReferenceMapper: (p0) => p0
.map((e) => (e.readTable(table), BaseReferences(db, table, e)))
.toList(),
prefetchHooksCallback: null,
),
);
}
typedef $$FirstStartTableProcessedTableManager =
ProcessedTableManager<
_$MewsDatabase,
$FirstStartTable,
FirstStartData,
$$FirstStartTableFilterComposer,
$$FirstStartTableOrderingComposer,
$$FirstStartTableAnnotationComposer,
$$FirstStartTableCreateCompanionBuilder,
$$FirstStartTableUpdateCompanionBuilder,
(
FirstStartData,
BaseReferences<_$MewsDatabase, $FirstStartTable, FirstStartData>,
),
FirstStartData,
PrefetchHooks Function()
>;
class $MewsDatabaseManager {
final _$MewsDatabase _db;
@ -5357,4 +5693,6 @@ class $MewsDatabaseManager {
$$ClientCertificatesTableTableManager(_db, _db.clientCertificates);
$$SiteCertificatesTableTableManager get siteCertificates =>
$$SiteCertificatesTableTableManager(_db, _db.siteCertificates);
$$FirstStartTableTableManager get firstStart =>
$$FirstStartTableTableManager(_db, _db.firstStart);
}

View file

@ -101,6 +101,11 @@ class AppDependencies {
Future<void> start() async {
await settings.load();
await applyLanguage();
// A fresh install starts with the project's directory of sites
// bookmarked, so the reader has somewhere to go; it is given once, so
// removing it is the reader's own and it never comes back.
await db.seedDirectoryBookmark(
'https://mews.page/directory', strings.t('bookmarks-directory-title'));
await bookmarks.load();
await subscriptions.load();
await browser.restoreLastSession();