add isolated tab feature

This commit is contained in:
Fabian Freund
2026-02-27 08:29:50 +01:00
parent 3ef567cc19
commit d24675094d
84 changed files with 3783 additions and 513 deletions
@@ -24,6 +24,7 @@ import 'package:drift/drift.dart';
import 'package:weblibre/features/geckoview/features/tabs/data/database/daos/container.drift.dart';
import 'package:weblibre/features/geckoview/features/tabs/data/database/database.dart';
import 'package:weblibre/features/geckoview/features/tabs/data/database/definitions.drift.dart';
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_mode.dart';
import 'package:weblibre/features/geckoview/features/tabs/data/models/container_data.dart';
import 'package:weblibre/features/geckoview/features/tabs/data/models/site_assignment.dart';
@@ -75,18 +76,17 @@ class ContainerDao extends DatabaseAccessor<TabDatabase>
Selectable<String> getAllTabIds({
bool includeRegular = true,
bool includePrivate = true,
bool includeIsolated = true,
}) {
final query = selectOnly(db.tab)..addColumns([db.tab.id]);
if (!includeRegular) {
query.where(
db.tab.isPrivate.isNotNull() & db.tab.isPrivate.isNotValue(false),
);
}
if (!includePrivate) {
query.where(
db.tab.isPrivate.isNotNull() & db.tab.isPrivate.isNotValue(true),
);
final excludedModes = <TabModeDbValue>[];
if (!includeRegular) excludedModes.add(TabModeDbValue.regular);
if (!includePrivate) excludedModes.add(TabModeDbValue.private);
if (!includeIsolated) excludedModes.add(TabModeDbValue.isolated);
if (excludedModes.isNotEmpty) {
query.where(db.tab.tabMode.isNotInValues(excludedModes));
}
return query.map((row) => row.read(db.tab.id)!);
@@ -96,6 +96,7 @@ class ContainerDao extends DatabaseAccessor<TabDatabase>
String? containerId, {
bool includeRegular = true,
bool includePrivate = true,
bool includeIsolated = true,
}) {
final query = selectOnly(db.tab)
..addColumns([db.tab.id])
@@ -106,15 +107,13 @@ class ContainerDao extends DatabaseAccessor<TabDatabase>
)
..orderBy([OrderingTerm.asc(db.tab.orderKey)]);
if (!includeRegular) {
query.where(
db.tab.isPrivate.isNotNull() & db.tab.isPrivate.isNotValue(false),
);
}
if (!includePrivate) {
query.where(
db.tab.isPrivate.isNotNull() & db.tab.isPrivate.isNotValue(true),
);
final excludedModes = <TabModeDbValue>[];
if (!includeRegular) excludedModes.add(TabModeDbValue.regular);
if (!includePrivate) excludedModes.add(TabModeDbValue.private);
if (!includeIsolated) excludedModes.add(TabModeDbValue.isolated);
if (excludedModes.isNotEmpty) {
query.where(db.tab.tabMode.isNotInValues(excludedModes));
}
return query.map((row) => row.read(db.tab.id)!);
@@ -26,11 +26,22 @@ import 'package:weblibre/features/geckoview/domain/entities/states/tab.dart';
import 'package:weblibre/features/geckoview/features/tabs/data/database/daos/tab.drift.dart';
import 'package:weblibre/features/geckoview/features/tabs/data/database/database.dart';
import 'package:weblibre/features/geckoview/features/tabs/data/database/definitions.drift.dart';
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_mode.dart';
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_source.dart';
import 'package:weblibre/features/geckoview/features/tabs/data/models/container_data.dart';
import 'package:weblibre/features/geckoview/features/tabs/data/models/tab_query_result.dart';
@DriftAccessor()
class SyncTabsResult {
final Set<String> deletedIsolationContextIds;
final int deletedCount;
const SyncTabsResult({
required this.deletedIsolationContextIds,
required this.deletedCount,
});
}
class TabDao extends DatabaseAccessor<TabDatabase> with $TabDaoMixin {
final _undoHistory = <String, TabData>{};
Timer? _clearHistoryTimer;
@@ -45,12 +56,25 @@ class TabDao extends DatabaseAccessor<TabDatabase> with $TabDaoMixin {
SingleOrNullSelectable<TabData> getTabDataById(String id) =>
db.tab.select()..where((t) => t.id.equals(id));
SingleOrNullSelectable<bool?> getTabIsPrivate(String tabId) {
SingleOrNullSelectable<TabMode> getTabMode(String tabId) {
final query = selectOnly(db.tab)
..addColumns([db.tab.isPrivate])
..addColumns([db.tab.tabMode, db.tab.isolationContextId])
..where(db.tab.id.equals(tabId));
return query.map((row) => row.read(db.tab.isPrivate));
return query.map(
(row) => TabMode.fromDbValue(
row.readWithConverter(db.tab.tabMode)!,
isolationContextId: row.read(db.tab.isolationContextId),
),
);
}
SingleOrNullSelectable<String?> getTabIsolationContextId(String tabId) {
final query = selectOnly(db.tab)
..addColumns([db.tab.isolationContextId])
..where(db.tab.id.equals(tabId));
return query.map((row) => row.read(db.tab.isolationContextId));
}
Selectable<String> getAllTabIds() {
@@ -124,18 +148,24 @@ class TabDao extends DatabaseAccessor<TabDatabase> with $TabDaoMixin {
Future<String> upsertTabTransactional(
Future<String> Function() createTab, {
required Value<bool?> isPrivate,
required Value<String?> parentId,
Value<String?> containerId = const Value.absent(),
Value<String?> orderKey = const Value.absent(),
Value<Uri?> url = const Value.absent(),
Value<String?> title = const Value.absent(),
Value<TabMode> tabMode = const Value.absent(),
}) {
return db.transaction(() async {
final tabId = await createTab();
final currentOrderKey =
orderKey.value ??
await _generateOrderKey(parentId: parentId, containerId: containerId);
final Value<TabModeDbValue> persistedTabMode = tabMode.present
? Value(tabMode.value.toDbValue())
: const Value.absent();
final Value<String?> isolationContextId = tabMode.present
? Value(tabMode.value.isolationContextId)
: const Value.absent();
await db.tab.insertOne(
TabCompanion.insert(
@@ -146,8 +176,9 @@ class TabDao extends DatabaseAccessor<TabDatabase> with $TabDaoMixin {
containerId: containerId,
url: url,
title: title,
isPrivate: isPrivate,
orderKey: currentOrderKey,
tabMode: persistedTabMode,
isolationContextId: isolationContextId,
),
onConflict: DoUpdate(
(old) => TabCompanion(
@@ -156,8 +187,9 @@ class TabDao extends DatabaseAccessor<TabDatabase> with $TabDaoMixin {
containerId: containerId,
url: url,
title: title,
isPrivate: isPrivate,
orderKey: Value.absentIfNull(orderKey.value),
tabMode: persistedTabMode,
isolationContextId: isolationContextId,
),
),
);
@@ -170,17 +202,23 @@ class TabDao extends DatabaseAccessor<TabDatabase> with $TabDaoMixin {
Future<String> insertTab(
String tabId, {
required TabSource source,
required Value<bool?> isPrivate,
required Value<String?> parentId,
Value<String?> containerId = const Value.absent(),
Value<String?> orderKey = const Value.absent(),
Value<Uri?> url = const Value.absent(),
Value<String?> title = const Value.absent(),
Value<TabMode> tabMode = const Value.absent(),
}) {
return db.transaction(() async {
final currentOrderKey =
orderKey.value ??
await _generateOrderKey(parentId: parentId, containerId: containerId);
final Value<TabModeDbValue> persistedTabMode = tabMode.present
? Value(tabMode.value.toDbValue())
: const Value.absent();
final Value<String?> isolationContextId = tabMode.present
? Value(tabMode.value.isolationContextId)
: const Value.absent();
await db.tab.insertOne(
TabCompanion.insert(
@@ -190,9 +228,10 @@ class TabDao extends DatabaseAccessor<TabDatabase> with $TabDaoMixin {
timestamp: DateTime.now(),
containerId: containerId,
orderKey: currentOrderKey,
isPrivate: isPrivate,
url: url,
title: title,
tabMode: persistedTabMode,
isolationContextId: isolationContextId,
),
onConflict: DoUpdate(
(old) => TabCompanion(
@@ -201,8 +240,9 @@ class TabDao extends DatabaseAccessor<TabDatabase> with $TabDaoMixin {
containerId: containerId,
url: url,
title: title,
isPrivate: isPrivate,
orderKey: Value.absentIfNull(orderKey.value),
tabMode: persistedTabMode,
isolationContextId: isolationContextId,
),
where: (old) => old.source.isSmallerThanValue(source.index),
),
@@ -301,7 +341,8 @@ class TabDao extends DatabaseAccessor<TabDatabase> with $TabDaoMixin {
if (previousState == null ||
previousState.url != state.url ||
previousState.title != state.title ||
previousState.parentId != state.parentId) {
previousState.parentId != state.parentId ||
previousState.tabMode != state.tabMode) {
batch.update(
db.tab,
TabCompanion(
@@ -314,6 +355,12 @@ class TabDao extends DatabaseAccessor<TabDatabase> with $TabDaoMixin {
title: (previousState?.title != state.title)
? Value(state.title)
: const Value.absent(),
tabMode: (previousState?.tabMode != state.tabMode)
? Value(state.tabMode.toDbValue())
: const Value.absent(),
isolationContextId: (previousState?.tabMode != state.tabMode)
? Value(state.isolationContextId)
: const Value.absent(),
),
where: (t) => t.id.equals(state.id),
);
@@ -323,12 +370,19 @@ class TabDao extends DatabaseAccessor<TabDatabase> with $TabDaoMixin {
});
}
Future<void> syncTabs({required List<String> retainTabIds}) {
/// Syncs DB tab rows with the engine's active tab list.
/// Returns metadata about deleted rows for follow-up cleanup.
Future<SyncTabsResult> syncTabs({required List<String> retainTabIds}) {
return db.transaction(() async {
final deleted =
await (db.tab.delete()..where((t) => t.id.isNotIn(retainTabIds)))
.goAndReturn();
final deletedIsolationContextIds = <String>{
for (final tab in deleted)
if (tab.isolationContextId != null) tab.isolationContextId!,
};
if (deleted.isNotEmpty) {
_clearHistoryTimer?.cancel();
@@ -361,6 +415,11 @@ class TabDao extends DatabaseAccessor<TabDatabase> with $TabDaoMixin {
}),
onConflict: DoNothing(),
);
return SyncTabsResult(
deletedIsolationContextIds: deletedIsolationContextIds,
deletedCount: deleted.length,
);
});
}
@@ -391,6 +450,19 @@ class TabDao extends DatabaseAccessor<TabDatabase> with $TabDaoMixin {
}
}
SingleSelectable<int> tabsInIsolationGroup(String contextId) {
return db.definitionsDrift.tabsInIsolationGroup(contextId: contextId);
}
Selectable<String?> allIsolationContextIds() {
return db.definitionsDrift.allIsolationContextIds();
}
Selectable<IsolatedContextContainerPairsResult>
isolatedContextContainerPairs() {
return db.definitionsDrift.isolatedContextContainerPairs();
}
Future<List<String>> getUnassignedTabsOlderThan(DateTime threshold) {
final query = selectOnly(db.tab)
..addColumns([db.tab.id])
@@ -31,7 +31,7 @@ import 'package:weblibre/features/search/domain/fts_tokenizer.dart';
@DriftDatabase(include: {'definitions.drift'}, daos: [ContainerDao, TabDao])
class TabDatabase extends $TabDatabase with TrigramQueryBuilderMixin {
@override
final int schemaVersion = 5;
final int schemaVersion = 6;
@override
final int ftsTokenLimit = 10;
@@ -99,5 +99,21 @@ class TabDatabase extends $TabDatabase with TrigramQueryBuilderMixin {
await m.drop(schema.tabMaintainParentChainOnDelete);
await m.create(schema.tabMaintainParentChainOnDelete);
},
from5To6: (m, schema) async {
await m.alterTable(
TableMigration(
schema.tab,
columnTransformer: {
// Backfill tab_mode from is_private: private=true -> 1 (private), else -> 0 (regular)
schema.tab.tabMode: const CustomExpression(
'CASE WHEN is_private = 1 THEN 1 ELSE 0 END',
),
},
newColumns: [schema.tab.tabMode, schema.tab.isolationContextId],
),
);
await m.alterTable(TableMigration(schema.tab));
},
);
}
@@ -1,9 +1,10 @@
// dart format width=80
import 'package:drift/internal/versioned_schema.dart' as i0;
import 'package:drift/drift.dart' as i1;
import 'package:drift/drift.dart'; // ignore_for_file: type=lint,unused_import
import 'package:drift/drift.dart'; // GENERATED BY drift_dev, DO NOT MODIFY.
// GENERATED BY drift_dev, DO NOT MODIFY.
// ignore_for_file: type=lint,unused_import
//
final class Schema3 extends i0.VersionedSchema {
Schema3({required super.database}) : super(version: 3);
@override
@@ -461,10 +462,143 @@ final class Schema5 extends i0.VersionedSchema {
);
}
final class Schema6 extends i0.VersionedSchema {
Schema6({required super.database}) : super(version: 6);
@override
late final List<i1.DatabaseSchemaEntity> entities = [
container,
tab,
tabFts,
tabMaintainParentChainOnDelete,
tabAfterInsert,
tabAfterDelete,
tabAfterUpdate,
];
late final Shape0 container = Shape0(
source: i0.VersionedTable(
entityName: 'container',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [_column_0, _column_1, _column_2, _column_3],
attachedDatabase: database,
),
alias: null,
);
late final Shape4 tab = Shape4(
source: i0.VersionedTable(
entityName: 'tab',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'CHECK((tab_mode = 2 AND isolation_context_id IS NOT NULL)OR(tab_mode != 2 AND isolation_context_id IS NULL))',
],
columns: [
_column_0,
_column_16,
_column_4,
_column_5,
_column_6,
_column_7,
_column_8,
_column_17,
_column_18,
_column_10,
_column_11,
_column_12,
_column_13,
_column_14,
_column_15,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape2 tabFts = Shape2(
source: i0.VersionedVirtualTable(
entityName: 'tab_fts',
moduleAndArgs:
'fts5(title, url, extracted_content_plain, full_content_plain, content=tab, tokenize="trigram")',
columns: [_column_8, _column_7, _column_12, _column_14],
attachedDatabase: database,
),
alias: null,
);
final i1.Trigger tabMaintainParentChainOnDelete = i1.Trigger(
'CREATE TRIGGER tab_maintain_parent_chain_on_delete BEFORE DELETE ON tab BEGIN UPDATE tab SET parent_id = CASE WHEN OLD.parent_id IS NOT NULL AND EXISTS (SELECT 1 FROM tab WHERE id = OLD.parent_id) THEN OLD.parent_id ELSE NULL END WHERE parent_id = OLD.id;END',
'tab_maintain_parent_chain_on_delete',
);
final i1.Trigger tabAfterInsert = i1.Trigger(
'CREATE TRIGGER tab_after_insert AFTER INSERT ON tab BEGIN INSERT INTO tab_fts ("rowid", title, url, extracted_content_plain, full_content_plain) VALUES (new."rowid", new.title, new.url, new.extracted_content_plain, new.full_content_plain);END',
'tab_after_insert',
);
final i1.Trigger tabAfterDelete = i1.Trigger(
'CREATE TRIGGER tab_after_delete AFTER DELETE ON tab BEGIN INSERT INTO tab_fts (tab_fts, "rowid", title, url, extracted_content_plain, full_content_plain) VALUES (\'delete\', old."rowid", old.title, old.url, old.extracted_content_plain, old.full_content_plain);END',
'tab_after_delete',
);
final i1.Trigger tabAfterUpdate = i1.Trigger(
'CREATE TRIGGER tab_after_update AFTER UPDATE ON tab BEGIN INSERT INTO tab_fts (tab_fts, "rowid", title, url, extracted_content_plain, full_content_plain) VALUES (\'delete\', old."rowid", old.title, old.url, old.extracted_content_plain, old.full_content_plain);INSERT INTO tab_fts ("rowid", title, url, extracted_content_plain, full_content_plain) VALUES (new."rowid", new.title, new.url, new.extracted_content_plain, new.full_content_plain);END',
'tab_after_update',
);
}
class Shape4 extends i0.VersionedTable {
Shape4({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<String> get id =>
columnsByName['id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<int> get source =>
columnsByName['source']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get parentId =>
columnsByName['parent_id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get containerId =>
columnsByName['container_id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get orderKey =>
columnsByName['order_key']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get url =>
columnsByName['url']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get title =>
columnsByName['title']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<int> get tabMode =>
columnsByName['tab_mode']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get isolationContextId =>
columnsByName['isolation_context_id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<int> get isProbablyReaderable =>
columnsByName['is_probably_readerable']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get extractedContentMarkdown =>
columnsByName['extracted_content_markdown']!
as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get extractedContentPlain =>
columnsByName['extracted_content_plain']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get fullContentMarkdown =>
columnsByName['full_content_markdown']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get fullContentPlain =>
columnsByName['full_content_plain']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<int> get timestamp =>
columnsByName['timestamp']! as i1.GeneratedColumn<int>;
}
i1.GeneratedColumn<int> _column_17(String aliasedName) =>
i1.GeneratedColumn<int>(
'tab_mode',
aliasedName,
false,
type: i1.DriftSqlType.int,
$customConstraints: 'NOT NULL DEFAULT 0',
defaultValue: const i1.CustomExpression('0'),
);
i1.GeneratedColumn<String> _column_18(String aliasedName) =>
i1.GeneratedColumn<String>(
'isolation_context_id',
aliasedName,
true,
type: i1.DriftSqlType.string,
$customConstraints: '',
);
i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
required Future<void> Function(i1.Migrator m, Schema4 schema) from3To4,
required Future<void> Function(i1.Migrator m, Schema5 schema) from4To5,
required Future<void> Function(i1.Migrator m, Schema6 schema) from5To6,
}) {
return (currentVersion, database) async {
switch (currentVersion) {
@@ -483,6 +617,11 @@ i0.MigrationStepWithVersion migrationSteps({
final migrator = i1.Migrator(database, schema);
await from4To5(migrator, schema);
return 5;
case 5:
final schema = Schema6(database: database);
final migrator = i1.Migrator(database, schema);
await from5To6(migrator, schema);
return 6;
default:
throw ArgumentError.value('Unknown migration from $currentVersion');
}
@@ -493,10 +632,12 @@ i1.OnUpgrade stepByStep({
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
required Future<void> Function(i1.Migrator m, Schema4 schema) from3To4,
required Future<void> Function(i1.Migrator m, Schema5 schema) from4To5,
required Future<void> Function(i1.Migrator m, Schema6 schema) from5To6,
}) => i0.VersionedSchema.stepByStepHelper(
step: migrationSteps(
from2To3: from2To3,
from3To4: from3To4,
from4To5: from4To5,
from5To6: from5To6,
),
);
@@ -4,6 +4,7 @@ import 'package:weblibre/features/geckoview/features/tabs/data/models/container_
import 'package:weblibre/features/geckoview/features/tabs/data/models/site_assignment.dart';
import 'package:weblibre/features/geckoview/features/tabs/data/models/tab_query_result.dart';
import 'package:weblibre/features/geckoview/features/tabs/data/database/converters/container_metadata_converter.dart';
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_mode.dart';
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_source.dart';
CREATE TABLE container (
@@ -13,7 +14,7 @@ CREATE TABLE container (
metadata TEXT MAPPED BY `const ContainerMetadataConverter()`
) WITH ContainerData;
CREATE TABLE tab(
CREATE TABLE tab(
id TEXT PRIMARY KEY NOT NULL,
source ENUM(TabSource) NOT NULL,
parent_id TEXT REFERENCES tab (id) ON DELETE SET NULL,
@@ -21,13 +22,18 @@ CREATE TABLE tab(
order_key TEXT NOT NULL,
url TEXT MAPPED BY `const UriConverterNullable()`,
title TEXT,
is_private BOOL,
tab_mode ENUM(TabModeDbValue) NOT NULL DEFAULT 0,
isolation_context_id TEXT,
is_probably_readerable BOOL,
extracted_content_markdown TEXT,
extracted_content_plain TEXT,
full_content_markdown TEXT,
full_content_plain TEXT,
timestamp DATETIME NOT NULL
timestamp DATETIME NOT NULL,
CHECK (
(tab_mode = 2 AND isolation_context_id IS NOT NULL) OR
(tab_mode != 2 AND isolation_context_id IS NULL)
)
);
CREATE VIRTUAL TABLE tab_fts
@@ -142,58 +148,58 @@ orderKeyBeforeTab(:tab_id AS TEXT, REQUIRED :container_id AS TEXT OR NULL):
queryTabsBasic WITH TabQueryResult:
WITH weights AS (
SELECT
SELECT
-- Customize these weights (higher = more important)
10.0 as title_weight, -- Title matches are most important
5.0 as url_weight -- URL matches are quite important
)
SELECT
SELECT
t.id,
t.container_id,
t.is_private,
t.tab_mode,
t.title,
CAST(t.url AS TEXT) AS url,
t.url AS clean_url,
bm25(tab_fts, weights.title_weight, weights.url_weight) AS weighted_rank
FROM tab_fts fts
INNER JOIN
tab t ON t.rowid = fts.rowid
tab t ON t.rowid = fts.rowid
CROSS JOIN weights
WHERE
WHERE
fts.title LIKE :query OR
fts.url LIKE :query
ORDER BY
ORDER BY
weighted_rank ASC,
t.timestamp DESC
LIMIT :limit;
queryTabsFullContent WITH TabQueryResult:
WITH weights AS (
SELECT
SELECT
-- Customize these weights (higher = more important)
10.0 as title_weight, -- Title matches are most important
5.0 as url_weight, -- URL matches are quite important
3.0 as extracted_weight, -- Extracted content matches
1.0 as full_weight -- Full content matches less important
)
SELECT
SELECT
t.id,
t.container_id,
t.is_private,
t.tab_mode,
highlight(tab_fts, 0, :beforeMatch, :afterMatch) AS title,
highlight(tab_fts, 1, :beforeMatch, :afterMatch) AS url,
snippet(tab_fts, 2, :beforeMatch, :afterMatch, :ellipsis, :snippetLength) AS extracted_content,
snippet(tab_fts, 3, :beforeMatch, :afterMatch, :ellipsis, :snippetLength) AS full_content,
t.url AS clean_url,
(
bm25(tab_fts, weights.title_weight, weights.url_weight,
bm25(tab_fts, weights.title_weight, weights.url_weight,
weights.extracted_weight, weights.full_weight)
) AS weighted_rank
FROM tab_fts(:query) fts
INNER JOIN
tab t ON t.rowid = fts.rowid
CROSS JOIN weights
ORDER BY
ORDER BY
weighted_rank ASC,
t.timestamp DESC
LIMIT :limit;
@@ -334,3 +340,16 @@ containersToClearOnExit:
WHERE
json_extract(container.metadata, '$.clearDataOnExit') = 1
AND container.metadata ->> '$.contextualIdentity' IS NOT NULL;
tabsInIsolationGroup:
SELECT COUNT(*) AS count FROM tab WHERE isolation_context_id = :contextId;
allIsolationContextIds:
SELECT DISTINCT isolation_context_id FROM tab WHERE isolation_context_id IS NOT NULL;
isolatedContextContainerPairs:
SELECT DISTINCT t.isolation_context_id, t.container_id
FROM tab t
WHERE t.tab_mode = 2
AND t.isolation_context_id IS NOT NULL
AND t.container_id IS NOT NULL;
@@ -11,12 +11,14 @@ import 'package:weblibre/features/geckoview/features/tabs/data/database/converte
as i5;
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_source.dart'
as i6;
import 'package:weblibre/data/database/converters/uri.dart' as i7;
import 'package:drift/internal/modular.dart' as i8;
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_mode.dart'
as i7;
import 'package:weblibre/data/database/converters/uri.dart' as i8;
import 'package:drift/internal/modular.dart' as i9;
import 'package:weblibre/features/geckoview/features/tabs/data/models/tab_query_result.dart'
as i9;
import 'package:weblibre/features/geckoview/features/tabs/data/models/site_assignment.dart'
as i10;
import 'package:weblibre/features/geckoview/features/tabs/data/models/site_assignment.dart'
as i11;
typedef $ContainerCreateCompanionBuilder =
i3.ContainerCompanion Function({
@@ -47,10 +49,10 @@ final class $ContainerReferences
static i0.MultiTypedResultKey<i3.Tab, List<i3.TabData>> _tabRefsTable(
i0.GeneratedDatabase db,
) => i0.MultiTypedResultKey.fromTable(
i8.ReadDatabaseContainer(db).resultSet<i3.Tab>('tab'),
i9.ReadDatabaseContainer(db).resultSet<i3.Tab>('tab'),
aliasName: i0.$_aliasNameGenerator(
i8.ReadDatabaseContainer(db).resultSet<i3.Container>('container').id,
i8.ReadDatabaseContainer(db).resultSet<i3.Tab>('tab').containerId,
i9.ReadDatabaseContainer(db).resultSet<i3.Container>('container').id,
i9.ReadDatabaseContainer(db).resultSet<i3.Tab>('tab').containerId,
),
);
@@ -58,7 +60,7 @@ final class $ContainerReferences
final manager = i3
.$TabTableManager(
$_db,
i8.ReadDatabaseContainer($_db).resultSet<i3.Tab>('tab'),
i9.ReadDatabaseContainer($_db).resultSet<i3.Tab>('tab'),
)
.filter((f) => f.containerId.id.sqlEquals($_itemColumn<String>('id')!));
@@ -110,7 +112,7 @@ class $ContainerFilterComposer
final i3.$TabFilterComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.id,
referencedTable: i8.ReadDatabaseContainer($db).resultSet<i3.Tab>('tab'),
referencedTable: i9.ReadDatabaseContainer($db).resultSet<i3.Tab>('tab'),
getReferencedColumn: (t) => t.containerId,
builder:
(
@@ -119,7 +121,7 @@ class $ContainerFilterComposer
$removeJoinBuilderFromRootComposer,
}) => i3.$TabFilterComposer(
$db: $db,
$table: i8.ReadDatabaseContainer($db).resultSet<i3.Tab>('tab'),
$table: i9.ReadDatabaseContainer($db).resultSet<i3.Tab>('tab'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
@@ -188,7 +190,7 @@ class $ContainerAnnotationComposer
final i3.$TabAnnotationComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.id,
referencedTable: i8.ReadDatabaseContainer($db).resultSet<i3.Tab>('tab'),
referencedTable: i9.ReadDatabaseContainer($db).resultSet<i3.Tab>('tab'),
getReferencedColumn: (t) => t.containerId,
builder:
(
@@ -197,7 +199,7 @@ class $ContainerAnnotationComposer
$removeJoinBuilderFromRootComposer,
}) => i3.$TabAnnotationComposer(
$db: $db,
$table: i8.ReadDatabaseContainer($db).resultSet<i3.Tab>('tab'),
$table: i9.ReadDatabaseContainer($db).resultSet<i3.Tab>('tab'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
@@ -275,7 +277,7 @@ class $ContainerTableManager
db: db,
explicitlyWatchedTables: [
if (tabRefs)
i8.ReadDatabaseContainer(db).resultSet<i3.Tab>('tab'),
i9.ReadDatabaseContainer(db).resultSet<i3.Tab>('tab'),
],
addJoins: null,
getPrefetchedDataCallback: (items) async {
@@ -329,7 +331,8 @@ typedef $TabCreateCompanionBuilder =
required String orderKey,
i0.Value<Uri?> url,
i0.Value<String?> title,
i0.Value<bool?> isPrivate,
i0.Value<i7.TabModeDbValue> tabMode,
i0.Value<String?> isolationContextId,
i0.Value<bool?> isProbablyReaderable,
i0.Value<String?> extractedContentMarkdown,
i0.Value<String?> extractedContentPlain,
@@ -347,7 +350,8 @@ typedef $TabUpdateCompanionBuilder =
i0.Value<String> orderKey,
i0.Value<Uri?> url,
i0.Value<String?> title,
i0.Value<bool?> isPrivate,
i0.Value<i7.TabModeDbValue> tabMode,
i0.Value<String?> isolationContextId,
i0.Value<bool?> isProbablyReaderable,
i0.Value<String?> extractedContentMarkdown,
i0.Value<String?> extractedContentPlain,
@@ -362,12 +366,12 @@ final class $TabReferences
$TabReferences(super.$_db, super.$_table, super.$_typedResult);
static i3.Container _containerIdTable(i0.GeneratedDatabase db) =>
i8.ReadDatabaseContainer(db)
i9.ReadDatabaseContainer(db)
.resultSet<i3.Container>('container')
.createAlias(
i0.$_aliasNameGenerator(
i8.ReadDatabaseContainer(db).resultSet<i3.Tab>('tab').containerId,
i8.ReadDatabaseContainer(
i9.ReadDatabaseContainer(db).resultSet<i3.Tab>('tab').containerId,
i9.ReadDatabaseContainer(
db,
).resultSet<i3.Container>('container').id,
),
@@ -379,7 +383,7 @@ final class $TabReferences
final manager = i3
.$ContainerTableManager(
$_db,
i8.ReadDatabaseContainer($_db).resultSet<i3.Container>('container'),
i9.ReadDatabaseContainer($_db).resultSet<i3.Container>('container'),
)
.filter((f) => f.id.sqlEquals($_column));
final item = $_typedResult.readTableOrNull(_containerIdTable($_db));
@@ -430,8 +434,14 @@ class $TabFilterComposer extends i0.Composer<i0.GeneratedDatabase, i3.Tab> {
builder: (column) => i0.ColumnFilters(column),
);
i0.ColumnFilters<bool> get isPrivate => $composableBuilder(
column: $table.isPrivate,
i0.ColumnWithTypeConverterFilters<i7.TabModeDbValue, i7.TabModeDbValue, int>
get tabMode => $composableBuilder(
column: $table.tabMode,
builder: (column) => i0.ColumnWithTypeConverterFilters(column),
);
i0.ColumnFilters<String> get isolationContextId => $composableBuilder(
column: $table.isolationContextId,
builder: (column) => i0.ColumnFilters(column),
);
@@ -469,7 +479,7 @@ class $TabFilterComposer extends i0.Composer<i0.GeneratedDatabase, i3.Tab> {
final i3.$ContainerFilterComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.containerId,
referencedTable: i8.ReadDatabaseContainer(
referencedTable: i9.ReadDatabaseContainer(
$db,
).resultSet<i3.Container>('container'),
getReferencedColumn: (t) => t.id,
@@ -480,7 +490,7 @@ class $TabFilterComposer extends i0.Composer<i0.GeneratedDatabase, i3.Tab> {
$removeJoinBuilderFromRootComposer,
}) => i3.$ContainerFilterComposer(
$db: $db,
$table: i8.ReadDatabaseContainer(
$table: i9.ReadDatabaseContainer(
$db,
).resultSet<i3.Container>('container'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
@@ -531,8 +541,13 @@ class $TabOrderingComposer extends i0.Composer<i0.GeneratedDatabase, i3.Tab> {
builder: (column) => i0.ColumnOrderings(column),
);
i0.ColumnOrderings<bool> get isPrivate => $composableBuilder(
column: $table.isPrivate,
i0.ColumnOrderings<int> get tabMode => $composableBuilder(
column: $table.tabMode,
builder: (column) => i0.ColumnOrderings(column),
);
i0.ColumnOrderings<String> get isolationContextId => $composableBuilder(
column: $table.isolationContextId,
builder: (column) => i0.ColumnOrderings(column),
);
@@ -570,7 +585,7 @@ class $TabOrderingComposer extends i0.Composer<i0.GeneratedDatabase, i3.Tab> {
final i3.$ContainerOrderingComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.containerId,
referencedTable: i8.ReadDatabaseContainer(
referencedTable: i9.ReadDatabaseContainer(
$db,
).resultSet<i3.Container>('container'),
getReferencedColumn: (t) => t.id,
@@ -581,7 +596,7 @@ class $TabOrderingComposer extends i0.Composer<i0.GeneratedDatabase, i3.Tab> {
$removeJoinBuilderFromRootComposer,
}) => i3.$ContainerOrderingComposer(
$db: $db,
$table: i8.ReadDatabaseContainer(
$table: i9.ReadDatabaseContainer(
$db,
).resultSet<i3.Container>('container'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
@@ -620,8 +635,13 @@ class $TabAnnotationComposer extends i0.Composer<i0.GeneratedDatabase, i3.Tab> {
i0.GeneratedColumn<String> get title =>
$composableBuilder(column: $table.title, builder: (column) => column);
i0.GeneratedColumn<bool> get isPrivate =>
$composableBuilder(column: $table.isPrivate, builder: (column) => column);
i0.GeneratedColumnWithTypeConverter<i7.TabModeDbValue, int> get tabMode =>
$composableBuilder(column: $table.tabMode, builder: (column) => column);
i0.GeneratedColumn<String> get isolationContextId => $composableBuilder(
column: $table.isolationContextId,
builder: (column) => column,
);
i0.GeneratedColumn<bool> get isProbablyReaderable => $composableBuilder(
column: $table.isProbablyReaderable,
@@ -655,7 +675,7 @@ class $TabAnnotationComposer extends i0.Composer<i0.GeneratedDatabase, i3.Tab> {
final i3.$ContainerAnnotationComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.containerId,
referencedTable: i8.ReadDatabaseContainer(
referencedTable: i9.ReadDatabaseContainer(
$db,
).resultSet<i3.Container>('container'),
getReferencedColumn: (t) => t.id,
@@ -666,7 +686,7 @@ class $TabAnnotationComposer extends i0.Composer<i0.GeneratedDatabase, i3.Tab> {
$removeJoinBuilderFromRootComposer,
}) => i3.$ContainerAnnotationComposer(
$db: $db,
$table: i8.ReadDatabaseContainer(
$table: i9.ReadDatabaseContainer(
$db,
).resultSet<i3.Container>('container'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
@@ -714,7 +734,8 @@ class $TabTableManager
i0.Value<String> orderKey = const i0.Value.absent(),
i0.Value<Uri?> url = const i0.Value.absent(),
i0.Value<String?> title = const i0.Value.absent(),
i0.Value<bool?> isPrivate = const i0.Value.absent(),
i0.Value<i7.TabModeDbValue> tabMode = const i0.Value.absent(),
i0.Value<String?> isolationContextId = const i0.Value.absent(),
i0.Value<bool?> isProbablyReaderable = const i0.Value.absent(),
i0.Value<String?> extractedContentMarkdown =
const i0.Value.absent(),
@@ -732,7 +753,8 @@ class $TabTableManager
orderKey: orderKey,
url: url,
title: title,
isPrivate: isPrivate,
tabMode: tabMode,
isolationContextId: isolationContextId,
isProbablyReaderable: isProbablyReaderable,
extractedContentMarkdown: extractedContentMarkdown,
extractedContentPlain: extractedContentPlain,
@@ -750,7 +772,8 @@ class $TabTableManager
required String orderKey,
i0.Value<Uri?> url = const i0.Value.absent(),
i0.Value<String?> title = const i0.Value.absent(),
i0.Value<bool?> isPrivate = const i0.Value.absent(),
i0.Value<i7.TabModeDbValue> tabMode = const i0.Value.absent(),
i0.Value<String?> isolationContextId = const i0.Value.absent(),
i0.Value<bool?> isProbablyReaderable = const i0.Value.absent(),
i0.Value<String?> extractedContentMarkdown =
const i0.Value.absent(),
@@ -768,7 +791,8 @@ class $TabTableManager
orderKey: orderKey,
url: url,
title: title,
isPrivate: isPrivate,
tabMode: tabMode,
isolationContextId: isolationContextId,
isProbablyReaderable: isProbablyReaderable,
extractedContentMarkdown: extractedContentMarkdown,
extractedContentPlain: extractedContentPlain,
@@ -1269,14 +1293,25 @@ class Tab extends i0.Table with i0.TableInfo<Tab, i3.TabData> {
requiredDuringInsert: false,
$customConstraints: '',
);
late final i0.GeneratedColumn<bool> isPrivate = i0.GeneratedColumn<bool>(
'is_private',
late final i0.GeneratedColumnWithTypeConverter<i7.TabModeDbValue, int>
tabMode = i0.GeneratedColumn<int>(
'tab_mode',
aliasedName,
true,
type: i0.DriftSqlType.bool,
false,
type: i0.DriftSqlType.int,
requiredDuringInsert: false,
$customConstraints: '',
);
$customConstraints: 'NOT NULL DEFAULT 0',
defaultValue: const i0.CustomExpression('0'),
).withConverter<i7.TabModeDbValue>(i3.Tab.$convertertabMode);
late final i0.GeneratedColumn<String> isolationContextId =
i0.GeneratedColumn<String>(
'isolation_context_id',
aliasedName,
true,
type: i0.DriftSqlType.string,
requiredDuringInsert: false,
$customConstraints: '',
);
late final i0.GeneratedColumn<bool> isProbablyReaderable =
i0.GeneratedColumn<bool>(
'is_probably_readerable',
@@ -1340,7 +1375,8 @@ class Tab extends i0.Table with i0.TableInfo<Tab, i3.TabData> {
orderKey,
url,
title,
isPrivate,
tabMode,
isolationContextId,
isProbablyReaderable,
extractedContentMarkdown,
extractedContentPlain,
@@ -1391,9 +1427,15 @@ class Tab extends i0.Table with i0.TableInfo<Tab, i3.TabData> {
i0.DriftSqlType.string,
data['${effectivePrefix}title'],
),
isPrivate: attachedDatabase.typeMapping.read(
i0.DriftSqlType.bool,
data['${effectivePrefix}is_private'],
tabMode: i3.Tab.$convertertabMode.fromSql(
attachedDatabase.typeMapping.read(
i0.DriftSqlType.int,
data['${effectivePrefix}tab_mode'],
)!,
),
isolationContextId: attachedDatabase.typeMapping.read(
i0.DriftSqlType.string,
data['${effectivePrefix}isolation_context_id'],
),
isProbablyReaderable: attachedDatabase.typeMapping.read(
i0.DriftSqlType.bool,
@@ -1430,7 +1472,13 @@ class Tab extends i0.Table with i0.TableInfo<Tab, i3.TabData> {
static i0.JsonTypeConverter2<i6.TabSource, int, int> $convertersource =
const i0.EnumIndexConverter<i6.TabSource>(i6.TabSource.values);
static i0.TypeConverter<Uri?, String?> $converterurl =
const i7.UriConverterNullable();
const i8.UriConverterNullable();
static i0.JsonTypeConverter2<i7.TabModeDbValue, int, int> $convertertabMode =
const i0.EnumIndexConverter<i7.TabModeDbValue>(i7.TabModeDbValue.values);
@override
List<String> get customConstraints => const [
'CHECK((tab_mode = 2 AND isolation_context_id IS NOT NULL)OR(tab_mode != 2 AND isolation_context_id IS NULL))',
];
@override
bool get dontWriteConstraints => true;
}
@@ -1443,7 +1491,8 @@ class TabData extends i0.DataClass implements i0.Insertable<i3.TabData> {
final String orderKey;
final Uri? url;
final String? title;
final bool? isPrivate;
final i7.TabModeDbValue tabMode;
final String? isolationContextId;
final bool? isProbablyReaderable;
final String? extractedContentMarkdown;
final String? extractedContentPlain;
@@ -1458,7 +1507,8 @@ class TabData extends i0.DataClass implements i0.Insertable<i3.TabData> {
required this.orderKey,
this.url,
this.title,
this.isPrivate,
required this.tabMode,
this.isolationContextId,
this.isProbablyReaderable,
this.extractedContentMarkdown,
this.extractedContentPlain,
@@ -1486,8 +1536,13 @@ class TabData extends i0.DataClass implements i0.Insertable<i3.TabData> {
if (!nullToAbsent || title != null) {
map['title'] = i0.Variable<String>(title);
}
if (!nullToAbsent || isPrivate != null) {
map['is_private'] = i0.Variable<bool>(isPrivate);
{
map['tab_mode'] = i0.Variable<int>(
i3.Tab.$convertertabMode.toSql(tabMode),
);
}
if (!nullToAbsent || isolationContextId != null) {
map['isolation_context_id'] = i0.Variable<String>(isolationContextId);
}
if (!nullToAbsent || isProbablyReaderable != null) {
map['is_probably_readerable'] = i0.Variable<bool>(isProbablyReaderable);
@@ -1527,7 +1582,12 @@ class TabData extends i0.DataClass implements i0.Insertable<i3.TabData> {
orderKey: serializer.fromJson<String>(json['order_key']),
url: serializer.fromJson<Uri?>(json['url']),
title: serializer.fromJson<String?>(json['title']),
isPrivate: serializer.fromJson<bool?>(json['is_private']),
tabMode: i3.Tab.$convertertabMode.fromJson(
serializer.fromJson<int>(json['tab_mode']),
),
isolationContextId: serializer.fromJson<String?>(
json['isolation_context_id'],
),
isProbablyReaderable: serializer.fromJson<bool?>(
json['is_probably_readerable'],
),
@@ -1557,7 +1617,10 @@ class TabData extends i0.DataClass implements i0.Insertable<i3.TabData> {
'order_key': serializer.toJson<String>(orderKey),
'url': serializer.toJson<Uri?>(url),
'title': serializer.toJson<String?>(title),
'is_private': serializer.toJson<bool?>(isPrivate),
'tab_mode': serializer.toJson<int>(
i3.Tab.$convertertabMode.toJson(tabMode),
),
'isolation_context_id': serializer.toJson<String?>(isolationContextId),
'is_probably_readerable': serializer.toJson<bool?>(isProbablyReaderable),
'extracted_content_markdown': serializer.toJson<String?>(
extractedContentMarkdown,
@@ -1579,7 +1642,8 @@ class TabData extends i0.DataClass implements i0.Insertable<i3.TabData> {
String? orderKey,
i0.Value<Uri?> url = const i0.Value.absent(),
i0.Value<String?> title = const i0.Value.absent(),
i0.Value<bool?> isPrivate = const i0.Value.absent(),
i7.TabModeDbValue? tabMode,
i0.Value<String?> isolationContextId = const i0.Value.absent(),
i0.Value<bool?> isProbablyReaderable = const i0.Value.absent(),
i0.Value<String?> extractedContentMarkdown = const i0.Value.absent(),
i0.Value<String?> extractedContentPlain = const i0.Value.absent(),
@@ -1594,7 +1658,10 @@ class TabData extends i0.DataClass implements i0.Insertable<i3.TabData> {
orderKey: orderKey ?? this.orderKey,
url: url.present ? url.value : this.url,
title: title.present ? title.value : this.title,
isPrivate: isPrivate.present ? isPrivate.value : this.isPrivate,
tabMode: tabMode ?? this.tabMode,
isolationContextId: isolationContextId.present
? isolationContextId.value
: this.isolationContextId,
isProbablyReaderable: isProbablyReaderable.present
? isProbablyReaderable.value
: this.isProbablyReaderable,
@@ -1623,7 +1690,10 @@ class TabData extends i0.DataClass implements i0.Insertable<i3.TabData> {
orderKey: data.orderKey.present ? data.orderKey.value : this.orderKey,
url: data.url.present ? data.url.value : this.url,
title: data.title.present ? data.title.value : this.title,
isPrivate: data.isPrivate.present ? data.isPrivate.value : this.isPrivate,
tabMode: data.tabMode.present ? data.tabMode.value : this.tabMode,
isolationContextId: data.isolationContextId.present
? data.isolationContextId.value
: this.isolationContextId,
isProbablyReaderable: data.isProbablyReaderable.present
? data.isProbablyReaderable.value
: this.isProbablyReaderable,
@@ -1653,7 +1723,8 @@ class TabData extends i0.DataClass implements i0.Insertable<i3.TabData> {
..write('orderKey: $orderKey, ')
..write('url: $url, ')
..write('title: $title, ')
..write('isPrivate: $isPrivate, ')
..write('tabMode: $tabMode, ')
..write('isolationContextId: $isolationContextId, ')
..write('isProbablyReaderable: $isProbablyReaderable, ')
..write('extractedContentMarkdown: $extractedContentMarkdown, ')
..write('extractedContentPlain: $extractedContentPlain, ')
@@ -1673,7 +1744,8 @@ class TabData extends i0.DataClass implements i0.Insertable<i3.TabData> {
orderKey,
url,
title,
isPrivate,
tabMode,
isolationContextId,
isProbablyReaderable,
extractedContentMarkdown,
extractedContentPlain,
@@ -1692,7 +1764,8 @@ class TabData extends i0.DataClass implements i0.Insertable<i3.TabData> {
other.orderKey == this.orderKey &&
other.url == this.url &&
other.title == this.title &&
other.isPrivate == this.isPrivate &&
other.tabMode == this.tabMode &&
other.isolationContextId == this.isolationContextId &&
other.isProbablyReaderable == this.isProbablyReaderable &&
other.extractedContentMarkdown == this.extractedContentMarkdown &&
other.extractedContentPlain == this.extractedContentPlain &&
@@ -1709,7 +1782,8 @@ class TabCompanion extends i0.UpdateCompanion<i3.TabData> {
final i0.Value<String> orderKey;
final i0.Value<Uri?> url;
final i0.Value<String?> title;
final i0.Value<bool?> isPrivate;
final i0.Value<i7.TabModeDbValue> tabMode;
final i0.Value<String?> isolationContextId;
final i0.Value<bool?> isProbablyReaderable;
final i0.Value<String?> extractedContentMarkdown;
final i0.Value<String?> extractedContentPlain;
@@ -1725,7 +1799,8 @@ class TabCompanion extends i0.UpdateCompanion<i3.TabData> {
this.orderKey = const i0.Value.absent(),
this.url = const i0.Value.absent(),
this.title = const i0.Value.absent(),
this.isPrivate = const i0.Value.absent(),
this.tabMode = const i0.Value.absent(),
this.isolationContextId = const i0.Value.absent(),
this.isProbablyReaderable = const i0.Value.absent(),
this.extractedContentMarkdown = const i0.Value.absent(),
this.extractedContentPlain = const i0.Value.absent(),
@@ -1742,7 +1817,8 @@ class TabCompanion extends i0.UpdateCompanion<i3.TabData> {
required String orderKey,
this.url = const i0.Value.absent(),
this.title = const i0.Value.absent(),
this.isPrivate = const i0.Value.absent(),
this.tabMode = const i0.Value.absent(),
this.isolationContextId = const i0.Value.absent(),
this.isProbablyReaderable = const i0.Value.absent(),
this.extractedContentMarkdown = const i0.Value.absent(),
this.extractedContentPlain = const i0.Value.absent(),
@@ -1762,7 +1838,8 @@ class TabCompanion extends i0.UpdateCompanion<i3.TabData> {
i0.Expression<String>? orderKey,
i0.Expression<String>? url,
i0.Expression<String>? title,
i0.Expression<bool>? isPrivate,
i0.Expression<int>? tabMode,
i0.Expression<String>? isolationContextId,
i0.Expression<bool>? isProbablyReaderable,
i0.Expression<String>? extractedContentMarkdown,
i0.Expression<String>? extractedContentPlain,
@@ -1779,7 +1856,9 @@ class TabCompanion extends i0.UpdateCompanion<i3.TabData> {
if (orderKey != null) 'order_key': orderKey,
if (url != null) 'url': url,
if (title != null) 'title': title,
if (isPrivate != null) 'is_private': isPrivate,
if (tabMode != null) 'tab_mode': tabMode,
if (isolationContextId != null)
'isolation_context_id': isolationContextId,
if (isProbablyReaderable != null)
'is_probably_readerable': isProbablyReaderable,
if (extractedContentMarkdown != null)
@@ -1802,7 +1881,8 @@ class TabCompanion extends i0.UpdateCompanion<i3.TabData> {
i0.Value<String>? orderKey,
i0.Value<Uri?>? url,
i0.Value<String?>? title,
i0.Value<bool?>? isPrivate,
i0.Value<i7.TabModeDbValue>? tabMode,
i0.Value<String?>? isolationContextId,
i0.Value<bool?>? isProbablyReaderable,
i0.Value<String?>? extractedContentMarkdown,
i0.Value<String?>? extractedContentPlain,
@@ -1819,7 +1899,8 @@ class TabCompanion extends i0.UpdateCompanion<i3.TabData> {
orderKey: orderKey ?? this.orderKey,
url: url ?? this.url,
title: title ?? this.title,
isPrivate: isPrivate ?? this.isPrivate,
tabMode: tabMode ?? this.tabMode,
isolationContextId: isolationContextId ?? this.isolationContextId,
isProbablyReaderable: isProbablyReaderable ?? this.isProbablyReaderable,
extractedContentMarkdown:
extractedContentMarkdown ?? this.extractedContentMarkdown,
@@ -1858,8 +1939,15 @@ class TabCompanion extends i0.UpdateCompanion<i3.TabData> {
if (title.present) {
map['title'] = i0.Variable<String>(title.value);
}
if (isPrivate.present) {
map['is_private'] = i0.Variable<bool>(isPrivate.value);
if (tabMode.present) {
map['tab_mode'] = i0.Variable<int>(
i3.Tab.$convertertabMode.toSql(tabMode.value),
);
}
if (isolationContextId.present) {
map['isolation_context_id'] = i0.Variable<String>(
isolationContextId.value,
);
}
if (isProbablyReaderable.present) {
map['is_probably_readerable'] = i0.Variable<bool>(
@@ -1903,7 +1991,8 @@ class TabCompanion extends i0.UpdateCompanion<i3.TabData> {
..write('orderKey: $orderKey, ')
..write('url: $url, ')
..write('title: $title, ')
..write('isPrivate: $isPrivate, ')
..write('tabMode: $tabMode, ')
..write('isolationContextId: $isolationContextId, ')
..write('isProbablyReaderable: $isProbablyReaderable, ')
..write('extractedContentMarkdown: $extractedContentMarkdown, ')
..write('extractedContentPlain: $extractedContentPlain, ')
@@ -2211,7 +2300,7 @@ i0.Trigger get tabAfterUpdate => i0.Trigger(
'tab_after_update',
);
class DefinitionsDrift extends i8.ModularAccessor {
class DefinitionsDrift extends i9.ModularAccessor {
DefinitionsDrift(i0.GeneratedDatabase db) : super(db);
Future<int> optimizeFtsIndex() {
return customInsert(
@@ -2284,19 +2373,19 @@ class DefinitionsDrift extends i8.ModularAccessor {
).map((i0.QueryRow row) => row.read<String>('_c0'));
}
i0.Selectable<i9.TabQueryResult> queryTabsBasic({
i0.Selectable<i10.TabQueryResult> queryTabsBasic({
required String query,
required int limit,
}) {
return customSelect(
'WITH weights AS (SELECT 10.0 AS title_weight, 5.0 AS url_weight) SELECT t.id, t.container_id, t.is_private, t.title, CAST(t.url AS TEXT) AS url, t.url AS clean_url, bm25(tab_fts, weights.title_weight, weights.url_weight) AS weighted_rank FROM tab_fts AS fts INNER JOIN tab AS t ON t."rowid" = fts."rowid" CROSS JOIN weights WHERE fts.title LIKE ?1 OR fts.url LIKE ?1 ORDER BY weighted_rank ASC, t.timestamp DESC LIMIT ?2',
'WITH weights AS (SELECT 10.0 AS title_weight, 5.0 AS url_weight) SELECT t.id, t.container_id, t.tab_mode, t.title, CAST(t.url AS TEXT) AS url, t.url AS clean_url, bm25(tab_fts, weights.title_weight, weights.url_weight) AS weighted_rank FROM tab_fts AS fts INNER JOIN tab AS t ON t."rowid" = fts."rowid" CROSS JOIN weights WHERE fts.title LIKE ?1 OR fts.url LIKE ?1 ORDER BY weighted_rank ASC, t.timestamp DESC LIMIT ?2',
variables: [i0.Variable<String>(query), i0.Variable<int>(limit)],
readsFrom: {tab, tabFts},
).map(
(i0.QueryRow row) => i9.TabQueryResult(
(i0.QueryRow row) => i10.TabQueryResult(
id: row.read<String>('id'),
containerId: row.readNullable<String>('container_id'),
isPrivate: row.readNullable<bool>('is_private'),
tabMode: i3.Tab.$convertertabMode.fromSql(row.read<int>('tab_mode')),
title: row.readNullable<String>('title'),
url: row.readNullable<String>('url'),
cleanUrl: i3.Tab.$converterurl.fromSql(
@@ -2307,7 +2396,7 @@ class DefinitionsDrift extends i8.ModularAccessor {
);
}
i0.Selectable<i9.TabQueryResult> queryTabsFullContent({
i0.Selectable<i10.TabQueryResult> queryTabsFullContent({
required String beforeMatch,
required String afterMatch,
required String ellipsis,
@@ -2316,7 +2405,7 @@ class DefinitionsDrift extends i8.ModularAccessor {
required int limit,
}) {
return customSelect(
'WITH weights AS (SELECT 10.0 AS title_weight, 5.0 AS url_weight, 3.0 AS extracted_weight, 1.0 AS full_weight) SELECT t.id, t.container_id, t.is_private, highlight(tab_fts, 0, ?1, ?2) AS title, highlight(tab_fts, 1, ?1, ?2) AS url, snippet(tab_fts, 2, ?1, ?2, ?3, ?4) AS extracted_content, snippet(tab_fts, 3, ?1, ?2, ?3, ?4) AS full_content, t.url AS clean_url,(bm25(tab_fts, weights.title_weight, weights.url_weight, weights.extracted_weight, weights.full_weight))AS weighted_rank FROM tab_fts(?5)AS fts INNER JOIN tab AS t ON t."rowid" = fts."rowid" CROSS JOIN weights ORDER BY weighted_rank ASC, t.timestamp DESC LIMIT ?6',
'WITH weights AS (SELECT 10.0 AS title_weight, 5.0 AS url_weight, 3.0 AS extracted_weight, 1.0 AS full_weight) SELECT t.id, t.container_id, t.tab_mode, highlight(tab_fts, 0, ?1, ?2) AS title, highlight(tab_fts, 1, ?1, ?2) AS url, snippet(tab_fts, 2, ?1, ?2, ?3, ?4) AS extracted_content, snippet(tab_fts, 3, ?1, ?2, ?3, ?4) AS full_content, t.url AS clean_url,(bm25(tab_fts, weights.title_weight, weights.url_weight, weights.extracted_weight, weights.full_weight))AS weighted_rank FROM tab_fts(?5)AS fts INNER JOIN tab AS t ON t."rowid" = fts."rowid" CROSS JOIN weights ORDER BY weighted_rank ASC, t.timestamp DESC LIMIT ?6',
variables: [
i0.Variable<String>(beforeMatch),
i0.Variable<String>(afterMatch),
@@ -2327,10 +2416,10 @@ class DefinitionsDrift extends i8.ModularAccessor {
],
readsFrom: {tab, tabFts},
).map(
(i0.QueryRow row) => i9.TabQueryResult(
(i0.QueryRow row) => i10.TabQueryResult(
id: row.read<String>('id'),
containerId: row.readNullable<String>('container_id'),
isPrivate: row.readNullable<bool>('is_private'),
tabMode: i3.Tab.$convertertabMode.fromSql(row.read<int>('tab_mode')),
title: row.readNullable<String>('title'),
url: row.readNullable<String>('url'),
cleanUrl: i3.Tab.$converterurl.fromSql(
@@ -2443,13 +2532,13 @@ class DefinitionsDrift extends i8.ModularAccessor {
).map((i0.QueryRow row) => row.read<String>('id'));
}
i0.Selectable<i10.SiteAssignment> allAssignedSites() {
i0.Selectable<i11.SiteAssignment> allAssignedSites() {
return customSelect(
'SELECT container.id, COALESCE(container.metadata ->> \'\$.contextualIdentity\', \'general\') AS contextualIdentity, value AS assigned_site FROM container CROSS JOIN json_each(container.metadata, \'\$.assignedSites\')WHERE value IS NOT NULL',
variables: [],
readsFrom: {container},
).map(
(i0.QueryRow row) => i10.SiteAssignment(
(i0.QueryRow row) => i11.SiteAssignment(
id: row.read<String>('id'),
contextualIdentity: row.read<String>('contextualIdentity'),
assignedSite: row.readNullable<String>('assigned_site'),
@@ -2465,14 +2554,46 @@ class DefinitionsDrift extends i8.ModularAccessor {
).map((i0.QueryRow row) => row.readNullable<String>('contextual_identity'));
}
i3.TabFts get tabFts => i8.ReadDatabaseContainer(
i0.Selectable<int> tabsInIsolationGroup({String? contextId}) {
return customSelect(
'SELECT COUNT(*) AS count FROM tab WHERE isolation_context_id = ?1',
variables: [i0.Variable<String>(contextId)],
readsFrom: {tab},
).map((i0.QueryRow row) => row.read<int>('count'));
}
i0.Selectable<String?> allIsolationContextIds() {
return customSelect(
'SELECT DISTINCT isolation_context_id FROM tab WHERE isolation_context_id IS NOT NULL',
variables: [],
readsFrom: {tab},
).map(
(i0.QueryRow row) => row.readNullable<String>('isolation_context_id'),
);
}
i0.Selectable<IsolatedContextContainerPairsResult>
isolatedContextContainerPairs() {
return customSelect(
'SELECT DISTINCT t.isolation_context_id, t.container_id FROM tab AS t WHERE t.tab_mode = 2 AND t.isolation_context_id IS NOT NULL AND t.container_id IS NOT NULL',
variables: [],
readsFrom: {tab},
).map(
(i0.QueryRow row) => IsolatedContextContainerPairsResult(
isolationContextId: row.readNullable<String>('isolation_context_id'),
containerId: row.readNullable<String>('container_id'),
),
);
}
i3.TabFts get tabFts => i9.ReadDatabaseContainer(
attachedDatabase,
).resultSet<i3.TabFts>('tab_fts');
i3.Container get container => i8.ReadDatabaseContainer(
i3.Container get container => i9.ReadDatabaseContainer(
attachedDatabase,
).resultSet<i3.Container>('container');
i3.Tab get tab =>
i8.ReadDatabaseContainer(attachedDatabase).resultSet<i3.Tab>('tab');
i9.ReadDatabaseContainer(attachedDatabase).resultSet<i3.Tab>('tab');
}
class TabTreesResult {
@@ -2493,3 +2614,12 @@ class UnorderedTabDescendantsResult {
final String? parentId;
UnorderedTabDescendantsResult({required this.id, this.parentId});
}
class IsolatedContextContainerPairsResult {
final String? isolationContextId;
final String? containerId;
IsolatedContextContainerPairsResult({
this.isolationContextId,
this.containerId,
});
}
@@ -0,0 +1,34 @@
/*
* 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:weblibre/core/uuid.dart';
const _isolatedPrefix = 'iso1_';
/// Generates a new unique isolation context ID in the format `iso1_<uuid-v4>`.
String newIsolatedContextId() {
final id = uuid.v4();
return '$_isolatedPrefix$id';
}
/// Returns `true` if the given context ID identifies an isolated tab context.
bool isIsolatedContextId(String? contextId) {
if (contextId == null) return false;
return contextId.startsWith(_isolatedPrefix);
}
@@ -0,0 +1,105 @@
/*
* 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:weblibre/core/routing/routes.dart';
import 'package:weblibre/features/geckoview/features/tabs/data/entities/isolation_context.dart';
/// Persisted tab privacy/isolation mode.
///
/// Values map to integer values stored in the `tab_mode` column:
/// - 0 = regular
/// - 1 = private
/// - 2 = isolated
enum TabModeDbValue { regular, private, isolated }
sealed class TabMode {
static const TabMode regular = RegularTabMode();
static const TabMode private = PrivateTabMode();
const TabMode();
factory TabMode.isolated(String isolationContextId) =>
IsolatedTabMode(isolationContextId);
factory TabMode.newIsolated() => IsolatedTabMode(newIsolatedContextId());
factory TabMode.fromTabType(TabType tabType) => switch (tabType) {
TabType.private => TabMode.private,
TabType.isolated => TabMode.newIsolated(),
_ => TabMode.regular,
};
TabModeDbValue toDbValue() => switch (this) {
RegularTabMode() => TabModeDbValue.regular,
PrivateTabMode() => TabModeDbValue.private,
IsolatedTabMode() => TabModeDbValue.isolated,
};
String? get isolationContextId => switch (this) {
IsolatedTabMode(:final isolationContextId) => isolationContextId,
_ => null,
};
TabType toTabType() => switch (this) {
RegularTabMode() => TabType.regular,
PrivateTabMode() => TabType.private,
IsolatedTabMode() => TabType.isolated,
};
factory TabMode.fromDbValue(
TabModeDbValue dbValue, {
required String? isolationContextId,
}) {
return switch (dbValue) {
TabModeDbValue.regular => regular,
TabModeDbValue.private => private,
TabModeDbValue.isolated when isolationContextId != null =>
TabMode.isolated(isolationContextId),
TabModeDbValue.isolated => regular,
};
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is TabMode &&
other.toDbValue() == toDbValue() &&
other.isolationContextId == isolationContextId;
}
@override
int get hashCode => Object.hash(toDbValue(), isolationContextId);
}
final class RegularTabMode extends TabMode {
const RegularTabMode();
}
final class PrivateTabMode extends TabMode {
const PrivateTabMode();
}
final class IsolatedTabMode extends TabMode {
@override
final String isolationContextId;
const IsolatedTabMode(this.isolationContextId);
}
@@ -18,11 +18,12 @@
* 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/tabs/data/entities/tab_mode.dart';
class TabQueryResult with FastEquatable {
final String id;
final String? containerId;
final bool? isPrivate;
final TabModeDbValue tabMode;
final String? title;
final Uri? cleanUrl;
@@ -36,7 +37,7 @@ class TabQueryResult with FastEquatable {
TabQueryResult({
required this.id,
required this.containerId,
required this.isPrivate,
required this.tabMode,
required this.title,
required this.url,
required this.cleanUrl,
@@ -49,7 +50,7 @@ class TabQueryResult with FastEquatable {
List<Object?> get hashParameters => [
id,
containerId,
isPrivate,
tabMode,
title,
cleanUrl,
url,