add isolated tab feature
This commit is contained in:
@@ -0,0 +1,457 @@
|
|||||||
|
{
|
||||||
|
"_meta": {
|
||||||
|
"description": "This file contains a serialized version of schema entities for drift.",
|
||||||
|
"version": "1.3.0"
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"store_date_time_values_as_text": false
|
||||||
|
},
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"id": 0,
|
||||||
|
"references": [],
|
||||||
|
"type": "table",
|
||||||
|
"data": {
|
||||||
|
"name": "container",
|
||||||
|
"was_declared_in_moor": true,
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"getter_name": "id",
|
||||||
|
"moor_type": "string",
|
||||||
|
"nullable": false,
|
||||||
|
"customConstraints": "PRIMARY KEY NOT NULL",
|
||||||
|
"default_dart": null,
|
||||||
|
"default_client_dart": null,
|
||||||
|
"dsl_features": [
|
||||||
|
"primary-key"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"getter_name": "name",
|
||||||
|
"moor_type": "string",
|
||||||
|
"nullable": true,
|
||||||
|
"customConstraints": "",
|
||||||
|
"default_dart": null,
|
||||||
|
"default_client_dart": null,
|
||||||
|
"dsl_features": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "color",
|
||||||
|
"getter_name": "color",
|
||||||
|
"moor_type": "int",
|
||||||
|
"nullable": false,
|
||||||
|
"customConstraints": "NOT NULL",
|
||||||
|
"default_dart": null,
|
||||||
|
"default_client_dart": null,
|
||||||
|
"dsl_features": [],
|
||||||
|
"type_converter": {
|
||||||
|
"dart_expr": "const ColorConverter()",
|
||||||
|
"dart_type_name": "Color"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "metadata",
|
||||||
|
"getter_name": "metadata",
|
||||||
|
"moor_type": "string",
|
||||||
|
"nullable": true,
|
||||||
|
"customConstraints": "",
|
||||||
|
"default_dart": null,
|
||||||
|
"default_client_dart": null,
|
||||||
|
"dsl_features": [],
|
||||||
|
"type_converter": {
|
||||||
|
"dart_expr": "const ContainerMetadataConverter()",
|
||||||
|
"dart_type_name": "ContainerMetadata"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_virtual": false,
|
||||||
|
"without_rowid": false,
|
||||||
|
"constraints": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"references": [
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"type": "table",
|
||||||
|
"data": {
|
||||||
|
"name": "tab",
|
||||||
|
"was_declared_in_moor": true,
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"getter_name": "id",
|
||||||
|
"moor_type": "string",
|
||||||
|
"nullable": false,
|
||||||
|
"customConstraints": "PRIMARY KEY NOT NULL",
|
||||||
|
"default_dart": null,
|
||||||
|
"default_client_dart": null,
|
||||||
|
"dsl_features": [
|
||||||
|
"primary-key"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "source",
|
||||||
|
"getter_name": "source",
|
||||||
|
"moor_type": "int",
|
||||||
|
"nullable": false,
|
||||||
|
"customConstraints": "NOT NULL",
|
||||||
|
"default_dart": null,
|
||||||
|
"default_client_dart": null,
|
||||||
|
"dsl_features": [],
|
||||||
|
"type_converter": {
|
||||||
|
"dart_expr": "const EnumIndexConverter<TabSource>(TabSource.values)",
|
||||||
|
"dart_type_name": "TabSource"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "parent_id",
|
||||||
|
"getter_name": "parentId",
|
||||||
|
"moor_type": "string",
|
||||||
|
"nullable": true,
|
||||||
|
"customConstraints": "REFERENCES tab(id)ON DELETE SET NULL",
|
||||||
|
"default_dart": null,
|
||||||
|
"default_client_dart": null,
|
||||||
|
"dsl_features": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "container_id",
|
||||||
|
"getter_name": "containerId",
|
||||||
|
"moor_type": "string",
|
||||||
|
"nullable": true,
|
||||||
|
"customConstraints": "REFERENCES container(id)ON DELETE CASCADE",
|
||||||
|
"default_dart": null,
|
||||||
|
"default_client_dart": null,
|
||||||
|
"dsl_features": [
|
||||||
|
{
|
||||||
|
"foreign_key": {
|
||||||
|
"to": {
|
||||||
|
"table": "container",
|
||||||
|
"column": "id"
|
||||||
|
},
|
||||||
|
"initially_deferred": false,
|
||||||
|
"on_update": null,
|
||||||
|
"on_delete": "cascade"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "order_key",
|
||||||
|
"getter_name": "orderKey",
|
||||||
|
"moor_type": "string",
|
||||||
|
"nullable": false,
|
||||||
|
"customConstraints": "NOT NULL",
|
||||||
|
"default_dart": null,
|
||||||
|
"default_client_dart": null,
|
||||||
|
"dsl_features": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "url",
|
||||||
|
"getter_name": "url",
|
||||||
|
"moor_type": "string",
|
||||||
|
"nullable": true,
|
||||||
|
"customConstraints": "",
|
||||||
|
"default_dart": null,
|
||||||
|
"default_client_dart": null,
|
||||||
|
"dsl_features": [],
|
||||||
|
"type_converter": {
|
||||||
|
"dart_expr": "const UriConverterNullable()",
|
||||||
|
"dart_type_name": "Uri?"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "title",
|
||||||
|
"getter_name": "title",
|
||||||
|
"moor_type": "string",
|
||||||
|
"nullable": true,
|
||||||
|
"customConstraints": "",
|
||||||
|
"default_dart": null,
|
||||||
|
"default_client_dart": null,
|
||||||
|
"dsl_features": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "tab_mode",
|
||||||
|
"getter_name": "tabMode",
|
||||||
|
"moor_type": "int",
|
||||||
|
"nullable": false,
|
||||||
|
"customConstraints": "NOT NULL DEFAULT 0",
|
||||||
|
"default_dart": "const CustomExpression('0')",
|
||||||
|
"default_client_dart": null,
|
||||||
|
"dsl_features": [],
|
||||||
|
"type_converter": {
|
||||||
|
"dart_expr": "const EnumIndexConverter<TabModeDbValue>(TabModeDbValue.values)",
|
||||||
|
"dart_type_name": "TabModeDbValue"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "isolation_context_id",
|
||||||
|
"getter_name": "isolationContextId",
|
||||||
|
"moor_type": "string",
|
||||||
|
"nullable": true,
|
||||||
|
"customConstraints": "",
|
||||||
|
"default_dart": null,
|
||||||
|
"default_client_dart": null,
|
||||||
|
"dsl_features": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "is_probably_readerable",
|
||||||
|
"getter_name": "isProbablyReaderable",
|
||||||
|
"moor_type": "bool",
|
||||||
|
"nullable": true,
|
||||||
|
"customConstraints": "",
|
||||||
|
"default_dart": null,
|
||||||
|
"default_client_dart": null,
|
||||||
|
"dsl_features": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "extracted_content_markdown",
|
||||||
|
"getter_name": "extractedContentMarkdown",
|
||||||
|
"moor_type": "string",
|
||||||
|
"nullable": true,
|
||||||
|
"customConstraints": "",
|
||||||
|
"default_dart": null,
|
||||||
|
"default_client_dart": null,
|
||||||
|
"dsl_features": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "extracted_content_plain",
|
||||||
|
"getter_name": "extractedContentPlain",
|
||||||
|
"moor_type": "string",
|
||||||
|
"nullable": true,
|
||||||
|
"customConstraints": "",
|
||||||
|
"default_dart": null,
|
||||||
|
"default_client_dart": null,
|
||||||
|
"dsl_features": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "full_content_markdown",
|
||||||
|
"getter_name": "fullContentMarkdown",
|
||||||
|
"moor_type": "string",
|
||||||
|
"nullable": true,
|
||||||
|
"customConstraints": "",
|
||||||
|
"default_dart": null,
|
||||||
|
"default_client_dart": null,
|
||||||
|
"dsl_features": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "full_content_plain",
|
||||||
|
"getter_name": "fullContentPlain",
|
||||||
|
"moor_type": "string",
|
||||||
|
"nullable": true,
|
||||||
|
"customConstraints": "",
|
||||||
|
"default_dart": null,
|
||||||
|
"default_client_dart": null,
|
||||||
|
"dsl_features": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "timestamp",
|
||||||
|
"getter_name": "timestamp",
|
||||||
|
"moor_type": "dateTime",
|
||||||
|
"nullable": false,
|
||||||
|
"customConstraints": "NOT NULL",
|
||||||
|
"default_dart": null,
|
||||||
|
"default_client_dart": null,
|
||||||
|
"dsl_features": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_virtual": false,
|
||||||
|
"without_rowid": false,
|
||||||
|
"constraints": [
|
||||||
|
"CHECK((tab_mode = 2 AND isolation_context_id IS NOT NULL)OR(tab_mode != 2 AND isolation_context_id IS NULL))"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"references": [
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"type": "table",
|
||||||
|
"data": {
|
||||||
|
"name": "tab_fts",
|
||||||
|
"was_declared_in_moor": true,
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "title",
|
||||||
|
"getter_name": "title",
|
||||||
|
"moor_type": "string",
|
||||||
|
"nullable": false,
|
||||||
|
"customConstraints": "",
|
||||||
|
"default_dart": null,
|
||||||
|
"default_client_dart": null,
|
||||||
|
"dsl_features": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "url",
|
||||||
|
"getter_name": "url",
|
||||||
|
"moor_type": "string",
|
||||||
|
"nullable": false,
|
||||||
|
"customConstraints": "",
|
||||||
|
"default_dart": null,
|
||||||
|
"default_client_dart": null,
|
||||||
|
"dsl_features": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "extracted_content_plain",
|
||||||
|
"getter_name": "extractedContentPlain",
|
||||||
|
"moor_type": "string",
|
||||||
|
"nullable": false,
|
||||||
|
"customConstraints": "",
|
||||||
|
"default_dart": null,
|
||||||
|
"default_client_dart": null,
|
||||||
|
"dsl_features": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "full_content_plain",
|
||||||
|
"getter_name": "fullContentPlain",
|
||||||
|
"moor_type": "string",
|
||||||
|
"nullable": false,
|
||||||
|
"customConstraints": "",
|
||||||
|
"default_dart": null,
|
||||||
|
"default_client_dart": null,
|
||||||
|
"dsl_features": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_virtual": true,
|
||||||
|
"create_virtual_stmt": "CREATE VIRTUAL TABLE \"tab_fts\" USING fts5(title, url, extracted_content_plain, full_content_plain, content=tab, tokenize=\"trigram\")",
|
||||||
|
"without_rowid": false,
|
||||||
|
"constraints": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"references": [
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"type": "trigger",
|
||||||
|
"data": {
|
||||||
|
"on": 1,
|
||||||
|
"references_in_body": [
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"name": "tab_maintain_parent_chain_on_delete",
|
||||||
|
"sql": "CREATE TRIGGER tab_maintain_parent_chain_on_delete BEFORE DELETE ON tab BEGIN\n -- Update all children of the deleted row to point to its parent (grandparent)\n -- Only if the grandparent exists, otherwise set to NULL\n UPDATE tab\n SET parent_id = CASE\n WHEN OLD.parent_id IS NOT NULL AND\n EXISTS(SELECT 1 FROM tab WHERE id = OLD.parent_id)\n THEN OLD.parent_id\n ELSE NULL\n END\n WHERE parent_id = OLD.id;\nEND;"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"references": [
|
||||||
|
1,
|
||||||
|
2
|
||||||
|
],
|
||||||
|
"type": "trigger",
|
||||||
|
"data": {
|
||||||
|
"on": 1,
|
||||||
|
"references_in_body": [
|
||||||
|
1,
|
||||||
|
2
|
||||||
|
],
|
||||||
|
"name": "tab_after_insert",
|
||||||
|
"sql": "CREATE TRIGGER tab_after_insert AFTER INSERT ON tab BEGIN\n INSERT INTO \n tab_fts(rowid, title, url, extracted_content_plain, full_content_plain) \n VALUES (new.rowid, new.title, new.url, new.extracted_content_plain, new.full_content_plain);\nEND;"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"references": [
|
||||||
|
1,
|
||||||
|
2
|
||||||
|
],
|
||||||
|
"type": "trigger",
|
||||||
|
"data": {
|
||||||
|
"on": 1,
|
||||||
|
"references_in_body": [
|
||||||
|
1,
|
||||||
|
2
|
||||||
|
],
|
||||||
|
"name": "tab_after_delete",
|
||||||
|
"sql": "CREATE TRIGGER tab_after_delete AFTER DELETE ON tab BEGIN\n INSERT INTO \n tab_fts(tab_fts, rowid, title, url, extracted_content_plain, full_content_plain) \n VALUES('delete', old.rowid, old.title, old.url, old.extracted_content_plain, old.full_content_plain);\nEND;"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"references": [
|
||||||
|
1,
|
||||||
|
2
|
||||||
|
],
|
||||||
|
"type": "trigger",
|
||||||
|
"data": {
|
||||||
|
"on": 1,
|
||||||
|
"references_in_body": [
|
||||||
|
1,
|
||||||
|
2
|
||||||
|
],
|
||||||
|
"name": "tab_after_update",
|
||||||
|
"sql": "CREATE TRIGGER tab_after_update AFTER UPDATE ON tab BEGIN\n INSERT INTO \n tab_fts(tab_fts, rowid, title, url, extracted_content_plain, full_content_plain) \n VALUES('delete', old.rowid, old.title, old.url, old.extracted_content_plain, old.full_content_plain);\n INSERT INTO \n tab_fts(rowid, title, url, extracted_content_plain, full_content_plain) \n VALUES (new.rowid, new.title, new.url, new.extracted_content_plain, new.full_content_plain);\nEND;"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fixed_sql": [
|
||||||
|
{
|
||||||
|
"name": "container",
|
||||||
|
"sql": [
|
||||||
|
{
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"sql": "CREATE TABLE IF NOT EXISTS \"container\" (\"id\" TEXT PRIMARY KEY NOT NULL, \"name\" TEXT, \"color\" INTEGER NOT NULL, \"metadata\" TEXT);"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "tab",
|
||||||
|
"sql": [
|
||||||
|
{
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"sql": "CREATE TABLE IF NOT EXISTS \"tab\" (\"id\" TEXT PRIMARY KEY NOT NULL, \"source\" INTEGER NOT NULL, \"parent_id\" TEXT REFERENCES tab(id)ON DELETE SET NULL, \"container_id\" TEXT REFERENCES container(id)ON DELETE CASCADE, \"order_key\" TEXT NOT NULL, \"url\" TEXT, \"title\" TEXT, \"tab_mode\" INTEGER NOT NULL DEFAULT 0, \"isolation_context_id\" TEXT, \"is_probably_readerable\" INTEGER, \"extracted_content_markdown\" TEXT, \"extracted_content_plain\" TEXT, \"full_content_markdown\" TEXT, \"full_content_plain\" TEXT, \"timestamp\" INTEGER NOT NULL, CHECK((tab_mode = 2 AND isolation_context_id IS NOT NULL)OR(tab_mode != 2 AND isolation_context_id IS NULL)));"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "tab_fts",
|
||||||
|
"sql": [
|
||||||
|
{
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"sql": "CREATE VIRTUAL TABLE IF NOT EXISTS \"tab_fts\" USING fts5(title, url, extracted_content_plain, full_content_plain, content=tab, tokenize=\"trigram\");"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "tab_maintain_parent_chain_on_delete",
|
||||||
|
"sql": [
|
||||||
|
{
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"sql": "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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "tab_after_insert",
|
||||||
|
"sql": [
|
||||||
|
{
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"sql": "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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "tab_after_delete",
|
||||||
|
"sql": [
|
||||||
|
{
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"sql": "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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "tab_after_update",
|
||||||
|
"sql": [
|
||||||
|
{
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"sql": "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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -27,6 +27,10 @@ class AppColors extends ThemeExtension<AppColors> {
|
|||||||
this.privateTabBackground = const Color(0xFF25003E),
|
this.privateTabBackground = const Color(0xFF25003E),
|
||||||
this.privateTabForeground = const Color(0xFFFFFFFF),
|
this.privateTabForeground = const Color(0xFFFFFFFF),
|
||||||
this.privateSelectionOverlay = const Color(0x648000D7),
|
this.privateSelectionOverlay = const Color(0x648000D7),
|
||||||
|
this.isolatedTabTeal = const Color(0xFF00897B),
|
||||||
|
this.isolatedTabBackground = const Color(0xFF003D36),
|
||||||
|
this.isolatedTabForeground = const Color(0xFFFFFFFF),
|
||||||
|
this.isolatedSelectionOverlay = const Color(0x6400897B),
|
||||||
this.torPurple = const Color(0xFF7D4698),
|
this.torPurple = const Color(0xFF7D4698),
|
||||||
this.torActiveGreen = const Color(0xFF68B030),
|
this.torActiveGreen = const Color(0xFF68B030),
|
||||||
this.torBackgroundGrey = const Color(0xFF333A41),
|
this.torBackgroundGrey = const Color(0xFF333A41),
|
||||||
@@ -38,6 +42,10 @@ class AppColors extends ThemeExtension<AppColors> {
|
|||||||
final Color privateTabBackground;
|
final Color privateTabBackground;
|
||||||
final Color privateTabForeground;
|
final Color privateTabForeground;
|
||||||
final Color privateSelectionOverlay;
|
final Color privateSelectionOverlay;
|
||||||
|
final Color isolatedTabTeal;
|
||||||
|
final Color isolatedTabBackground;
|
||||||
|
final Color isolatedTabForeground;
|
||||||
|
final Color isolatedSelectionOverlay;
|
||||||
final Color torPurple;
|
final Color torPurple;
|
||||||
final Color torActiveGreen;
|
final Color torActiveGreen;
|
||||||
final Color torBackgroundGrey;
|
final Color torBackgroundGrey;
|
||||||
@@ -53,6 +61,10 @@ class AppColors extends ThemeExtension<AppColors> {
|
|||||||
Color? privateTabBackground,
|
Color? privateTabBackground,
|
||||||
Color? privateTabForeground,
|
Color? privateTabForeground,
|
||||||
Color? privateSelectionOverlay,
|
Color? privateSelectionOverlay,
|
||||||
|
Color? isolatedTabTeal,
|
||||||
|
Color? isolatedTabBackground,
|
||||||
|
Color? isolatedTabForeground,
|
||||||
|
Color? isolatedSelectionOverlay,
|
||||||
Color? torPurple,
|
Color? torPurple,
|
||||||
Color? torActiveGreen,
|
Color? torActiveGreen,
|
||||||
Color? torBackgroundGrey,
|
Color? torBackgroundGrey,
|
||||||
@@ -65,6 +77,13 @@ class AppColors extends ThemeExtension<AppColors> {
|
|||||||
privateTabForeground: privateTabForeground ?? this.privateTabForeground,
|
privateTabForeground: privateTabForeground ?? this.privateTabForeground,
|
||||||
privateSelectionOverlay:
|
privateSelectionOverlay:
|
||||||
privateSelectionOverlay ?? this.privateSelectionOverlay,
|
privateSelectionOverlay ?? this.privateSelectionOverlay,
|
||||||
|
isolatedTabTeal: isolatedTabTeal ?? this.isolatedTabTeal,
|
||||||
|
isolatedTabBackground:
|
||||||
|
isolatedTabBackground ?? this.isolatedTabBackground,
|
||||||
|
isolatedTabForeground:
|
||||||
|
isolatedTabForeground ?? this.isolatedTabForeground,
|
||||||
|
isolatedSelectionOverlay:
|
||||||
|
isolatedSelectionOverlay ?? this.isolatedSelectionOverlay,
|
||||||
torPurple: torPurple ?? this.torPurple,
|
torPurple: torPurple ?? this.torPurple,
|
||||||
torActiveGreen: torActiveGreen ?? this.torActiveGreen,
|
torActiveGreen: torActiveGreen ?? this.torActiveGreen,
|
||||||
torBackgroundGrey: torBackgroundGrey ?? this.torBackgroundGrey,
|
torBackgroundGrey: torBackgroundGrey ?? this.torBackgroundGrey,
|
||||||
@@ -100,6 +119,22 @@ class AppColors extends ThemeExtension<AppColors> {
|
|||||||
other.privateSelectionOverlay,
|
other.privateSelectionOverlay,
|
||||||
t,
|
t,
|
||||||
)!,
|
)!,
|
||||||
|
isolatedTabTeal: Color.lerp(isolatedTabTeal, other.isolatedTabTeal, t)!,
|
||||||
|
isolatedTabBackground: Color.lerp(
|
||||||
|
isolatedTabBackground,
|
||||||
|
other.isolatedTabBackground,
|
||||||
|
t,
|
||||||
|
)!,
|
||||||
|
isolatedTabForeground: Color.lerp(
|
||||||
|
isolatedTabForeground,
|
||||||
|
other.isolatedTabForeground,
|
||||||
|
t,
|
||||||
|
)!,
|
||||||
|
isolatedSelectionOverlay: Color.lerp(
|
||||||
|
isolatedSelectionOverlay,
|
||||||
|
other.isolatedSelectionOverlay,
|
||||||
|
t,
|
||||||
|
)!,
|
||||||
torPurple: Color.lerp(torPurple, other.torPurple, t)!,
|
torPurple: Color.lerp(torPurple, other.torPurple, t)!,
|
||||||
torActiveGreen: Color.lerp(torActiveGreen, other.torActiveGreen, t)!,
|
torActiveGreen: Color.lerp(torActiveGreen, other.torActiveGreen, t)!,
|
||||||
torBackgroundGrey: Color.lerp(
|
torBackgroundGrey: Color.lerp(
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ class BrowserRoute extends GoRouteData with $BrowserRoute {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum TabType { regular, private, child }
|
enum TabType { regular, private, child, isolated }
|
||||||
|
|
||||||
class SearchRoute extends GoRouteData with $SearchRoute {
|
class SearchRoute extends GoRouteData with $SearchRoute {
|
||||||
static const String emptySearchText = ' ';
|
static const String emptySearchText = ' ';
|
||||||
|
|||||||
@@ -664,6 +664,7 @@ const _$TabTypeEnumMap = {
|
|||||||
TabType.regular: 'regular',
|
TabType.regular: 'regular',
|
||||||
TabType.private: 'private',
|
TabType.private: 'private',
|
||||||
TabType.child: 'child',
|
TabType.child: 'child',
|
||||||
|
TabType.isolated: 'isolated',
|
||||||
};
|
};
|
||||||
|
|
||||||
mixin $TabViewRoute on GoRouteData {
|
mixin $TabViewRoute on GoRouteData {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:weblibre/core/routing/routes.dart';
|
import 'package:weblibre/core/routing/routes.dart';
|
||||||
import 'package:weblibre/features/about/domain/providers.dart';
|
import 'package:weblibre/features/about/domain/providers.dart';
|
||||||
import 'package:weblibre/features/geckoview/domain/repositories/tab.dart';
|
import 'package:weblibre/features/geckoview/domain/repositories/tab.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_mode.dart';
|
||||||
|
|
||||||
class AboutDialogScreen extends HookConsumerWidget {
|
class AboutDialogScreen extends HookConsumerWidget {
|
||||||
const AboutDialogScreen({super.key});
|
const AboutDialogScreen({super.key});
|
||||||
@@ -67,7 +68,7 @@ class AboutDialogScreen extends HookConsumerWidget {
|
|||||||
.read(tabRepositoryProvider.notifier)
|
.read(tabRepositoryProvider.notifier)
|
||||||
.addTab(
|
.addTab(
|
||||||
url: Uri.https('feedback.weblibre.eu'),
|
url: Uri.https('feedback.weblibre.eu'),
|
||||||
private: false,
|
tabMode: TabMode.regular,
|
||||||
selectTab: true,
|
selectTab: true,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -85,7 +86,7 @@ class AboutDialogScreen extends HookConsumerWidget {
|
|||||||
.read(tabRepositoryProvider.notifier)
|
.read(tabRepositoryProvider.notifier)
|
||||||
.addTab(
|
.addTab(
|
||||||
url: Uri.https('github.com').replace(path: 'FaFre/WebLibre'),
|
url: Uri.https('github.com').replace(path: 'FaFre/WebLibre'),
|
||||||
private: false,
|
tabMode: TabMode.regular,
|
||||||
selectTab: true,
|
selectTab: true,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -104,7 +105,7 @@ class AboutDialogScreen extends HookConsumerWidget {
|
|||||||
.read(tabRepositoryProvider.notifier)
|
.read(tabRepositoryProvider.notifier)
|
||||||
.addTab(
|
.addTab(
|
||||||
url: Uri.https('docs.weblibre.eu'),
|
url: Uri.https('docs.weblibre.eu'),
|
||||||
private: false,
|
tabMode: TabMode.regular,
|
||||||
selectTab: true,
|
selectTab: true,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -123,7 +124,7 @@ class AboutDialogScreen extends HookConsumerWidget {
|
|||||||
.read(tabRepositoryProvider.notifier)
|
.read(tabRepositoryProvider.notifier)
|
||||||
.addTab(
|
.addTab(
|
||||||
url: Uri.https('github.com').replace(path: 'FaFre/WebLibre'),
|
url: Uri.https('github.com').replace(path: 'FaFre/WebLibre'),
|
||||||
private: false,
|
tabMode: TabMode.regular,
|
||||||
selectTab: true,
|
selectTab: true,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
// dart format width=80
|
// dart format width=80
|
||||||
import 'package:drift/internal/versioned_schema.dart' as i0;
|
import 'package:drift/internal/versioned_schema.dart' as i0;
|
||||||
import 'package:drift/drift.dart' as i1;
|
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 Schema2 extends i0.VersionedSchema {
|
final class Schema2 extends i0.VersionedSchema {
|
||||||
Schema2({required super.database}) : super(version: 2);
|
Schema2({required super.database}) : super(version: 2);
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import 'package:nullability/nullability.dart';
|
|||||||
import 'package:weblibre/core/routing/routes.dart';
|
import 'package:weblibre/core/routing/routes.dart';
|
||||||
import 'package:weblibre/features/bangs/data/models/bang_data.dart';
|
import 'package:weblibre/features/bangs/data/models/bang_data.dart';
|
||||||
import 'package:weblibre/features/geckoview/domain/repositories/tab.dart';
|
import 'package:weblibre/features/geckoview/domain/repositories/tab.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_mode.dart';
|
||||||
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
||||||
import 'package:weblibre/presentation/widgets/url_icon.dart';
|
import 'package:weblibre/presentation/widgets/url_icon.dart';
|
||||||
|
|
||||||
@@ -88,19 +89,15 @@ class BangDetails extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
final url = Uri.parse(bangData.getDefaultUrl().origin);
|
final url = Uri.parse(bangData.getDefaultUrl().origin);
|
||||||
final isPrivate =
|
final tabMode = TabMode.fromTabType(
|
||||||
ref
|
ref
|
||||||
.read(generalSettingsWithDefaultsProvider)
|
.read(generalSettingsWithDefaultsProvider)
|
||||||
.defaultCreateTabType ==
|
.defaultCreateTabType,
|
||||||
TabType.private;
|
);
|
||||||
|
|
||||||
await ref
|
await ref
|
||||||
.read(tabRepositoryProvider.notifier)
|
.read(tabRepositoryProvider.notifier)
|
||||||
.addTab(
|
.addTab(url: url, tabMode: tabMode, selectTab: true);
|
||||||
url: url,
|
|
||||||
private: isPrivate,
|
|
||||||
selectTab: true,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
const BrowserRoute().go(context);
|
const BrowserRoute().go(context);
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import 'package:weblibre/features/geckoview/domain/entities/states/find_result.d
|
|||||||
import 'package:weblibre/features/geckoview/domain/entities/states/history.dart';
|
import 'package:weblibre/features/geckoview/domain/entities/states/history.dart';
|
||||||
import 'package:weblibre/features/geckoview/domain/entities/states/readerable.dart';
|
import 'package:weblibre/features/geckoview/domain/entities/states/readerable.dart';
|
||||||
import 'package:weblibre/features/geckoview/domain/entities/states/security.dart';
|
import 'package:weblibre/features/geckoview/domain/entities/states/security.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_mode.dart';
|
||||||
|
|
||||||
part 'tab.g.dart';
|
part 'tab.g.dart';
|
||||||
|
|
||||||
@@ -61,7 +62,9 @@ class TabState extends WebPageInfo {
|
|||||||
|
|
||||||
final int progress;
|
final int progress;
|
||||||
|
|
||||||
final bool isPrivate;
|
final TabMode tabMode;
|
||||||
|
String? get isolationContextId => tabMode.isolationContextId;
|
||||||
|
|
||||||
final bool isFullScreen;
|
final bool isFullScreen;
|
||||||
final bool isLoading;
|
final bool isLoading;
|
||||||
final bool showToolbarAsExpanded;
|
final bool showToolbarAsExpanded;
|
||||||
@@ -82,7 +85,7 @@ class TabState extends WebPageInfo {
|
|||||||
required this.icon,
|
required this.icon,
|
||||||
required this.thumbnail,
|
required this.thumbnail,
|
||||||
required this.progress,
|
required this.progress,
|
||||||
required this.isPrivate,
|
this.tabMode = TabMode.regular,
|
||||||
required this.isFullScreen,
|
required this.isFullScreen,
|
||||||
required this.isLoading,
|
required this.isLoading,
|
||||||
required this.showToolbarAsExpanded,
|
required this.showToolbarAsExpanded,
|
||||||
@@ -101,7 +104,6 @@ class TabState extends WebPageInfo {
|
|||||||
icon: null,
|
icon: null,
|
||||||
thumbnail: null,
|
thumbnail: null,
|
||||||
progress: 0,
|
progress: 0,
|
||||||
isPrivate: false,
|
|
||||||
isFullScreen: false,
|
isFullScreen: false,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
showToolbarAsExpanded: false,
|
showToolbarAsExpanded: false,
|
||||||
@@ -120,7 +122,7 @@ class TabState extends WebPageInfo {
|
|||||||
icon,
|
icon,
|
||||||
thumbnail,
|
thumbnail,
|
||||||
progress,
|
progress,
|
||||||
isPrivate,
|
tabMode,
|
||||||
isFullScreen,
|
isFullScreen,
|
||||||
isLoading,
|
isLoading,
|
||||||
showToolbarAsExpanded,
|
showToolbarAsExpanded,
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ abstract class _$TabStateCWProxy {
|
|||||||
|
|
||||||
TabState progress(int progress);
|
TabState progress(int progress);
|
||||||
|
|
||||||
TabState isPrivate(bool isPrivate);
|
TabState tabMode(TabMode tabMode);
|
||||||
|
|
||||||
TabState isFullScreen(bool isFullScreen);
|
TabState isFullScreen(bool isFullScreen);
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ abstract class _$TabStateCWProxy {
|
|||||||
EquatableImage? icon,
|
EquatableImage? icon,
|
||||||
EquatableImage? thumbnail,
|
EquatableImage? thumbnail,
|
||||||
int progress,
|
int progress,
|
||||||
bool isPrivate,
|
TabMode tabMode,
|
||||||
bool isFullScreen,
|
bool isFullScreen,
|
||||||
bool isLoading,
|
bool isLoading,
|
||||||
bool showToolbarAsExpanded,
|
bool showToolbarAsExpanded,
|
||||||
@@ -92,7 +92,7 @@ class _$TabStateCWProxyImpl implements _$TabStateCWProxy {
|
|||||||
TabState progress(int progress) => call(progress: progress);
|
TabState progress(int progress) => call(progress: progress);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
TabState isPrivate(bool isPrivate) => call(isPrivate: isPrivate);
|
TabState tabMode(TabMode tabMode) => call(tabMode: tabMode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
TabState isFullScreen(bool isFullScreen) => call(isFullScreen: isFullScreen);
|
TabState isFullScreen(bool isFullScreen) => call(isFullScreen: isFullScreen);
|
||||||
@@ -136,7 +136,7 @@ class _$TabStateCWProxyImpl implements _$TabStateCWProxy {
|
|||||||
Object? icon = const $CopyWithPlaceholder(),
|
Object? icon = const $CopyWithPlaceholder(),
|
||||||
Object? thumbnail = const $CopyWithPlaceholder(),
|
Object? thumbnail = const $CopyWithPlaceholder(),
|
||||||
Object? progress = const $CopyWithPlaceholder(),
|
Object? progress = const $CopyWithPlaceholder(),
|
||||||
Object? isPrivate = const $CopyWithPlaceholder(),
|
Object? tabMode = const $CopyWithPlaceholder(),
|
||||||
Object? isFullScreen = const $CopyWithPlaceholder(),
|
Object? isFullScreen = const $CopyWithPlaceholder(),
|
||||||
Object? isLoading = const $CopyWithPlaceholder(),
|
Object? isLoading = const $CopyWithPlaceholder(),
|
||||||
Object? showToolbarAsExpanded = const $CopyWithPlaceholder(),
|
Object? showToolbarAsExpanded = const $CopyWithPlaceholder(),
|
||||||
@@ -175,10 +175,10 @@ class _$TabStateCWProxyImpl implements _$TabStateCWProxy {
|
|||||||
? _value.progress
|
? _value.progress
|
||||||
// ignore: cast_nullable_to_non_nullable
|
// ignore: cast_nullable_to_non_nullable
|
||||||
: progress as int,
|
: progress as int,
|
||||||
isPrivate: isPrivate == const $CopyWithPlaceholder() || isPrivate == null
|
tabMode: tabMode == const $CopyWithPlaceholder() || tabMode == null
|
||||||
? _value.isPrivate
|
? _value.tabMode
|
||||||
// ignore: cast_nullable_to_non_nullable
|
// ignore: cast_nullable_to_non_nullable
|
||||||
: isPrivate as bool,
|
: tabMode as TabMode,
|
||||||
isFullScreen:
|
isFullScreen:
|
||||||
isFullScreen == const $CopyWithPlaceholder() || isFullScreen == null
|
isFullScreen == const $CopyWithPlaceholder() || isFullScreen == null
|
||||||
? _value.isFullScreen
|
? _value.isFullScreen
|
||||||
|
|||||||
@@ -20,7 +20,6 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
||||||
import 'package:nullability/nullability.dart';
|
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
import 'package:share_plus/share_plus.dart';
|
import 'package:share_plus/share_plus.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
@@ -32,6 +31,7 @@ import 'package:weblibre/features/geckoview/domain/providers/selected_tab.dart';
|
|||||||
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
|
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
|
||||||
import 'package:weblibre/features/geckoview/domain/repositories/tab.dart';
|
import 'package:weblibre/features/geckoview/domain/repositories/tab.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/find_in_page/domain/repositories/find_in_page.dart';
|
import 'package:weblibre/features/geckoview/features/find_in_page/domain/repositories/find_in_page.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_mode.dart';
|
||||||
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
||||||
|
|
||||||
part 'providers.g.dart';
|
part 'providers.g.dart';
|
||||||
@@ -51,13 +51,10 @@ GeckoSelectionActionService selectionActionService(Ref ref) {
|
|||||||
tabStatesProvider,
|
tabStatesProvider,
|
||||||
)[ref.read(selectedTabProvider)];
|
)[ref.read(selectedTabProvider)];
|
||||||
|
|
||||||
final isPrivate = selectedTabState?.isPrivate.mapNotNull(
|
final selectedTabType = selectedTabState?.tabMode.toTabType();
|
||||||
(isCurrentPrivate) =>
|
|
||||||
isCurrentPrivate ? TabType.private : TabType.regular,
|
|
||||||
);
|
|
||||||
|
|
||||||
final route = SearchRoute(
|
final route = SearchRoute(
|
||||||
tabType: isPrivate ?? settings.defaultCreateTabType,
|
tabType: selectedTabType ?? settings.defaultCreateTabType,
|
||||||
searchText: text,
|
searchText: text,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -76,19 +73,20 @@ GeckoSelectionActionService selectionActionService(Ref ref) {
|
|||||||
tabStatesProvider,
|
tabStatesProvider,
|
||||||
)[ref.read(selectedTabProvider)];
|
)[ref.read(selectedTabProvider)];
|
||||||
|
|
||||||
final isPrivate =
|
final tabMode =
|
||||||
currentTab?.isPrivate ??
|
currentTab?.tabMode ??
|
||||||
ref
|
TabMode.fromTabType(
|
||||||
.read(generalSettingsWithDefaultsProvider)
|
ref
|
||||||
.defaultCreateTabType ==
|
.read(generalSettingsWithDefaultsProvider)
|
||||||
TabType.private;
|
.defaultCreateTabType,
|
||||||
|
);
|
||||||
|
|
||||||
await ref
|
await ref
|
||||||
.read(tabRepositoryProvider.notifier)
|
.read(tabRepositoryProvider.notifier)
|
||||||
.addTab(
|
.addTab(
|
||||||
url: defaultSearchBang.getTemplateUrl(text),
|
url: defaultSearchBang.getTemplateUrl(text),
|
||||||
parentId: currentTab?.id,
|
parentId: currentTab?.id,
|
||||||
private: isPrivate,
|
tabMode: tabMode,
|
||||||
selectTab: true,
|
selectTab: true,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ final class SelectionActionServiceProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _$selectionActionServiceHash() =>
|
String _$selectionActionServiceHash() =>
|
||||||
r'75fa0fa770e74e49554d4098102fde9f9e531c4f';
|
r'75701bb0be3302b461d296a7b0b28e3a30a9d346';
|
||||||
|
|
||||||
@ProviderFor(eventService)
|
@ProviderFor(eventService)
|
||||||
final eventServiceProvider = EventServiceProvider._();
|
final eventServiceProvider = EventServiceProvider._();
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ import 'package:weblibre/features/geckoview/domain/entities/states/tab.dart';
|
|||||||
import 'package:weblibre/features/geckoview/domain/providers.dart';
|
import 'package:weblibre/features/geckoview/domain/providers.dart';
|
||||||
import 'package:weblibre/features/geckoview/domain/providers/selected_tab.dart';
|
import 'package:weblibre/features/geckoview/domain/providers/selected_tab.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/find_in_page/domain/repositories/find_in_page.dart';
|
import 'package:weblibre/features/geckoview/features/find_in_page/domain/repositories/find_in_page.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/tabs/data/entities/isolation_context.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_mode.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/domain/providers.dart';
|
import 'package:weblibre/features/geckoview/features/tabs/domain/providers.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/gecko_inference.dart';
|
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/gecko_inference.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart';
|
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart';
|
||||||
@@ -78,13 +80,23 @@ class TabStates extends _$TabStates {
|
|||||||
resolvedTitle = contentState.title;
|
resolvedTitle = contentState.title;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Infer tabMode from context ID if not already set and context is isolated
|
||||||
|
final inferredTabMode = switch (current.tabMode) {
|
||||||
|
IsolatedTabMode() => current.tabMode,
|
||||||
|
_ when isIsolatedContextId(contentState.contextId) => TabMode.isolated(
|
||||||
|
contentState.contextId!,
|
||||||
|
),
|
||||||
|
_ when contentState.isPrivate => TabMode.private,
|
||||||
|
_ => TabMode.regular,
|
||||||
|
};
|
||||||
|
|
||||||
final newState = current.copyWith(
|
final newState = current.copyWith(
|
||||||
parentId: contentState.parentId,
|
parentId: contentState.parentId,
|
||||||
contextId: contentState.contextId,
|
contextId: contentState.contextId,
|
||||||
url: url,
|
url: url,
|
||||||
title: resolvedTitle,
|
title: resolvedTitle,
|
||||||
progress: contentState.progress,
|
progress: contentState.progress,
|
||||||
isPrivate: contentState.isPrivate,
|
tabMode: inferredTabMode,
|
||||||
isFullScreen: contentState.isFullScreen,
|
isFullScreen: contentState.isFullScreen,
|
||||||
isLoading: contentState.isLoading,
|
isLoading: contentState.isLoading,
|
||||||
showToolbarAsExpanded: contentState.showToolbarAsExpanded,
|
showToolbarAsExpanded: contentState.showToolbarAsExpanded,
|
||||||
@@ -373,7 +385,9 @@ Future<bool> isTabTunneled(Ref ref, String? tabId) async {
|
|||||||
final torSettings = ref.watch(torSettingsWithDefaultsProvider);
|
final torSettings = ref.watch(torSettingsWithDefaultsProvider);
|
||||||
|
|
||||||
if (tabState != null) {
|
if (tabState != null) {
|
||||||
if (tabState.isPrivate) {
|
// Isolated tabs follow the same proxy rules as regular tabs
|
||||||
|
// (container-based routing via proxy aliasing)
|
||||||
|
if (tabState.tabMode is PrivateTabMode) {
|
||||||
return torSettings.proxyPrivateTabsTor;
|
return torSettings.proxyPrivateTabsTor;
|
||||||
} else {
|
} else {
|
||||||
switch (torSettings.proxyRegularTabsMode) {
|
switch (torSettings.proxyRegularTabsMode) {
|
||||||
@@ -402,13 +416,9 @@ TabState? selectedTabState(Ref ref) {
|
|||||||
|
|
||||||
@Riverpod()
|
@Riverpod()
|
||||||
TabType? selectedTabType(Ref ref) {
|
TabType? selectedTabType(Ref ref) {
|
||||||
final isPrivate = ref.watch(
|
final selectedState = ref.watch(selectedTabStateProvider);
|
||||||
selectedTabStateProvider.select((value) => value?.isPrivate),
|
|
||||||
);
|
|
||||||
|
|
||||||
return isPrivate.mapNotNull(
|
return selectedState?.tabMode.toTabType();
|
||||||
(isCurrentPrivate) => isCurrentPrivate ? TabType.private : TabType.regular,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Riverpod()
|
@Riverpod()
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ final class TabStatesProvider
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$tabStatesHash() => r'975de1242f42e2017754dc560125788b83ad8b72';
|
String _$tabStatesHash() => r'0fd97193ff963006e1943436b25b025dd3885c65';
|
||||||
|
|
||||||
abstract class _$TabStates extends $Notifier<Map<String, TabState>> {
|
abstract class _$TabStates extends $Notifier<Map<String, TabState>> {
|
||||||
Map<String, TabState> build();
|
Map<String, TabState> build();
|
||||||
@@ -258,7 +258,7 @@ final class IsTabTunneledProvider
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$isTabTunneledHash() => r'55a42ccb73d1921eed27d32f6da3a54680d4ac8c';
|
String _$isTabTunneledHash() => r'7265f5e6f32056846d3df38f48476d10c3a70bac';
|
||||||
|
|
||||||
final class IsTabTunneledFamily extends $Family
|
final class IsTabTunneledFamily extends $Family
|
||||||
with $FunctionalFamilyOverride<FutureOr<bool>, String?> {
|
with $FunctionalFamilyOverride<FutureOr<bool>, String?> {
|
||||||
@@ -358,7 +358,7 @@ final class SelectedTabTypeProvider
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$selectedTabTypeHash() => r'53093fc6db8becde7500163661e087fb0a2eb955';
|
String _$selectedTabTypeHash() => r'fe14b5c9f81d8f5859ae9d758e440c42908e8a54';
|
||||||
|
|
||||||
@ProviderFor(selectedTabContainerId)
|
@ProviderFor(selectedTabContainerId)
|
||||||
final selectedTabContainerIdProvider = SelectedTabContainerIdProvider._();
|
final selectedTabContainerIdProvider = SelectedTabContainerIdProvider._();
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import 'package:drift/drift.dart';
|
|||||||
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
||||||
import 'package:nullability/nullability.dart';
|
import 'package:nullability/nullability.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
import 'package:synchronized/synchronized.dart';
|
||||||
import 'package:weblibre/core/logger.dart';
|
import 'package:weblibre/core/logger.dart';
|
||||||
import 'package:weblibre/features/geckoview/domain/entities/states/tab.dart';
|
import 'package:weblibre/features/geckoview/domain/entities/states/tab.dart';
|
||||||
import 'package:weblibre/features/geckoview/domain/entities/tab_container_selection.dart';
|
import 'package:weblibre/features/geckoview/domain/entities/tab_container_selection.dart';
|
||||||
@@ -31,12 +32,16 @@ import 'package:weblibre/features/geckoview/domain/providers.dart';
|
|||||||
import 'package:weblibre/features/geckoview/domain/providers/selected_tab.dart';
|
import 'package:weblibre/features/geckoview/domain/providers/selected_tab.dart';
|
||||||
import 'package:weblibre/features/geckoview/domain/providers/tab_list.dart';
|
import 'package:weblibre/features/geckoview/domain/providers/tab_list.dart';
|
||||||
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
|
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/browser/domain/services/browser_data.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/tabs/data/entities/isolation_context.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/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/container_data.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/data/providers.dart';
|
import 'package:weblibre/features/geckoview/features/tabs/data/providers.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/domain/providers/selected_container.dart';
|
import 'package:weblibre/features/geckoview/features/tabs/domain/providers/selected_container.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/container.dart';
|
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/container.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart';
|
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart';
|
||||||
|
import 'package:weblibre/features/tor/domain/repositories/tor_proxy.dart';
|
||||||
import 'package:weblibre/utils/debouncer.dart';
|
import 'package:weblibre/utils/debouncer.dart';
|
||||||
|
|
||||||
part 'tab.g.dart';
|
part 'tab.g.dart';
|
||||||
@@ -46,6 +51,8 @@ class TabRepository extends _$TabRepository {
|
|||||||
final _tabsService = GeckoTabService();
|
final _tabsService = GeckoTabService();
|
||||||
|
|
||||||
final _tabFromIntent = <String>{};
|
final _tabFromIntent = <String>{};
|
||||||
|
final _closeLock = Lock();
|
||||||
|
final _pendingIsolationCleanup = <String>{};
|
||||||
|
|
||||||
bool hasLaunchedFromIntent(String? tabId) {
|
bool hasLaunchedFromIntent(String? tabId) {
|
||||||
if (tabId == null) {
|
if (tabId == null) {
|
||||||
@@ -79,13 +86,13 @@ class TabRepository extends _$TabRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<String> addTab({
|
Future<String> addTab({
|
||||||
|
required TabMode tabMode,
|
||||||
Uri? url,
|
Uri? url,
|
||||||
required bool selectTab,
|
required bool selectTab,
|
||||||
bool startLoading = true,
|
bool startLoading = true,
|
||||||
String? parentId,
|
String? parentId,
|
||||||
LoadUrlFlags flags = LoadUrlFlags.NONE,
|
LoadUrlFlags flags = LoadUrlFlags.NONE,
|
||||||
Source source = Internal.newTab,
|
Source source = Internal.newTab,
|
||||||
required bool private,
|
|
||||||
HistoryMetadataKey? historyMetadata,
|
HistoryMetadataKey? historyMetadata,
|
||||||
Map<String, String>? additionalHeaders,
|
Map<String, String>? additionalHeaders,
|
||||||
TabContainerSelection containerSelection =
|
TabContainerSelection containerSelection =
|
||||||
@@ -101,10 +108,20 @@ class TabRepository extends _$TabRepository {
|
|||||||
SpecificContainerTabSelection(:final container) => container,
|
SpecificContainerTabSelection(:final container) => container,
|
||||||
};
|
};
|
||||||
|
|
||||||
final validatedParentId = await _resolveParentIdForContext(
|
// For isolated tabs, skip parent context validation since
|
||||||
parentId: parentId,
|
// isolated tabs use their own immutable context ID.
|
||||||
targetContextId: assignedContainer?.metadata.contextualIdentity,
|
final validatedParentId = tabMode is IsolatedTabMode
|
||||||
);
|
? parentId
|
||||||
|
: await _resolveParentIdForContext(
|
||||||
|
parentId: parentId,
|
||||||
|
targetContextId: assignedContainer?.metadata.contextualIdentity,
|
||||||
|
);
|
||||||
|
|
||||||
|
final effectiveIsolationContextId = tabMode.isolationContextId;
|
||||||
|
|
||||||
|
final effectiveContextId = tabMode is IsolatedTabMode
|
||||||
|
? effectiveIsolationContextId
|
||||||
|
: assignedContainer?.metadata.contextualIdentity;
|
||||||
|
|
||||||
final newTabId = await tabDao.upsertTabTransactional(
|
final newTabId = await tabDao.upsertTabTransactional(
|
||||||
() {
|
() {
|
||||||
@@ -114,17 +131,17 @@ class TabRepository extends _$TabRepository {
|
|||||||
startLoading: startLoading,
|
startLoading: startLoading,
|
||||||
parentId: validatedParentId,
|
parentId: validatedParentId,
|
||||||
flags: flags,
|
flags: flags,
|
||||||
contextId: assignedContainer?.metadata.contextualIdentity,
|
contextId: effectiveContextId,
|
||||||
source: source,
|
source: source,
|
||||||
private: private,
|
private: tabMode is PrivateTabMode,
|
||||||
historyMetadata: historyMetadata,
|
historyMetadata: historyMetadata,
|
||||||
additionalHeaders: additionalHeaders,
|
additionalHeaders: additionalHeaders,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
parentId: Value(validatedParentId),
|
parentId: Value(validatedParentId),
|
||||||
containerId: Value(assignedContainer?.id),
|
containerId: Value(assignedContainer?.id),
|
||||||
isPrivate: Value(private),
|
|
||||||
url: Value(url),
|
url: Value(url),
|
||||||
|
tabMode: Value(tabMode),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (launchedFromIntent) {
|
if (launchedFromIntent) {
|
||||||
@@ -188,8 +205,14 @@ class TabRepository extends _$TabRepository {
|
|||||||
parentId: Value(validatedParentId),
|
parentId: Value(validatedParentId),
|
||||||
source: TabSource.manual,
|
source: TabSource.manual,
|
||||||
containerId: Value(assignedContainer?.id),
|
containerId: Value(assignedContainer?.id),
|
||||||
isPrivate: Value(tab.private),
|
|
||||||
url: Value(Uri.tryParse(tab.url)),
|
url: Value(Uri.tryParse(tab.url)),
|
||||||
|
tabMode: Value(
|
||||||
|
isIsolatedContextId(tab.contextId)
|
||||||
|
? TabMode.isolated(tab.contextId!)
|
||||||
|
: tab.private
|
||||||
|
? TabMode.private
|
||||||
|
: TabMode.regular,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,19 +227,35 @@ class TabRepository extends _$TabRepository {
|
|||||||
}) async {
|
}) async {
|
||||||
final tabDao = ref.read(tabDatabaseProvider).tabDao;
|
final tabDao = ref.read(tabDatabaseProvider).tabDao;
|
||||||
|
|
||||||
|
final sourceTabMode =
|
||||||
|
await tabDao.getTabMode(selectTabId).getSingleOrNull() ??
|
||||||
|
TabMode.regular;
|
||||||
|
|
||||||
|
// Duplicating an isolated tab creates a new isolation group
|
||||||
|
final duplicateIsolationContextId = sourceTabMode is IsolatedTabMode
|
||||||
|
? newIsolatedContextId()
|
||||||
|
: null;
|
||||||
|
|
||||||
|
final duplicateTabMode = sourceTabMode is IsolatedTabMode
|
||||||
|
? TabMode.isolated(duplicateIsolationContextId!)
|
||||||
|
: sourceTabMode;
|
||||||
|
|
||||||
|
// Isolated tabs always use their isolation context ID
|
||||||
|
final effectiveContextId = sourceTabMode is IsolatedTabMode
|
||||||
|
? duplicateIsolationContextId
|
||||||
|
: containerData?.metadata.contextualIdentity;
|
||||||
|
|
||||||
return await tabDao.upsertTabTransactional(
|
return await tabDao.upsertTabTransactional(
|
||||||
() {
|
() {
|
||||||
return _tabsService.duplicateTab(
|
return _tabsService.duplicateTab(
|
||||||
selectTabId: selectTabId,
|
selectTabId: selectTabId,
|
||||||
newContextId: containerData?.metadata.contextualIdentity,
|
newContextId: effectiveContextId,
|
||||||
selectNewTab: selectTab,
|
selectNewTab: selectTab,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
parentId: const Value.absent(),
|
parentId: const Value.absent(),
|
||||||
containerId: Value(containerData?.id),
|
containerId: Value(containerData?.id),
|
||||||
isPrivate: Value(
|
tabMode: Value(duplicateTabMode),
|
||||||
await tabDao.getTabIsPrivate(selectTabId).getSingleOrNull(),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -402,27 +441,148 @@ class TabRepository extends _$TabRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> closeTab(String tabId) async {
|
Future<void> closeTab(String tabId) {
|
||||||
if (ref.read(selectedTabProvider) == tabId) {
|
return _closeLock.synchronized(() async {
|
||||||
await _selectNextTab(tabId);
|
// Collect isolation context before close
|
||||||
}
|
final isolationContextId = ref
|
||||||
|
.read(tabStatesProvider)[tabId]
|
||||||
|
?.isolationContextId;
|
||||||
|
|
||||||
return _tabsService.removeTab(tabId: tabId);
|
if (ref.read(selectedTabProvider) == tabId) {
|
||||||
|
await _selectNextTab(tabId);
|
||||||
|
}
|
||||||
|
|
||||||
|
await _tabsService.removeTab(tabId: tabId);
|
||||||
|
|
||||||
|
// Queue isolation cleanup — actual cleanup runs after syncTabs
|
||||||
|
// deletes the DB row, so the count check is accurate.
|
||||||
|
if (isolationContextId != null) {
|
||||||
|
_pendingIsolationCleanup.add(isolationContextId);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> closeTabs(List<String> tabIds) async {
|
Future<void> closeTabs(List<String> tabIds) {
|
||||||
final selectedTab = ref.read(selectedTabProvider);
|
return _closeLock.synchronized(() async {
|
||||||
if (selectedTab.mapNotNull(tabIds.contains) ?? false) {
|
// Collect isolation contexts from tabs being closed
|
||||||
await _selectNextTab(selectedTab!);
|
for (final tabId in tabIds) {
|
||||||
|
final contextId = ref
|
||||||
|
.read(tabStatesProvider)[tabId]
|
||||||
|
?.isolationContextId;
|
||||||
|
if (contextId != null) {
|
||||||
|
_pendingIsolationCleanup.add(contextId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final selectedTab = ref.read(selectedTabProvider);
|
||||||
|
if (selectedTab.mapNotNull(tabIds.contains) ?? false) {
|
||||||
|
await _selectNextTab(selectedTab!);
|
||||||
|
}
|
||||||
|
|
||||||
|
await _tabsService.removeTabs(ids: tabIds);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clears Gecko browsing data and removes proxy alias for an isolation
|
||||||
|
/// context if no more tabs share it.
|
||||||
|
Future<void> _cleanupIsolationContextIfEmpty(String contextId) async {
|
||||||
|
final tabDao = ref.read(tabDatabaseProvider).tabDao;
|
||||||
|
|
||||||
|
// Re-verify count after close (handles concurrent close races)
|
||||||
|
final remaining = await tabDao.tabsInIsolationGroup(contextId).getSingle();
|
||||||
|
|
||||||
|
if (remaining > 0) return;
|
||||||
|
|
||||||
|
// Guard against debounced DB persistence: a sibling tab can already be
|
||||||
|
// active in-memory for this context before isolation_context_id is written.
|
||||||
|
final activeTabs = ref.read(tabListProvider).value;
|
||||||
|
final activeStates = ref.read(tabStatesProvider);
|
||||||
|
final hasActiveSibling = activeTabs.any((tabId) {
|
||||||
|
final state = activeStates[tabId];
|
||||||
|
if (state == null) return false;
|
||||||
|
|
||||||
|
return state.isolationContextId == contextId ||
|
||||||
|
state.contextId == contextId;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hasActiveSibling) {
|
||||||
|
logger.i(
|
||||||
|
'Skipping isolation cleanup for active context still in memory: $contextId',
|
||||||
|
);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return _tabsService.removeTabs(ids: tabIds);
|
logger.i('Cleaning up isolation context: $contextId');
|
||||||
|
|
||||||
|
// Clear Gecko browsing data for this context
|
||||||
|
try {
|
||||||
|
await ref
|
||||||
|
.read(browserDataServiceProvider.notifier)
|
||||||
|
.clearDataForContext(contextId);
|
||||||
|
} catch (e, st) {
|
||||||
|
logger.e(
|
||||||
|
'Failed to clear data for isolation context $contextId',
|
||||||
|
error: e,
|
||||||
|
stackTrace: st,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Best-effort: remove proxy alias (no-op if never set)
|
||||||
|
try {
|
||||||
|
await ref
|
||||||
|
.read(torProxyRepositoryProvider.notifier)
|
||||||
|
.removeContainerProxy(contextId);
|
||||||
|
} catch (e, st) {
|
||||||
|
logger.e(
|
||||||
|
'Failed to remove proxy for isolation context $contextId',
|
||||||
|
error: e,
|
||||||
|
stackTrace: st,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> undoClose() {
|
Future<void> undoClose() {
|
||||||
return _tabsService.undo();
|
return _tabsService.undo();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Cleans up isolation contexts from previous crashed sessions.
|
||||||
|
/// Called once after tab list stabilizes on startup.
|
||||||
|
// Future<void> _cleanupOrphanedIsolationContexts() async {
|
||||||
|
// final tabDao = ref.read(tabDatabaseProvider).tabDao;
|
||||||
|
|
||||||
|
// try {
|
||||||
|
// await _closeLock.synchronized(() async {
|
||||||
|
// if (!ref.mounted) return;
|
||||||
|
|
||||||
|
// // Reconcile DB rows against the current engine tab snapshot, including
|
||||||
|
// // valid empty-tab sessions (retainTabIds can be empty here).
|
||||||
|
// final syncTabsResult = await tabDao.syncTabs(
|
||||||
|
// retainTabIds: ref.read(tabListProvider).value,
|
||||||
|
// );
|
||||||
|
// _pendingIsolationCleanup.addAll(
|
||||||
|
// syncTabsResult.deletedIsolationContextIds,
|
||||||
|
// );
|
||||||
|
|
||||||
|
// if (_pendingIsolationCleanup.isNotEmpty) {
|
||||||
|
// final pending = Set<String>.of(_pendingIsolationCleanup);
|
||||||
|
// _pendingIsolationCleanup.clear();
|
||||||
|
|
||||||
|
// for (final contextId in pending) {
|
||||||
|
// if (!ref.mounted) return;
|
||||||
|
// logger.i('Cleaning orphaned isolation context: $contextId');
|
||||||
|
// await _cleanupIsolationContextIfEmpty(contextId);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// } catch (e, st) {
|
||||||
|
// logger.e(
|
||||||
|
// 'Error during orphan isolation context cleanup',
|
||||||
|
// error: e,
|
||||||
|
// stackTrace: st,
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void build() {
|
void build() {
|
||||||
final eventSerivce = ref.watch(eventServiceProvider);
|
final eventSerivce = ref.watch(eventServiceProvider);
|
||||||
@@ -438,7 +598,6 @@ class TabRepository extends _$TabRepository {
|
|||||||
parentId: const Value.absent(),
|
parentId: const Value.absent(),
|
||||||
source: TabSource.addedEvent,
|
source: TabSource.addedEvent,
|
||||||
containerId: Value(containerId),
|
containerId: Value(containerId),
|
||||||
isPrivate: const Value.absent(),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
onError: (Object error, StackTrace stackTrace) {
|
onError: (Object error, StackTrace stackTrace) {
|
||||||
@@ -475,7 +634,7 @@ class TabRepository extends _$TabRepository {
|
|||||||
if (event.blocked || tabIsEmpty) {
|
if (event.blocked || tabIsEmpty) {
|
||||||
await addTab(
|
await addTab(
|
||||||
url: uri,
|
url: uri,
|
||||||
private: tabState.isPrivate,
|
tabMode: tabState.tabMode,
|
||||||
containerSelection: TabContainerSelection.specific(
|
containerSelection: TabContainerSelection.specific(
|
||||||
containerData,
|
containerData,
|
||||||
),
|
),
|
||||||
@@ -567,12 +726,42 @@ class TabRepository extends _$TabRepository {
|
|||||||
tabListProvider,
|
tabListProvider,
|
||||||
(previous, next) async {
|
(previous, next) async {
|
||||||
//Only sync tabs if there has been a previous value or is not empty
|
//Only sync tabs if there has been a previous value or is not empty
|
||||||
final syncTabs =
|
final shouldSyncTabs =
|
||||||
next.value.isNotEmpty || (previous?.value.isNotEmpty ?? false);
|
next.value.isNotEmpty || (previous?.value.isNotEmpty ?? false);
|
||||||
|
|
||||||
if (syncTabs) {
|
if (shouldSyncTabs) {
|
||||||
await db.tabDao.syncTabs(retainTabIds: next.value);
|
final syncTabsResult = await db.tabDao.syncTabs(
|
||||||
|
retainTabIds: next.value,
|
||||||
|
);
|
||||||
|
// Capture isolation contexts from rows deleted by syncTabs
|
||||||
|
// (orphaned tabs from crashes, or tabs the engine dropped).
|
||||||
|
_pendingIsolationCleanup.addAll(
|
||||||
|
syncTabsResult.deletedIsolationContextIds,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Process pending isolation context cleanups after syncTabs
|
||||||
|
// has deleted the rows, so the count check is accurate.
|
||||||
|
if (_pendingIsolationCleanup.isNotEmpty) {
|
||||||
|
final pending = Set<String>.of(_pendingIsolationCleanup);
|
||||||
|
_pendingIsolationCleanup.clear();
|
||||||
|
for (final contextId in pending) {
|
||||||
|
if (!ref.mounted) break;
|
||||||
|
await _cleanupIsolationContextIfEmpty(contextId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// One-shot orphan cleanup after tab list stabilizes (5s debounce).
|
||||||
|
// Also runs for DB-only contexts whose rows were already deleted
|
||||||
|
// by syncTabs above (those are handled via _pendingIsolationCleanup).
|
||||||
|
// if (!orphanCleanupDone) {
|
||||||
|
// orphanCleanupTimer?.cancel();
|
||||||
|
// orphanCleanupTimer = Timer(const Duration(seconds: 5), () async {
|
||||||
|
// if (orphanCleanupDone || !ref.mounted) return;
|
||||||
|
// orphanCleanupDone = true;
|
||||||
|
// await _cleanupOrphanedIsolationContexts();
|
||||||
|
// });
|
||||||
|
// }
|
||||||
},
|
},
|
||||||
onError: (Object error, StackTrace stackTrace) {
|
onError: (Object error, StackTrace stackTrace) {
|
||||||
logger.e(
|
logger.e(
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ final class TabRepositoryProvider
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$tabRepositoryHash() => r'd77e4e74bb7ee1b466172bca245b05c6bf03196c';
|
String _$tabRepositoryHash() => r'a6953b50e4b9d0e7e297a058872b9ad1c7d55c1a';
|
||||||
|
|
||||||
abstract class _$TabRepository extends $Notifier<void> {
|
abstract class _$TabRepository extends $Notifier<void> {
|
||||||
void build();
|
void build();
|
||||||
|
|||||||
+109
@@ -21,8 +21,11 @@ import 'package:fast_equatable/fast_equatable.dart';
|
|||||||
import 'package:nullability/nullability.dart';
|
import 'package:nullability/nullability.dart';
|
||||||
import 'package:riverpod/riverpod.dart';
|
import 'package:riverpod/riverpod.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
import 'package:synchronized/synchronized.dart';
|
||||||
import 'package:weblibre/core/logger.dart';
|
import 'package:weblibre/core/logger.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/tabs/data/providers.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/domain/providers.dart';
|
import 'package:weblibre/features/geckoview/features/tabs/domain/providers.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/container.dart';
|
||||||
import 'package:weblibre/features/tor/domain/repositories/tor_proxy.dart';
|
import 'package:weblibre/features/tor/domain/repositories/tor_proxy.dart';
|
||||||
import 'package:weblibre/features/tor/domain/services/tor_proxy.dart';
|
import 'package:weblibre/features/tor/domain/services/tor_proxy.dart';
|
||||||
import 'package:weblibre/features/user/data/models/tor_settings.dart';
|
import 'package:weblibre/features/user/data/models/tor_settings.dart';
|
||||||
@@ -32,6 +35,82 @@ part 'proxy_settings_replication.g.dart';
|
|||||||
|
|
||||||
@Riverpod(keepAlive: true)
|
@Riverpod(keepAlive: true)
|
||||||
class ProxySettingsReplication extends _$ProxySettingsReplication {
|
class ProxySettingsReplication extends _$ProxySettingsReplication {
|
||||||
|
var _proxiedIsolationContexts = <String>{};
|
||||||
|
|
||||||
|
final _recomputeLock = Lock();
|
||||||
|
|
||||||
|
Future<void> _queueIsolatedProxyAliasesRecompute() async {
|
||||||
|
if (_recomputeLock.inLock) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _recomputeLock.synchronized(() async {
|
||||||
|
try {
|
||||||
|
await _recomputeIsolatedProxyAliases();
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
logger.e(
|
||||||
|
'Error recomputing isolated proxy aliases',
|
||||||
|
error: error,
|
||||||
|
stackTrace: stackTrace,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _recomputeIsolatedProxyAliases() async {
|
||||||
|
final db = ref.read(tabDatabaseProvider);
|
||||||
|
|
||||||
|
final contextContainerMap = <String, Set<String>>{};
|
||||||
|
final pairs = await db.tabDao.isolatedContextContainerPairs().get();
|
||||||
|
|
||||||
|
for (final pair in pairs) {
|
||||||
|
final isolationContextId = pair.isolationContextId;
|
||||||
|
final containerId = pair.containerId;
|
||||||
|
|
||||||
|
if (isolationContextId == null || containerId == null) continue;
|
||||||
|
|
||||||
|
contextContainerMap
|
||||||
|
.putIfAbsent(isolationContextId, () => <String>{})
|
||||||
|
.add(containerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
final containers = await ref
|
||||||
|
.read(containerRepositoryProvider.notifier)
|
||||||
|
.getAllContainersWithCount();
|
||||||
|
|
||||||
|
// Build set of container IDs that have useProxy enabled.
|
||||||
|
final proxiedContainerIds = <String>{
|
||||||
|
for (final c in containers)
|
||||||
|
if (c.metadata.useProxy) c.id,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Compute which isolation contexts need proxy aliases.
|
||||||
|
// A context needs an alias if ANY of its associated containers
|
||||||
|
// has useProxy enabled.
|
||||||
|
final newProxied = <String>{
|
||||||
|
for (final entry in contextContainerMap.entries)
|
||||||
|
if (entry.value.any(proxiedContainerIds.contains)) entry.key,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Remove aliases that are no longer needed.
|
||||||
|
final toRemove = _proxiedIsolationContexts.difference(newProxied);
|
||||||
|
for (final contextId in toRemove) {
|
||||||
|
await ref
|
||||||
|
.read(torProxyRepositoryProvider.notifier)
|
||||||
|
.removeContainerProxy(contextId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add aliases that are newly needed.
|
||||||
|
final toAdd = newProxied.difference(_proxiedIsolationContexts);
|
||||||
|
for (final contextId in toAdd) {
|
||||||
|
await ref
|
||||||
|
.read(torProxyRepositoryProvider.notifier)
|
||||||
|
.addContainerProxy(contextId);
|
||||||
|
}
|
||||||
|
|
||||||
|
_proxiedIsolationContexts = newProxied;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void build() {
|
void build() {
|
||||||
ref.listen(
|
ref.listen(
|
||||||
@@ -142,5 +221,35 @@ class ProxySettingsReplication extends _$ProxySettingsReplication {
|
|||||||
.setSiteAssignments(next.requireValue);
|
.setSiteAssignments(next.requireValue);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ref.listen(
|
||||||
|
fireImmediately: true,
|
||||||
|
watchIsolatedContextContainerMapProvider.select(
|
||||||
|
(value) => EquatableValue(value.value),
|
||||||
|
),
|
||||||
|
(previous, next) => _queueIsolatedProxyAliasesRecompute(),
|
||||||
|
onError: (error, stackTrace) {
|
||||||
|
logger.e(
|
||||||
|
'Error listening to isolated context proxy aliases',
|
||||||
|
error: error,
|
||||||
|
stackTrace: stackTrace,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
ref.listen(
|
||||||
|
fireImmediately: true,
|
||||||
|
watchContainersWithCountProvider.select(
|
||||||
|
(value) => EquatableValue(value.value),
|
||||||
|
),
|
||||||
|
(previous, next) => _queueIsolatedProxyAliasesRecompute(),
|
||||||
|
onError: (error, stackTrace) {
|
||||||
|
logger.e(
|
||||||
|
'Error listening to container proxy changes for isolated aliases',
|
||||||
|
error: error,
|
||||||
|
stackTrace: stackTrace,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -42,7 +42,7 @@ final class ProxySettingsReplicationProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _$proxySettingsReplicationHash() =>
|
String _$proxySettingsReplicationHash() =>
|
||||||
r'e4c5e35b9aab2aae60e3f09a99472f98a7beb69e';
|
r'79eca77aa476ed46cfe342e52926ea1cf956dc70';
|
||||||
|
|
||||||
abstract class _$ProxySettingsReplication extends $Notifier<void> {
|
abstract class _$ProxySettingsReplication extends $Notifier<void> {
|
||||||
void build();
|
void build();
|
||||||
|
|||||||
+2
-1
@@ -23,6 +23,7 @@ import 'package:weblibre/extensions/uri.dart';
|
|||||||
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
|
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/domain/repositories/site_permissions.dart';
|
import 'package:weblibre/features/geckoview/features/browser/domain/repositories/site_permissions.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/sheets/tracking_protection_provider.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/sheets/tracking_protection_provider.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_mode.dart';
|
||||||
|
|
||||||
part 'site_settings_badge_provider.g.dart';
|
part 'site_settings_badge_provider.g.dart';
|
||||||
|
|
||||||
@@ -48,7 +49,7 @@ Future<bool> showSiteSettingsBadge(Ref ref) async {
|
|||||||
final permissions = await ref.watch(
|
final permissions = await ref.watch(
|
||||||
sitePermissionsRepositoryProvider(
|
sitePermissionsRepositoryProvider(
|
||||||
origin: tabState.url.origin,
|
origin: tabState.url.origin,
|
||||||
isPrivate: tabState.isPrivate,
|
isPrivate: tabState.tabMode is PrivateTabMode,
|
||||||
).future,
|
).future,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -48,4 +48,4 @@ final class ShowSiteSettingsBadgeProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _$showSiteSettingsBadgeHash() =>
|
String _$showSiteSettingsBadgeHash() =>
|
||||||
r'd1499809901363705d23b17aab375fd414080544';
|
r'f44282c62556c39d439c499519a2a9861082cfcb';
|
||||||
|
|||||||
@@ -798,6 +798,22 @@ class _Browser extends HookConsumerWidget {
|
|||||||
|
|
||||||
final overlayBuilder = ref.watch(overlayControllerProvider);
|
final overlayBuilder = ref.watch(overlayControllerProvider);
|
||||||
|
|
||||||
|
Future<bool> confirmIsolatedTabCloseIfNeeded(String tabId) async {
|
||||||
|
final allStates = ref.read(tabStatesProvider);
|
||||||
|
final tabState = allStates[tabId];
|
||||||
|
final contextId = tabState?.isolationContextId;
|
||||||
|
|
||||||
|
if (contextId == null) return true;
|
||||||
|
|
||||||
|
final groupCount = allStates.values
|
||||||
|
.where((state) => state.isolationContextId == contextId)
|
||||||
|
.length;
|
||||||
|
|
||||||
|
if (groupCount > 1 || !context.mounted) return groupCount > 1;
|
||||||
|
|
||||||
|
return ui_helper.confirmIsolatedTabClose(context);
|
||||||
|
}
|
||||||
|
|
||||||
return DragTarget<TabDragData>(
|
return DragTarget<TabDragData>(
|
||||||
onMove: (details) {
|
onMove: (details) {
|
||||||
ref
|
ref
|
||||||
@@ -809,6 +825,10 @@ class _Browser extends HookConsumerWidget {
|
|||||||
},
|
},
|
||||||
onAcceptWithDetails: (details) async {
|
onAcceptWithDetails: (details) async {
|
||||||
ref.read(willAcceptDropProvider.notifier).clear();
|
ref.read(willAcceptDropProvider.notifier).clear();
|
||||||
|
if (!await confirmIsolatedTabCloseIfNeeded(details.data.tabId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await ref
|
await ref
|
||||||
.read(tabRepositoryProvider.notifier)
|
.read(tabRepositoryProvider.notifier)
|
||||||
.closeTab(details.data.tabId);
|
.closeTab(details.data.tabId);
|
||||||
@@ -916,6 +936,10 @@ class _Browser extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (tabState != null) {
|
if (tabState != null) {
|
||||||
|
if (!await confirmIsolatedTabCloseIfNeeded(tabState.id)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
await ref
|
await ref
|
||||||
.read(tabRepositoryProvider.notifier)
|
.read(tabRepositoryProvider.notifier)
|
||||||
.closeTab(tabState.id);
|
.closeTab(tabState.id);
|
||||||
@@ -933,6 +957,10 @@ class _Browser extends HookConsumerWidget {
|
|||||||
lastBackButtonPress.value = null;
|
lastBackButtonPress.value = null;
|
||||||
|
|
||||||
if (tabState != null && tabCount > 1) {
|
if (tabState != null && tabCount > 1) {
|
||||||
|
if (!await confirmIsolatedTabCloseIfNeeded(tabState.id)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
await ref
|
await ref
|
||||||
.read(tabRepositoryProvider.notifier)
|
.read(tabRepositoryProvider.notifier)
|
||||||
.closeTab(tabState.id);
|
.closeTab(tabState.id);
|
||||||
|
|||||||
+10
-2
@@ -32,6 +32,7 @@ import 'package:weblibre/features/geckoview/features/browser/domain/entities/she
|
|||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/providers/site_settings_badge_provider.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/providers/site_settings_badge_provider.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_icon.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_icon.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/toolbar_button.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/toolbar_button.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_mode.dart';
|
||||||
import 'package:weblibre/presentation/widgets/uri_breadcrumb.dart';
|
import 'package:weblibre/presentation/widgets/uri_breadcrumb.dart';
|
||||||
|
|
||||||
class AppBarTitle extends HookConsumerWidget {
|
class AppBarTitle extends HookConsumerWidget {
|
||||||
@@ -117,7 +118,7 @@ class AppBarTitle extends HookConsumerWidget {
|
|||||||
searchText: searchText.isEmpty
|
searchText: searchText.isEmpty
|
||||||
? SearchRoute.emptySearchText
|
? SearchRoute.emptySearchText
|
||||||
: searchText,
|
: searchText,
|
||||||
tabType: tabState.isPrivate ? TabType.private : TabType.regular,
|
tabType: tabState.tabMode.toTabType(),
|
||||||
).push(context);
|
).push(context);
|
||||||
},
|
},
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -158,13 +159,20 @@ class AppBarTitle extends HookConsumerWidget {
|
|||||||
],
|
],
|
||||||
icon,
|
icon,
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
if (tabState.isPrivate) ...[
|
if (tabState.tabMode is PrivateTabMode) ...[
|
||||||
Icon(
|
Icon(
|
||||||
MdiIcons.dominoMask,
|
MdiIcons.dominoMask,
|
||||||
color: appColors.privateTabPurple,
|
color: appColors.privateTabPurple,
|
||||||
size: 14,
|
size: 14,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
|
] else if (tabState.tabMode is IsolatedTabMode) ...[
|
||||||
|
Icon(
|
||||||
|
MdiIcons.shieldLock,
|
||||||
|
color: appColors.isolatedTabTeal,
|
||||||
|
size: 14,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
],
|
],
|
||||||
Expanded(
|
Expanded(
|
||||||
child: UriBreadcrumb(
|
child: UriBreadcrumb(
|
||||||
|
|||||||
+23
-6
@@ -50,6 +50,7 @@ import 'package:weblibre/features/geckoview/features/browser/presentation/widget
|
|||||||
import 'package:weblibre/features/geckoview/features/history/domain/repositories/history.dart';
|
import 'package:weblibre/features/geckoview/features/history/domain/repositories/history.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/readerview/presentation/controllers/readerable.dart';
|
import 'package:weblibre/features/geckoview/features/readerview/presentation/controllers/readerable.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/readerview/presentation/widgets/reader_button.dart';
|
import 'package:weblibre/features/geckoview/features/readerview/presentation/widgets/reader_button.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_mode.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/domain/providers.dart';
|
import 'package:weblibre/features/geckoview/features/tabs/domain/providers.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/utils/container_colors.dart';
|
import 'package:weblibre/features/geckoview/features/tabs/utils/container_colors.dart';
|
||||||
import 'package:weblibre/features/user/data/models/general_settings.dart';
|
import 'package:weblibre/features/user/data/models/general_settings.dart';
|
||||||
@@ -405,7 +406,7 @@ class BrowserTabBar extends HookConsumerWidget {
|
|||||||
typedef _QuickTabItem = ({
|
typedef _QuickTabItem = ({
|
||||||
Color? color,
|
Color? color,
|
||||||
String id,
|
String id,
|
||||||
bool isPrivate,
|
TabMode tabMode,
|
||||||
bool isHistory,
|
bool isHistory,
|
||||||
String title,
|
String title,
|
||||||
Uri url,
|
Uri url,
|
||||||
@@ -522,7 +523,10 @@ class QuickTabSwitcher extends HookConsumerWidget {
|
|||||||
scrollController: chipScrollController,
|
scrollController: chipScrollController,
|
||||||
itemId: (item) => item.id,
|
itemId: (item) => item.id,
|
||||||
labelPadding: (item) =>
|
labelPadding: (item) =>
|
||||||
(!showTitles && !item.isHistory && !item.isPrivate)
|
(!showTitles &&
|
||||||
|
!item.isHistory &&
|
||||||
|
item.tabMode is! PrivateTabMode &&
|
||||||
|
item.tabMode is! IsolatedTabMode)
|
||||||
? EdgeInsets.zero
|
? EdgeInsets.zero
|
||||||
: null,
|
: null,
|
||||||
itemLabel: (item) {
|
itemLabel: (item) {
|
||||||
@@ -534,7 +538,16 @@ class QuickTabSwitcher extends HookConsumerWidget {
|
|||||||
constraints: const BoxConstraints(maxWidth: 64),
|
constraints: const BoxConstraints(maxWidth: 64),
|
||||||
child: Text(item.title),
|
child: Text(item.title),
|
||||||
),
|
),
|
||||||
if (item.isPrivate)
|
if (item.tabMode is IsolatedTabMode)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 8.0),
|
||||||
|
child: Icon(
|
||||||
|
MdiIcons.shieldLock,
|
||||||
|
color: appColors.isolatedTabTeal,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else if (item.tabMode is PrivateTabMode)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(left: 8.0),
|
padding: const EdgeInsets.only(left: 8.0),
|
||||||
child: Icon(
|
child: Icon(
|
||||||
@@ -567,7 +580,11 @@ class QuickTabSwitcher extends HookConsumerWidget {
|
|||||||
if (item.isHistory) {
|
if (item.isHistory) {
|
||||||
await ref
|
await ref
|
||||||
.read(tabRepositoryProvider.notifier)
|
.read(tabRepositoryProvider.notifier)
|
||||||
.addTab(url: item.url, private: false, selectTab: true);
|
.addTab(
|
||||||
|
url: item.url,
|
||||||
|
tabMode: TabMode.regular,
|
||||||
|
selectTab: true,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
await ref.read(tabRepositoryProvider.notifier).selectTab(item.id);
|
await ref.read(tabRepositoryProvider.notifier).selectTab(item.id);
|
||||||
}
|
}
|
||||||
@@ -606,7 +623,7 @@ class QuickTabSwitcher extends HookConsumerWidget {
|
|||||||
(state) => (
|
(state) => (
|
||||||
id: state.$1.id,
|
id: state.$1.id,
|
||||||
title: state.$1.titleOrAuthority,
|
title: state.$1.titleOrAuthority,
|
||||||
isPrivate: state.$1.isPrivate,
|
tabMode: state.$1.tabMode,
|
||||||
isHistory: false,
|
isHistory: false,
|
||||||
url: state.$1.url,
|
url: state.$1.url,
|
||||||
color: state.$2?.color,
|
color: state.$2?.color,
|
||||||
@@ -620,7 +637,7 @@ class QuickTabSwitcher extends HookConsumerWidget {
|
|||||||
return (
|
return (
|
||||||
id: state.url,
|
id: state.url,
|
||||||
title: state.title ?? url.authority,
|
title: state.title ?? url.authority,
|
||||||
isPrivate: false,
|
tabMode: TabMode.regular,
|
||||||
isHistory: true,
|
isHistory: true,
|
||||||
url: url,
|
url: url,
|
||||||
color: null,
|
color: null,
|
||||||
|
|||||||
+20
-4
@@ -49,6 +49,7 @@ import 'package:weblibre/features/geckoview/features/browser/domain/services/pro
|
|||||||
import 'package:weblibre/features/geckoview/features/history/domain/repositories/history.dart';
|
import 'package:weblibre/features/geckoview/features/history/domain/repositories/history.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/preferences/data/repositories/preference_observer.dart';
|
import 'package:weblibre/features/geckoview/features/preferences/data/repositories/preference_observer.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/pwa/domain/providers.dart';
|
import 'package:weblibre/features/geckoview/features/pwa/domain/providers.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_mode.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/container.dart';
|
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/container.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart';
|
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart';
|
||||||
import 'package:weblibre/features/share_intent/domain/entities/shared_content.dart';
|
import 'package:weblibre/features/share_intent/domain/entities/shared_content.dart';
|
||||||
@@ -202,9 +203,11 @@ class _BrowserViewState extends ConsumerState<BrowserView>
|
|||||||
.read(tabRepositoryProvider.notifier)
|
.read(tabRepositoryProvider.notifier)
|
||||||
.addTab(
|
.addTab(
|
||||||
url: sharedContent.url,
|
url: sharedContent.url,
|
||||||
private:
|
tabMode:
|
||||||
settings.tabIntentOpenSetting ==
|
settings.tabIntentOpenSetting ==
|
||||||
TabIntentOpenSetting.private,
|
TabIntentOpenSetting.private
|
||||||
|
? TabMode.private
|
||||||
|
: TabMode.regular,
|
||||||
launchedFromIntent: true,
|
launchedFromIntent: true,
|
||||||
selectTab: true,
|
selectTab: true,
|
||||||
);
|
);
|
||||||
@@ -217,9 +220,11 @@ class _BrowserViewState extends ConsumerState<BrowserView>
|
|||||||
.read(tabRepositoryProvider.notifier)
|
.read(tabRepositoryProvider.notifier)
|
||||||
.addTab(
|
.addTab(
|
||||||
url: bang?.getTemplateUrl(sharedContent.text),
|
url: bang?.getTemplateUrl(sharedContent.text),
|
||||||
private:
|
tabMode:
|
||||||
settings.tabIntentOpenSetting ==
|
settings.tabIntentOpenSetting ==
|
||||||
TabIntentOpenSetting.private,
|
TabIntentOpenSetting.private
|
||||||
|
? TabMode.private
|
||||||
|
: TabMode.regular,
|
||||||
launchedFromIntent: true,
|
launchedFromIntent: true,
|
||||||
selectTab: true,
|
selectTab: true,
|
||||||
);
|
);
|
||||||
@@ -327,6 +332,13 @@ class _BrowserViewState extends ConsumerState<BrowserView>
|
|||||||
final router = await ref.read(routerProvider.future);
|
final router = await ref.read(routerProvider.future);
|
||||||
const route = SearchRoute(tabType: TabType.private);
|
const route = SearchRoute(tabType: TabType.private);
|
||||||
|
|
||||||
|
await router.push(route.location);
|
||||||
|
} else if (type == 'new_isolated_tab') {
|
||||||
|
lastAction = DateTime.now();
|
||||||
|
|
||||||
|
final router = await ref.read(routerProvider.future);
|
||||||
|
const route = SearchRoute(tabType: TabType.isolated);
|
||||||
|
|
||||||
await router.push(route.location);
|
await router.push(route.location);
|
||||||
} else {
|
} else {
|
||||||
throw UnimplementedError(
|
throw UnimplementedError(
|
||||||
@@ -346,6 +358,10 @@ class _BrowserViewState extends ConsumerState<BrowserView>
|
|||||||
type: 'new_private_tab',
|
type: 'new_private_tab',
|
||||||
localizedTitle: 'New Private Tab',
|
localizedTitle: 'New Private Tab',
|
||||||
),
|
),
|
||||||
|
const ShortcutItem(
|
||||||
|
type: 'new_isolated_tab',
|
||||||
|
localizedTitle: 'New Isolated Tab',
|
||||||
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
initializationCompleter.complete();
|
initializationCompleter.complete();
|
||||||
|
|||||||
+7
-6
@@ -33,6 +33,7 @@ import 'package:weblibre/features/geckoview/domain/providers.dart';
|
|||||||
import 'package:weblibre/features/geckoview/domain/providers/web_extensions_state.dart';
|
import 'package:weblibre/features/geckoview/domain/providers/web_extensions_state.dart';
|
||||||
import 'package:weblibre/features/geckoview/domain/repositories/tab.dart';
|
import 'package:weblibre/features/geckoview/domain/repositories/tab.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/extension_badge_icon.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/extension_badge_icon.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_mode.dart';
|
||||||
import 'package:weblibre/features/sync/domain/entities/sync_repository_state.dart';
|
import 'package:weblibre/features/sync/domain/entities/sync_repository_state.dart';
|
||||||
import 'package:weblibre/features/sync/domain/repositories/sync.dart';
|
import 'package:weblibre/features/sync/domain/repositories/sync.dart';
|
||||||
import 'package:weblibre/features/tor/domain/services/tor_proxy.dart';
|
import 'package:weblibre/features/tor/domain/services/tor_proxy.dart';
|
||||||
@@ -318,17 +319,17 @@ class _ExtensionsSection extends HookConsumerWidget {
|
|||||||
dense: true,
|
dense: true,
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
Navigator.of(context).pop(); // Close drawer
|
Navigator.of(context).pop(); // Close drawer
|
||||||
final isPrivate =
|
final tabMode = TabMode.fromTabType(
|
||||||
ref
|
ref
|
||||||
.read(generalSettingsWithDefaultsProvider)
|
.read(generalSettingsWithDefaultsProvider)
|
||||||
.defaultCreateTabType ==
|
.defaultCreateTabType,
|
||||||
TabType.private;
|
);
|
||||||
|
|
||||||
await ref
|
await ref
|
||||||
.read(tabRepositoryProvider.notifier)
|
.read(tabRepositoryProvider.notifier)
|
||||||
.addTab(
|
.addTab(
|
||||||
url: Uri.parse('https://addons.mozilla.org'),
|
url: Uri.parse('https://addons.mozilla.org'),
|
||||||
private: isPrivate,
|
tabMode: tabMode,
|
||||||
containerSelection:
|
containerSelection:
|
||||||
const TabContainerSelection.unassigned(),
|
const TabContainerSelection.unassigned(),
|
||||||
selectTab: true,
|
selectTab: true,
|
||||||
|
|||||||
+3
-4
@@ -29,6 +29,7 @@ import 'package:weblibre/features/geckoview/features/browser/presentation/widget
|
|||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/sheets/clear_site_data_section.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/sheets/clear_site_data_section.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/sheets/permissions_section.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/sheets/permissions_section.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/sheets/tracking_protection_section.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/sheets/tracking_protection_section.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_mode.dart';
|
||||||
import 'package:weblibre/presentation/widgets/website_title_tile.dart';
|
import 'package:weblibre/presentation/widgets/website_title_tile.dart';
|
||||||
|
|
||||||
class ClampingScrollPhysicsWithoutImplicit extends ClampingScrollPhysics {
|
class ClampingScrollPhysicsWithoutImplicit extends ClampingScrollPhysics {
|
||||||
@@ -121,9 +122,7 @@ class ViewTabSheetWidget extends HookConsumerWidget {
|
|||||||
searchText: searchText.isEmpty
|
searchText: searchText.isEmpty
|
||||||
? SearchRoute.emptySearchText
|
? SearchRoute.emptySearchText
|
||||||
: searchText,
|
: searchText,
|
||||||
tabType: initialTabState.isPrivate
|
tabType: initialTabState.tabMode.toTabType(),
|
||||||
? TabType.private
|
|
||||||
: TabType.regular,
|
|
||||||
).push(context);
|
).push(context);
|
||||||
},
|
},
|
||||||
child: WebsiteTitleTile(initialTabState),
|
child: WebsiteTitleTile(initialTabState),
|
||||||
@@ -151,7 +150,7 @@ class ViewTabSheetWidget extends HookConsumerWidget {
|
|||||||
// Permissions Section
|
// Permissions Section
|
||||||
PermissionsSection(
|
PermissionsSection(
|
||||||
origin: initialTabState.url.origin,
|
origin: initialTabState.url.origin,
|
||||||
isPrivate: initialTabState.isPrivate,
|
isPrivate: initialTabState.tabMode is PrivateTabMode,
|
||||||
),
|
),
|
||||||
const Divider(),
|
const Divider(),
|
||||||
// Clear Site Data Section
|
// Clear Site Data Section
|
||||||
|
|||||||
+11
@@ -20,6 +20,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
|
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:weblibre/core/design/app_colors.dart';
|
||||||
import 'package:weblibre/core/routing/routes.dart';
|
import 'package:weblibre/core/routing/routes.dart';
|
||||||
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
||||||
import 'package:weblibre/presentation/icons/weblibre_icons.dart';
|
import 'package:weblibre/presentation/icons/weblibre_icons.dart';
|
||||||
@@ -62,6 +63,16 @@ class TabCreationMenu extends HookConsumerWidget {
|
|||||||
leadingIcon: const Icon(WebLibreIcons.privateTab),
|
leadingIcon: const Icon(WebLibreIcons.privateTab),
|
||||||
child: const Text('Add Private Tab'),
|
child: const Text('Add Private Tab'),
|
||||||
),
|
),
|
||||||
|
MenuItemButton(
|
||||||
|
onPressed: () async {
|
||||||
|
await const SearchRoute(tabType: TabType.isolated).push(context);
|
||||||
|
},
|
||||||
|
leadingIcon: Icon(
|
||||||
|
MdiIcons.shieldLock,
|
||||||
|
color: AppColors.of(context).isolatedTabTeal,
|
||||||
|
),
|
||||||
|
child: const Text('Add Isolated Tab'),
|
||||||
|
),
|
||||||
if (createChildTabsOption)
|
if (createChildTabsOption)
|
||||||
MenuItemButton(
|
MenuItemButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import 'package:flutter_material_design_icons/flutter_material_design_icons.dart
|
|||||||
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:nullability/nullability.dart';
|
import 'package:nullability/nullability.dart';
|
||||||
|
import 'package:weblibre/core/design/app_colors.dart';
|
||||||
import 'package:weblibre/core/routing/routes.dart';
|
import 'package:weblibre/core/routing/routes.dart';
|
||||||
import 'package:weblibre/features/geckoview/domain/controllers/bottom_sheet.dart';
|
import 'package:weblibre/features/geckoview/domain/controllers/bottom_sheet.dart';
|
||||||
import 'package:weblibre/features/geckoview/domain/entities/tab_container_selection.dart';
|
import 'package:weblibre/features/geckoview/domain/entities/tab_container_selection.dart';
|
||||||
@@ -41,6 +42,7 @@ import 'package:weblibre/features/geckoview/features/pwa/domain/providers.dart';
|
|||||||
import 'package:weblibre/features/geckoview/features/pwa/presentation/widgets/pwa_install_button.dart';
|
import 'package:weblibre/features/geckoview/features/pwa/presentation/widgets/pwa_install_button.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/readerview/presentation/controllers/readerable.dart';
|
import 'package:weblibre/features/geckoview/features/readerview/presentation/controllers/readerable.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/readerview/presentation/widgets/reader_button.dart';
|
import 'package:weblibre/features/geckoview/features/readerview/presentation/widgets/reader_button.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/container_data.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/domain/entities/container_selection_result.dart';
|
import 'package:weblibre/features/geckoview/features/tabs/domain/entities/container_selection_result.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/domain/providers.dart';
|
import 'package:weblibre/features/geckoview/features/tabs/domain/providers.dart';
|
||||||
@@ -234,12 +236,12 @@ class TabMenu extends HookConsumerWidget {
|
|||||||
.read(tabDataRepositoryProvider.notifier)
|
.read(tabDataRepositoryProvider.notifier)
|
||||||
.getTabContainerData(selectedTabId);
|
.getTabContainerData(selectedTabId);
|
||||||
|
|
||||||
final tabId = (tabState.isPrivate)
|
final tabId = (tabState.tabMode is! RegularTabMode)
|
||||||
? await ref
|
? await ref
|
||||||
.read(tabRepositoryProvider.notifier)
|
.read(tabRepositoryProvider.notifier)
|
||||||
.addTab(
|
.addTab(
|
||||||
|
tabMode: TabMode.regular,
|
||||||
url: tabState.url,
|
url: tabState.url,
|
||||||
private: false,
|
|
||||||
containerSelection: containerData == null
|
containerSelection: containerData == null
|
||||||
? const TabContainerSelection.unassigned()
|
? const TabContainerSelection.unassigned()
|
||||||
: TabContainerSelection.specific(
|
: TabContainerSelection.specific(
|
||||||
@@ -277,12 +279,12 @@ class TabMenu extends HookConsumerWidget {
|
|||||||
.read(tabDataRepositoryProvider.notifier)
|
.read(tabDataRepositoryProvider.notifier)
|
||||||
.getTabContainerData(selectedTabId);
|
.getTabContainerData(selectedTabId);
|
||||||
|
|
||||||
final tabId = (!tabState.isPrivate)
|
final tabId = (tabState.tabMode is! PrivateTabMode)
|
||||||
? await ref
|
? await ref
|
||||||
.read(tabRepositoryProvider.notifier)
|
.read(tabRepositoryProvider.notifier)
|
||||||
.addTab(
|
.addTab(
|
||||||
url: tabState.url,
|
url: tabState.url,
|
||||||
private: true,
|
tabMode: TabMode.private,
|
||||||
containerSelection: containerData == null
|
containerSelection: containerData == null
|
||||||
? const TabContainerSelection.unassigned()
|
? const TabContainerSelection.unassigned()
|
||||||
: TabContainerSelection.specific(
|
: TabContainerSelection.specific(
|
||||||
@@ -302,6 +304,41 @@ class TabMenu extends HookConsumerWidget {
|
|||||||
//save reference before pop `ref` gets disposed
|
//save reference before pop `ref` gets disposed
|
||||||
final repo = ref.read(tabRepositoryProvider.notifier);
|
final repo = ref.read(tabRepositoryProvider.notifier);
|
||||||
|
|
||||||
|
ui_helper.showTabSwitchMessage(
|
||||||
|
context,
|
||||||
|
onSwitch: () async {
|
||||||
|
await repo.selectTab(tabId);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
MenuItemButton(
|
||||||
|
leadingIcon: Icon(
|
||||||
|
MdiIcons.shieldLock,
|
||||||
|
color: AppColors.of(context).isolatedTabTeal,
|
||||||
|
),
|
||||||
|
child: const Text('Isolated'),
|
||||||
|
onPressed: () async {
|
||||||
|
final tabState = ref.read(tabStateProvider(selectedTabId))!;
|
||||||
|
final containerData = await ref
|
||||||
|
.read(tabDataRepositoryProvider.notifier)
|
||||||
|
.getTabContainerData(selectedTabId);
|
||||||
|
|
||||||
|
final tabId = await ref
|
||||||
|
.read(tabRepositoryProvider.notifier)
|
||||||
|
.addTab(
|
||||||
|
url: tabState.url,
|
||||||
|
tabMode: TabMode.newIsolated(),
|
||||||
|
containerSelection: containerData == null
|
||||||
|
? const TabContainerSelection.unassigned()
|
||||||
|
: TabContainerSelection.specific(containerData),
|
||||||
|
selectTab: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
|
final repo = ref.read(tabRepositoryProvider.notifier);
|
||||||
|
|
||||||
ui_helper.showTabSwitchMessage(
|
ui_helper.showTabSwitchMessage(
|
||||||
context,
|
context,
|
||||||
onSwitch: () async {
|
onSwitch: () async {
|
||||||
@@ -540,6 +577,24 @@ class TabMenu extends HookConsumerWidget {
|
|||||||
if (enableCloseTab)
|
if (enableCloseTab)
|
||||||
MenuItemButton(
|
MenuItemButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
|
// Confirm before closing the last tab in an isolation group
|
||||||
|
final tabState = ref.read(tabStateProvider(selectedTabId));
|
||||||
|
if (tabState != null && tabState.tabMode is IsolatedTabMode) {
|
||||||
|
final allStates = ref.read(tabStatesProvider);
|
||||||
|
final groupCount = allStates.values
|
||||||
|
.where(
|
||||||
|
(s) =>
|
||||||
|
s.isolationContextId == tabState.isolationContextId,
|
||||||
|
)
|
||||||
|
.length;
|
||||||
|
if (groupCount <= 1 && context.mounted) {
|
||||||
|
final confirmed = await ui_helper.confirmIsolatedTabClose(
|
||||||
|
context,
|
||||||
|
);
|
||||||
|
if (!confirmed) return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await ref
|
await ref
|
||||||
.read(tabRepositoryProvider.notifier)
|
.read(tabRepositoryProvider.notifier)
|
||||||
.closeTab(selectedTabId);
|
.closeTab(selectedTabId);
|
||||||
|
|||||||
+93
-30
@@ -31,6 +31,7 @@ import 'package:weblibre/features/geckoview/domain/repositories/tab.dart';
|
|||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_icon.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_icon.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/find_in_page/domain/entities/find_in_page_state.dart';
|
import 'package:weblibre/features/geckoview/features/find_in_page/domain/entities/find_in_page_state.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/find_in_page/presentation/controllers/find_in_page.dart';
|
import 'package:weblibre/features/geckoview/features/find_in_page/presentation/controllers/find_in_page.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_mode.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart';
|
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart';
|
||||||
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
||||||
import 'package:weblibre/presentation/hooks/menu_controller.dart';
|
import 'package:weblibre/presentation/hooks/menu_controller.dart';
|
||||||
@@ -39,14 +40,35 @@ import 'package:weblibre/presentation/widgets/uri_breadcrumb.dart';
|
|||||||
import 'package:weblibre/presentation/widgets/url_icon.dart';
|
import 'package:weblibre/presentation/widgets/url_icon.dart';
|
||||||
import 'package:weblibre/utils/ui_helper.dart' as ui_helper;
|
import 'package:weblibre/utils/ui_helper.dart' as ui_helper;
|
||||||
|
|
||||||
|
Future<bool> _confirmIsolatedTabCloseIfNeeded(
|
||||||
|
BuildContext context,
|
||||||
|
WidgetRef ref,
|
||||||
|
String tabId,
|
||||||
|
) async {
|
||||||
|
final allStates = ref.read(tabStatesProvider);
|
||||||
|
final tabState = allStates[tabId];
|
||||||
|
final contextId = tabState?.isolationContextId;
|
||||||
|
|
||||||
|
if (contextId == null) return true;
|
||||||
|
|
||||||
|
final groupCount = allStates.values
|
||||||
|
.where((state) => state.isolationContextId == contextId)
|
||||||
|
.length;
|
||||||
|
|
||||||
|
if (groupCount > 1) return true;
|
||||||
|
if (!context.mounted) return false;
|
||||||
|
|
||||||
|
return ui_helper.confirmIsolatedTabClose(context);
|
||||||
|
}
|
||||||
|
|
||||||
class GridTabItemContainer extends StatelessWidget {
|
class GridTabItemContainer extends StatelessWidget {
|
||||||
final bool isActive;
|
final bool isActive;
|
||||||
final bool isPrivate;
|
final TabMode tabMode;
|
||||||
final Widget? child;
|
final Widget? child;
|
||||||
|
|
||||||
const GridTabItemContainer({
|
const GridTabItemContainer({
|
||||||
required this.isActive,
|
required this.isActive,
|
||||||
required this.isPrivate,
|
required this.tabMode,
|
||||||
this.child,
|
this.child,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
@@ -56,6 +78,12 @@ class GridTabItemContainer extends StatelessWidget {
|
|||||||
final colorScheme = Theme.of(context).colorScheme;
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
final appColors = AppColors.of(context);
|
final appColors = AppColors.of(context);
|
||||||
|
|
||||||
|
final bgColor = switch (tabMode) {
|
||||||
|
PrivateTabMode() => appColors.privateTabBackground,
|
||||||
|
IsolatedTabMode() => appColors.isolatedTabBackground,
|
||||||
|
RegularTabMode() => colorScheme.surfaceContainerHighest,
|
||||||
|
};
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
@@ -65,9 +93,7 @@ class GridTabItemContainer extends StatelessWidget {
|
|||||||
borderRadius: const BorderRadius.all(Radius.circular(16.0)),
|
borderRadius: const BorderRadius.all(Radius.circular(16.0)),
|
||||||
),
|
),
|
||||||
child: Material(
|
child: Material(
|
||||||
color: isPrivate
|
color: bgColor,
|
||||||
? appColors.privateTabBackground
|
|
||||||
: colorScheme.surfaceContainerHighest,
|
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(14.0)),
|
borderRadius: const BorderRadius.all(Radius.circular(14.0)),
|
||||||
child: child,
|
child: child,
|
||||||
),
|
),
|
||||||
@@ -113,9 +139,15 @@ class GridTabPreview extends HookConsumerWidget {
|
|||||||
|
|
||||||
final extendedDeleteMenuController = useMenuController();
|
final extendedDeleteMenuController = useMenuController();
|
||||||
|
|
||||||
|
final modeTextColor = switch (tabState.tabMode) {
|
||||||
|
PrivateTabMode() => appColors.privateTabForeground,
|
||||||
|
IsolatedTabMode() => appColors.isolatedTabForeground,
|
||||||
|
RegularTabMode() => null,
|
||||||
|
};
|
||||||
|
|
||||||
return GridTabItemContainer(
|
return GridTabItemContainer(
|
||||||
isActive: isActive,
|
isActive: isActive,
|
||||||
isPrivate: tabState.isPrivate,
|
tabMode: tabState.tabMode,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(14.0)),
|
borderRadius: const BorderRadius.all(Radius.circular(14.0)),
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
@@ -132,8 +164,8 @@ class GridTabPreview extends HookConsumerWidget {
|
|||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
tabState.titleOrAuthority,
|
tabState.titleOrAuthority,
|
||||||
maxLines: 2,
|
maxLines: 2,
|
||||||
style: tabState.isPrivate
|
style: modeTextColor != null
|
||||||
? TextStyle(color: appColors.privateTabForeground)
|
? TextStyle(color: modeTextColor)
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -183,14 +215,13 @@ class GridTabPreview extends HookConsumerWidget {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
tabState.url.authority,
|
tabState.url.authority,
|
||||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
style: Theme.of(
|
||||||
color: tabState.isPrivate
|
context,
|
||||||
? appColors.privateTabForeground
|
).textTheme.bodySmall?.copyWith(color: modeTextColor),
|
||||||
: null,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (tabState.isPrivate) ...[
|
if (tabState.tabMode is PrivateTabMode ||
|
||||||
|
tabState.tabMode is IsolatedTabMode) ...[
|
||||||
const SizedBox(width: 6.0),
|
const SizedBox(width: 6.0),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 16,
|
height: 16,
|
||||||
@@ -201,8 +232,12 @@ class GridTabPreview extends HookConsumerWidget {
|
|||||||
Positioned(
|
Positioned(
|
||||||
top: -4,
|
top: -4,
|
||||||
child: Icon(
|
child: Icon(
|
||||||
MdiIcons.dominoMask,
|
tabState.tabMode is IsolatedTabMode
|
||||||
color: appColors.privateTabPurple,
|
? MdiIcons.shieldLock
|
||||||
|
: MdiIcons.dominoMask,
|
||||||
|
color: tabState.tabMode is IsolatedTabMode
|
||||||
|
? appColors.isolatedTabTeal
|
||||||
|
: appColors.privateTabPurple,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -290,9 +325,25 @@ class ListTabPreview extends HookConsumerWidget {
|
|||||||
_ => TabIcon(tabState: tabState, iconSize: 32),
|
_ => TabIcon(tabState: tabState, iconSize: 32),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
final listBgColor = switch (tabState.tabMode) {
|
||||||
|
PrivateTabMode() => appColors.privateTabBackground,
|
||||||
|
IsolatedTabMode() => appColors.isolatedTabBackground,
|
||||||
|
RegularTabMode() => null,
|
||||||
|
};
|
||||||
|
final listTextColor = switch (tabState.tabMode) {
|
||||||
|
PrivateTabMode() => appColors.privateTabForeground,
|
||||||
|
IsolatedTabMode() => appColors.isolatedTabForeground,
|
||||||
|
RegularTabMode() => null,
|
||||||
|
};
|
||||||
|
final (modeBadgeIcon, modeBadgeColor) = switch (tabState.tabMode) {
|
||||||
|
PrivateTabMode() => (MdiIcons.dominoMask, appColors.privateTabPurple),
|
||||||
|
IsolatedTabMode() => (MdiIcons.shieldLock, appColors.isolatedTabTeal),
|
||||||
|
RegularTabMode() => (null, null),
|
||||||
|
};
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: tabState.isPrivate ? appColors.privateTabBackground : null,
|
color: listBgColor,
|
||||||
border: isActive ? Border.all(color: colorScheme.primary) : null,
|
border: isActive ? Border.all(color: colorScheme.primary) : null,
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(4.0)),
|
borderRadius: const BorderRadius.all(Radius.circular(4.0)),
|
||||||
),
|
),
|
||||||
@@ -309,28 +360,22 @@ class ListTabPreview extends HookConsumerWidget {
|
|||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
tabState.titleOrAuthority,
|
tabState.titleOrAuthority,
|
||||||
maxLines: 2,
|
maxLines: 2,
|
||||||
style: tabState.isPrivate
|
style: listTextColor != null
|
||||||
? TextStyle(color: appColors.privateTabForeground)
|
? TextStyle(color: listTextColor)
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
subtitle: Row(
|
subtitle: Row(
|
||||||
children: [
|
children: [
|
||||||
if (tabState.isPrivate) ...[
|
if (modeBadgeIcon != null) ...[
|
||||||
Icon(
|
Icon(modeBadgeIcon, color: modeBadgeColor, size: 14),
|
||||||
MdiIcons.dominoMask,
|
|
||||||
color: appColors.privateTabPurple,
|
|
||||||
size: 14,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
],
|
],
|
||||||
Expanded(
|
Expanded(
|
||||||
child: UriBreadcrumb(
|
child: UriBreadcrumb(
|
||||||
uri: tabState.url,
|
uri: tabState.url,
|
||||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
style: Theme.of(
|
||||||
color: tabState.isPrivate
|
context,
|
||||||
? appColors.privateTabForeground
|
).textTheme.bodySmall?.copyWith(color: listTextColor),
|
||||||
: null,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -463,6 +508,11 @@ class SingleGridTabPreview extends HookConsumerWidget {
|
|||||||
},
|
},
|
||||||
onHorizontalDragEnd: (details) async {
|
onHorizontalDragEnd: (details) async {
|
||||||
if (draggedDistance.value >= deleteThreshold) {
|
if (draggedDistance.value >= deleteThreshold) {
|
||||||
|
if (!await _confirmIsolatedTabCloseIfNeeded(context, ref, tabId)) {
|
||||||
|
draggedDistance.value = 0.0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await ref.read(tabRepositoryProvider.notifier).closeTab(tabId);
|
await ref.read(tabRepositoryProvider.notifier).closeTab(tabId);
|
||||||
|
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
@@ -525,6 +575,10 @@ class SingleGridTabPreview extends HookConsumerWidget {
|
|||||||
onDelete: () async {
|
onDelete: () async {
|
||||||
onBeforeDelete?.call();
|
onBeforeDelete?.call();
|
||||||
|
|
||||||
|
if (!await _confirmIsolatedTabCloseIfNeeded(context, ref, tabId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await ref.read(tabRepositoryProvider.notifier).closeTab(tabId);
|
await ref.read(tabRepositoryProvider.notifier).closeTab(tabId);
|
||||||
|
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
@@ -579,6 +633,11 @@ class SingleListTabPreview extends HookConsumerWidget {
|
|||||||
},
|
},
|
||||||
onHorizontalDragEnd: (details) async {
|
onHorizontalDragEnd: (details) async {
|
||||||
if (draggedDistance.value >= deleteThreshold) {
|
if (draggedDistance.value >= deleteThreshold) {
|
||||||
|
if (!await _confirmIsolatedTabCloseIfNeeded(context, ref, tabId)) {
|
||||||
|
draggedDistance.value = 0.0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await ref.read(tabRepositoryProvider.notifier).closeTab(tabId);
|
await ref.read(tabRepositoryProvider.notifier).closeTab(tabId);
|
||||||
|
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
@@ -641,6 +700,10 @@ class SingleListTabPreview extends HookConsumerWidget {
|
|||||||
onDelete: () async {
|
onDelete: () async {
|
||||||
onBeforeDelete?.call();
|
onBeforeDelete?.call();
|
||||||
|
|
||||||
|
if (!await _confirmIsolatedTabCloseIfNeeded(context, ref, tabId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await ref.read(tabRepositoryProvider.notifier).closeTab(tabId);
|
await ref.read(tabRepositoryProvider.notifier).closeTab(tabId);
|
||||||
|
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
|
|||||||
+31
-4
@@ -71,11 +71,11 @@ class _TabTreePreview extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final isTabPrivate = ref.watch(
|
final tabMode = ref.watch(
|
||||||
tabStateProvider(entity.tabId).select((value) => value?.isPrivate),
|
tabStateProvider(entity.tabId).select((value) => value?.tabMode),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isTabPrivate == null) {
|
if (tabMode == null) {
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,7 +89,7 @@ class _TabTreePreview extends HookConsumerWidget {
|
|||||||
stackCount,
|
stackCount,
|
||||||
GridTabItemContainer(
|
GridTabItemContainer(
|
||||||
isActive: entity.tabId == activeTabId,
|
isActive: entity.tabId == activeTabId,
|
||||||
isPrivate: isTabPrivate == true,
|
tabMode: tabMode,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
_addPadding(
|
_addPadding(
|
||||||
@@ -152,6 +152,33 @@ class _TabTreePreview extends HookConsumerWidget {
|
|||||||
.read(tabDataRepositoryProvider.notifier)
|
.read(tabDataRepositoryProvider.notifier)
|
||||||
.getTabDescendants(entity.rootId);
|
.getTabDescendants(entity.rootId);
|
||||||
|
|
||||||
|
final allStates = ref.read(tabStatesProvider);
|
||||||
|
final isolatedContextInCloseSet = <String, int>{};
|
||||||
|
|
||||||
|
for (final tabId in tabs.keys) {
|
||||||
|
final contextId = allStates[tabId]?.isolationContextId;
|
||||||
|
if (contextId == null) continue;
|
||||||
|
isolatedContextInCloseSet[contextId] =
|
||||||
|
(isolatedContextInCloseSet[contextId] ?? 0) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
final groupsToDelete = isolatedContextInCloseSet.entries.where((
|
||||||
|
entry,
|
||||||
|
) {
|
||||||
|
final totalInGroup = allStates.values
|
||||||
|
.where((state) => state.isolationContextId == entry.key)
|
||||||
|
.length;
|
||||||
|
return totalInGroup == entry.value;
|
||||||
|
}).length;
|
||||||
|
|
||||||
|
if (groupsToDelete > 0 && context.mounted) {
|
||||||
|
final confirmed = await ui_helper.confirmIsolatedTabClose(
|
||||||
|
context,
|
||||||
|
groupCount: groupsToDelete,
|
||||||
|
);
|
||||||
|
if (!confirmed) return;
|
||||||
|
}
|
||||||
|
|
||||||
await ref
|
await ref
|
||||||
.read(tabRepositoryProvider.notifier)
|
.read(tabRepositoryProvider.notifier)
|
||||||
.closeTabs(tabs.keys.toList());
|
.closeTabs(tabs.keys.toList());
|
||||||
|
|||||||
+66
-4
@@ -25,10 +25,12 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
|||||||
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
|
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
|
||||||
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:weblibre/core/design/app_colors.dart';
|
||||||
import 'package:weblibre/core/logger.dart';
|
import 'package:weblibre/core/logger.dart';
|
||||||
import 'package:weblibre/core/routing/routes.dart';
|
import 'package:weblibre/core/routing/routes.dart';
|
||||||
import 'package:weblibre/features/geckoview/domain/entities/tab_container_selection.dart';
|
import 'package:weblibre/features/geckoview/domain/entities/tab_container_selection.dart';
|
||||||
import 'package:weblibre/features/geckoview/domain/providers.dart';
|
import 'package:weblibre/features/geckoview/domain/providers.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
|
||||||
import 'package:weblibre/features/geckoview/domain/repositories/tab.dart';
|
import 'package:weblibre/features/geckoview/domain/repositories/tab.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/bookmarks/domain/repositories/bookmarks.dart';
|
import 'package:weblibre/features/geckoview/features/bookmarks/domain/repositories/bookmarks.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/domain/services/browser_data.dart';
|
import 'package:weblibre/features/geckoview/features/browser/domain/services/browser_data.dart';
|
||||||
@@ -39,6 +41,7 @@ import 'package:weblibre/features/geckoview/features/browser/presentation/widget
|
|||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_view/dialogs/close_all_private_tabs_dialog.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_view/dialogs/close_all_private_tabs_dialog.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_view/dialogs/close_all_tabs_dialog.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_view/dialogs/close_all_tabs_dialog.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_view/dialogs/enable_ai_tab_suggestions_dialog.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_view/dialogs/enable_ai_tab_suggestions_dialog.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_mode.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/domain/providers/selected_container.dart';
|
import 'package:weblibre/features/geckoview/features/tabs/domain/providers/selected_container.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart';
|
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab_search.dart';
|
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab_search.dart';
|
||||||
@@ -482,6 +485,7 @@ class TabViewHeader extends HookConsumerWidget {
|
|||||||
.closeContainerTabs(
|
.closeContainerTabs(
|
||||||
selectedContainerId,
|
selectedContainerId,
|
||||||
includeRegular: false,
|
includeRegular: false,
|
||||||
|
includeIsolated: false,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
@@ -499,6 +503,55 @@ class TabViewHeader extends HookConsumerWidget {
|
|||||||
},
|
},
|
||||||
child: const Text('Close Private Tabs'),
|
child: const Text('Close Private Tabs'),
|
||||||
),
|
),
|
||||||
|
MenuItemButton(
|
||||||
|
leadingIcon: Icon(
|
||||||
|
MdiIcons.shieldLock,
|
||||||
|
color: AppColors.of(context).isolatedTabTeal,
|
||||||
|
),
|
||||||
|
onPressed: isSyncedScope
|
||||||
|
? null
|
||||||
|
: () async {
|
||||||
|
// Count distinct isolation groups that will be destroyed
|
||||||
|
final allStates = ref.read(tabStatesProvider);
|
||||||
|
final isolatedContextIds = allStates.values
|
||||||
|
.where(
|
||||||
|
(s) =>
|
||||||
|
s.tabMode is IsolatedTabMode &&
|
||||||
|
s.isolationContextId != null,
|
||||||
|
)
|
||||||
|
.map((s) => s.isolationContextId!)
|
||||||
|
.toSet();
|
||||||
|
|
||||||
|
if (isolatedContextIds.isNotEmpty &&
|
||||||
|
context.mounted) {
|
||||||
|
final confirmed = await ui_helper
|
||||||
|
.confirmIsolatedTabClose(
|
||||||
|
context,
|
||||||
|
groupCount: isolatedContextIds.length,
|
||||||
|
);
|
||||||
|
if (!confirmed) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final count = await ref
|
||||||
|
.read(tabDataRepositoryProvider.notifier)
|
||||||
|
.closeContainerTabs(
|
||||||
|
selectedContainerId,
|
||||||
|
includeRegular: false,
|
||||||
|
includePrivate: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
|
ui_helper.showTabUndoClose(
|
||||||
|
context,
|
||||||
|
ref
|
||||||
|
.read(tabRepositoryProvider.notifier)
|
||||||
|
.undoClose,
|
||||||
|
count: count.length,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: const Text('Close Isolated Tabs'),
|
||||||
|
),
|
||||||
const Divider(),
|
const Divider(),
|
||||||
MenuItemButton(
|
MenuItemButton(
|
||||||
leadingIcon: const Icon(MdiIcons.bookmarkPlusOutline),
|
leadingIcon: const Icon(MdiIcons.bookmarkPlusOutline),
|
||||||
@@ -652,14 +705,23 @@ class TabViewHeader extends HookConsumerWidget {
|
|||||||
startLoading: true,
|
startLoading: true,
|
||||||
parentId: parentId,
|
parentId: parentId,
|
||||||
private:
|
private:
|
||||||
tab.isPrivate ?? false,
|
tab.tabMode ==
|
||||||
|
TabModeDbValue.private,
|
||||||
flags: LoadUrlFlags.NONE
|
flags: LoadUrlFlags.NONE
|
||||||
.toValue(),
|
.toValue(),
|
||||||
source: Internal.newTab
|
source: Internal.newTab
|
||||||
.toValue(),
|
.toValue(),
|
||||||
contextId: selectedContainer
|
contextId:
|
||||||
.metadata
|
tab.tabMode ==
|
||||||
.contextualIdentity,
|
TabModeDbValue
|
||||||
|
.isolated
|
||||||
|
? tab.isolationContextId ??
|
||||||
|
selectedContainer
|
||||||
|
.metadata
|
||||||
|
.contextualIdentity
|
||||||
|
: selectedContainer
|
||||||
|
.metadata
|
||||||
|
.contextualIdentity,
|
||||||
);
|
);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
containerSelection:
|
containerSelection:
|
||||||
|
|||||||
+2
-1
@@ -24,6 +24,7 @@ import 'package:go_router/go_router.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
|
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/contextmenu/extensions/hit_result.dart';
|
import 'package:weblibre/features/geckoview/features/contextmenu/extensions/hit_result.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_mode.dart';
|
||||||
|
|
||||||
class CopyImage extends HookConsumerWidget {
|
class CopyImage extends HookConsumerWidget {
|
||||||
final HitResult hitResult;
|
final HitResult hitResult;
|
||||||
@@ -47,7 +48,7 @@ class CopyImage extends HookConsumerWidget {
|
|||||||
await GeckoDownloadsService().copyInternetResource(
|
await GeckoDownloadsService().copyInternetResource(
|
||||||
currentTab.id,
|
currentTab.id,
|
||||||
url: url,
|
url: url,
|
||||||
isPrivate: currentTab.isPrivate,
|
isPrivate: currentTab.tabMode is PrivateTabMode,
|
||||||
referrerUrl: currentTab.url,
|
referrerUrl: currentTab.url,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
+9
-8
@@ -22,10 +22,10 @@ import 'package:flutter_material_design_icons/flutter_material_design_icons.dart
|
|||||||
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:weblibre/core/routing/routes.dart';
|
|
||||||
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
|
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
|
||||||
import 'package:weblibre/features/geckoview/domain/repositories/tab.dart';
|
import 'package:weblibre/features/geckoview/domain/repositories/tab.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/contextmenu/extensions/hit_result.dart';
|
import 'package:weblibre/features/geckoview/features/contextmenu/extensions/hit_result.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_mode.dart';
|
||||||
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
||||||
import 'package:weblibre/utils/ui_helper.dart';
|
import 'package:weblibre/utils/ui_helper.dart';
|
||||||
|
|
||||||
@@ -45,12 +45,13 @@ class OpenImageInNewTab extends HookConsumerWidget {
|
|||||||
title: const Text('Open image in new tab'),
|
title: const Text('Open image in new tab'),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
final currentTab = ref.read(selectedTabStateProvider);
|
final currentTab = ref.read(selectedTabStateProvider);
|
||||||
final isPrivate =
|
final tabMode =
|
||||||
currentTab?.isPrivate ??
|
currentTab?.tabMode ??
|
||||||
ref
|
TabMode.fromTabType(
|
||||||
.read(generalSettingsWithDefaultsProvider)
|
ref
|
||||||
.defaultCreateTabType ==
|
.read(generalSettingsWithDefaultsProvider)
|
||||||
TabType.private;
|
.defaultCreateTabType,
|
||||||
|
);
|
||||||
|
|
||||||
final tabId = await ref
|
final tabId = await ref
|
||||||
.read(tabRepositoryProvider.notifier)
|
.read(tabRepositoryProvider.notifier)
|
||||||
@@ -58,7 +59,7 @@ class OpenImageInNewTab extends HookConsumerWidget {
|
|||||||
url: hitResult.tryGetSource(),
|
url: hitResult.tryGetSource(),
|
||||||
parentId: currentTab?.id,
|
parentId: currentTab?.id,
|
||||||
selectTab: false,
|
selectTab: false,
|
||||||
private: isPrivate,
|
tabMode: tabMode,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
|
|||||||
+9
-7
@@ -32,6 +32,7 @@ import 'package:weblibre/features/geckoview/domain/entities/tab_container_select
|
|||||||
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
|
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
|
||||||
import 'package:weblibre/features/geckoview/domain/repositories/tab.dart';
|
import 'package:weblibre/features/geckoview/domain/repositories/tab.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/contextmenu/extensions/hit_result.dart';
|
import 'package:weblibre/features/geckoview/features/contextmenu/extensions/hit_result.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/container_data.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/domain/providers.dart';
|
import 'package:weblibre/features/geckoview/features/tabs/domain/providers.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/container.dart';
|
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/container.dart';
|
||||||
@@ -89,12 +90,13 @@ class OpenInContainer extends HookConsumerWidget {
|
|||||||
if (selectedContainer == null || !context.mounted) return;
|
if (selectedContainer == null || !context.mounted) return;
|
||||||
|
|
||||||
final currentTab = ref.read(selectedTabStateProvider);
|
final currentTab = ref.read(selectedTabStateProvider);
|
||||||
final isPrivate =
|
final tabMode =
|
||||||
currentTab?.isPrivate ??
|
currentTab?.tabMode ??
|
||||||
ref
|
TabMode.fromTabType(
|
||||||
.read(generalSettingsWithDefaultsProvider)
|
ref
|
||||||
.defaultCreateTabType ==
|
.read(generalSettingsWithDefaultsProvider)
|
||||||
TabType.private;
|
.defaultCreateTabType,
|
||||||
|
);
|
||||||
|
|
||||||
final tabId = await ref
|
final tabId = await ref
|
||||||
.read(tabRepositoryProvider.notifier)
|
.read(tabRepositoryProvider.notifier)
|
||||||
@@ -102,7 +104,7 @@ class OpenInContainer extends HookConsumerWidget {
|
|||||||
url: hitResult.tryGetLink(),
|
url: hitResult.tryGetLink(),
|
||||||
parentId: currentTab?.id,
|
parentId: currentTab?.id,
|
||||||
selectTab: false,
|
selectTab: false,
|
||||||
private: isPrivate,
|
tabMode: tabMode,
|
||||||
containerSelection: TabContainerSelection.specific(
|
containerSelection: TabContainerSelection.specific(
|
||||||
selectedContainer,
|
selectedContainer,
|
||||||
),
|
),
|
||||||
|
|||||||
+9
-8
@@ -22,10 +22,10 @@ import 'package:flutter_material_design_icons/flutter_material_design_icons.dart
|
|||||||
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:weblibre/core/routing/routes.dart';
|
|
||||||
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
|
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
|
||||||
import 'package:weblibre/features/geckoview/domain/repositories/tab.dart';
|
import 'package:weblibre/features/geckoview/domain/repositories/tab.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/contextmenu/extensions/hit_result.dart';
|
import 'package:weblibre/features/geckoview/features/contextmenu/extensions/hit_result.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_mode.dart';
|
||||||
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
||||||
import 'package:weblibre/utils/ui_helper.dart';
|
import 'package:weblibre/utils/ui_helper.dart';
|
||||||
|
|
||||||
@@ -45,12 +45,13 @@ class OpenInNewTab extends HookConsumerWidget {
|
|||||||
title: const Text('Open in new tab'),
|
title: const Text('Open in new tab'),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
final currentTab = ref.read(selectedTabStateProvider);
|
final currentTab = ref.read(selectedTabStateProvider);
|
||||||
final isPrivate =
|
final tabMode =
|
||||||
currentTab?.isPrivate ??
|
currentTab?.tabMode ??
|
||||||
ref
|
TabMode.fromTabType(
|
||||||
.read(generalSettingsWithDefaultsProvider)
|
ref
|
||||||
.defaultCreateTabType ==
|
.read(generalSettingsWithDefaultsProvider)
|
||||||
TabType.private;
|
.defaultCreateTabType,
|
||||||
|
);
|
||||||
|
|
||||||
final tabId = await ref
|
final tabId = await ref
|
||||||
.read(tabRepositoryProvider.notifier)
|
.read(tabRepositoryProvider.notifier)
|
||||||
@@ -58,7 +59,7 @@ class OpenInNewTab extends HookConsumerWidget {
|
|||||||
url: hitResult.tryGetLink(),
|
url: hitResult.tryGetLink(),
|
||||||
parentId: currentTab?.id,
|
parentId: currentTab?.id,
|
||||||
selectTab: false,
|
selectTab: false,
|
||||||
private: isPrivate,
|
tabMode: tabMode,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
|
|||||||
+2
-1
@@ -24,6 +24,7 @@ import 'package:go_router/go_router.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
|
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/contextmenu/extensions/hit_result.dart';
|
import 'package:weblibre/features/geckoview/features/contextmenu/extensions/hit_result.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_mode.dart';
|
||||||
|
|
||||||
class SaveFile extends HookConsumerWidget {
|
class SaveFile extends HookConsumerWidget {
|
||||||
final HitResult hitResult;
|
final HitResult hitResult;
|
||||||
@@ -48,7 +49,7 @@ class SaveFile extends HookConsumerWidget {
|
|||||||
currentTab.id,
|
currentTab.id,
|
||||||
url: url,
|
url: url,
|
||||||
skipConfirmation: true,
|
skipConfirmation: true,
|
||||||
isPrivate: currentTab.isPrivate,
|
isPrivate: currentTab.tabMode is PrivateTabMode,
|
||||||
referrerUrl: currentTab.url,
|
referrerUrl: currentTab.url,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -24,6 +24,7 @@ import 'package:go_router/go_router.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
|
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/contextmenu/extensions/hit_result.dart';
|
import 'package:weblibre/features/geckoview/features/contextmenu/extensions/hit_result.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_mode.dart';
|
||||||
|
|
||||||
class SaveImage extends HookConsumerWidget {
|
class SaveImage extends HookConsumerWidget {
|
||||||
final HitResult hitResult;
|
final HitResult hitResult;
|
||||||
@@ -48,7 +49,7 @@ class SaveImage extends HookConsumerWidget {
|
|||||||
currentTab.id,
|
currentTab.id,
|
||||||
url: url,
|
url: url,
|
||||||
skipConfirmation: true,
|
skipConfirmation: true,
|
||||||
isPrivate: currentTab.isPrivate,
|
isPrivate: currentTab.tabMode is PrivateTabMode,
|
||||||
referrerUrl: currentTab.url,
|
referrerUrl: currentTab.url,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -24,6 +24,7 @@ import 'package:go_router/go_router.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
|
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/contextmenu/extensions/hit_result.dart';
|
import 'package:weblibre/features/geckoview/features/contextmenu/extensions/hit_result.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_mode.dart';
|
||||||
|
|
||||||
class ShareImage extends HookConsumerWidget {
|
class ShareImage extends HookConsumerWidget {
|
||||||
final HitResult hitResult;
|
final HitResult hitResult;
|
||||||
@@ -47,7 +48,7 @@ class ShareImage extends HookConsumerWidget {
|
|||||||
await GeckoDownloadsService().shareInternetResource(
|
await GeckoDownloadsService().shareInternetResource(
|
||||||
currentTab.id,
|
currentTab.id,
|
||||||
url: url,
|
url: url,
|
||||||
isPrivate: currentTab.isPrivate,
|
isPrivate: currentTab.tabMode is PrivateTabMode,
|
||||||
referrerUrl: currentTab.url,
|
referrerUrl: currentTab.url,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -33,13 +33,13 @@ import 'package:nullability/nullability.dart';
|
|||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
import 'package:sliver_tools/sliver_tools.dart';
|
import 'package:sliver_tools/sliver_tools.dart';
|
||||||
import 'package:timeago/timeago.dart' as timeago;
|
import 'package:timeago/timeago.dart' as timeago;
|
||||||
|
|
||||||
import 'package:weblibre/features/geckoview/domain/repositories/tab.dart';
|
import 'package:weblibre/features/geckoview/domain/repositories/tab.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/dialogs/delete_data.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/dialogs/delete_data.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/history/domain/entities/history_filter_options.dart';
|
import 'package:weblibre/features/geckoview/features/history/domain/entities/history_filter_options.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/history/domain/providers.dart';
|
import 'package:weblibre/features/geckoview/features/history/domain/providers.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/history/domain/repositories/history.dart';
|
import 'package:weblibre/features/geckoview/features/history/domain/repositories/history.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/history/presentation/dialogs/delete_file.dart';
|
import 'package:weblibre/features/geckoview/features/history/presentation/dialogs/delete_file.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_mode.dart';
|
||||||
import 'package:weblibre/features/user/data/models/general_settings.dart';
|
import 'package:weblibre/features/user/data/models/general_settings.dart';
|
||||||
import 'package:weblibre/presentation/hooks/menu_controller.dart';
|
import 'package:weblibre/presentation/hooks/menu_controller.dart';
|
||||||
import 'package:weblibre/presentation/widgets/failure_widget.dart';
|
import 'package:weblibre/presentation/widgets/failure_widget.dart';
|
||||||
@@ -504,7 +504,7 @@ class HistoryScreen extends HookConsumerWidget {
|
|||||||
.read(tabRepositoryProvider.notifier)
|
.read(tabRepositoryProvider.notifier)
|
||||||
.addTab(
|
.addTab(
|
||||||
url: Uri.parse(item.url),
|
url: Uri.parse(item.url),
|
||||||
private: false,
|
tabMode: TabMode.regular,
|
||||||
selectTab: true,
|
selectTab: true,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
+38
-11
@@ -36,6 +36,7 @@ import 'package:weblibre/features/geckoview/features/open_link_tools/domain/serv
|
|||||||
import 'package:weblibre/features/geckoview/features/open_link_tools/presentation/controllers/open_shared_content_unshorten_controller.dart';
|
import 'package:weblibre/features/geckoview/features/open_link_tools/presentation/controllers/open_shared_content_unshorten_controller.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/open_link_tools/presentation/dialogs/tracking_details_dialog.dart';
|
import 'package:weblibre/features/geckoview/features/open_link_tools/presentation/dialogs/tracking_details_dialog.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/open_link_tools/presentation/widgets/attribution_link.dart';
|
import 'package:weblibre/features/geckoview/features/open_link_tools/presentation/widgets/attribution_link.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/container_data.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/presentation/widgets/container_chips.dart';
|
import 'package:weblibre/features/geckoview/features/tabs/presentation/widgets/container_chips.dart';
|
||||||
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
||||||
@@ -160,13 +161,13 @@ class OpenSharedContent extends HookConsumerWidget {
|
|||||||
showInfoMessage(context, 'URL preview applied');
|
showInfoMessage(context, 'URL preview applied');
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> openTab(bool isPrivate) async {
|
Future<void> openTab(TabMode tabMode) async {
|
||||||
if (formKey.currentState?.validate() == true) {
|
if (formKey.currentState?.validate() == true) {
|
||||||
await ref
|
await ref
|
||||||
.read(tabRepositoryProvider.notifier)
|
.read(tabRepositoryProvider.notifier)
|
||||||
.addTab(
|
.addTab(
|
||||||
url: Uri.parse(textController.text),
|
url: Uri.parse(textController.text),
|
||||||
private: isPrivate,
|
tabMode: tabMode,
|
||||||
containerSelection: selectedContainer.value == null
|
containerSelection: selectedContainer.value == null
|
||||||
? const TabContainerSelection.unassigned()
|
? const TabContainerSelection.unassigned()
|
||||||
: TabContainerSelection.specific(selectedContainer.value!),
|
: TabContainerSelection.specific(selectedContainer.value!),
|
||||||
@@ -359,16 +360,42 @@ class OpenSharedContent extends HookConsumerWidget {
|
|||||||
title: 'Open in new tab',
|
title: 'Open in new tab',
|
||||||
subtitle: 'Add to your browser tabs',
|
subtitle: 'Add to your browser tabs',
|
||||||
icon: MdiIcons.tab,
|
icon: MdiIcons.tab,
|
||||||
trailing: IconButton(
|
trailing: PopupMenuButton<TabMode>(
|
||||||
icon: Icon(
|
icon: Icon(MdiIcons.tabUnselected, size: 24),
|
||||||
MdiIcons.dominoMask,
|
tooltip: 'Private / Isolated',
|
||||||
color: appColors.privateTabPurple,
|
onSelected: openTab,
|
||||||
size: 24,
|
itemBuilder: (context) => [
|
||||||
),
|
PopupMenuItem(
|
||||||
tooltip: 'Private',
|
value: TabMode.private,
|
||||||
onPressed: () => openTab(true),
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
MdiIcons.dominoMask,
|
||||||
|
color: appColors.privateTabPurple,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
const Text('Private'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
value: TabMode.newIsolated(),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
MdiIcons.shieldLock,
|
||||||
|
color: appColors.isolatedTabTeal,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
const Text('Isolated'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
onTap: () => openTab(false),
|
onTap: () => openTab(TabMode.regular),
|
||||||
),
|
),
|
||||||
_OpenActionTile(
|
_OpenActionTile(
|
||||||
title: 'Open in custom tab',
|
title: 'Open in custom tab',
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ import 'package:weblibre/features/geckoview/features/search/presentation/widgets
|
|||||||
import 'package:weblibre/features/geckoview/features/search/presentation/widgets/search_modules/full_search_suggestions.dart';
|
import 'package:weblibre/features/geckoview/features/search/presentation/widgets/search_modules/full_search_suggestions.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/search/presentation/widgets/search_modules/history_suggestions.dart';
|
import 'package:weblibre/features/geckoview/features/search/presentation/widgets/search_modules/history_suggestions.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/search/presentation/widgets/search_modules/tab_search.dart';
|
import 'package:weblibre/features/geckoview/features/search/presentation/widgets/search_modules/tab_search.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/tabs/data/entities/isolation_context.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_mode.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/domain/providers/selected_container.dart';
|
import 'package:weblibre/features/geckoview/features/tabs/domain/providers/selected_container.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/presentation/widgets/compact_container_selector.dart';
|
import 'package:weblibre/features/geckoview/features/tabs/presentation/widgets/compact_container_selector.dart';
|
||||||
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
||||||
@@ -92,15 +94,24 @@ class SearchScreen extends HookConsumerWidget {
|
|||||||
// Determine if we're in edit mode (tabId provided AND tab still exists)
|
// Determine if we're in edit mode (tabId provided AND tab still exists)
|
||||||
final isEditMode = tabId != null && existingTabState != null;
|
final isEditMode = tabId != null && existingTabState != null;
|
||||||
|
|
||||||
// Derive private mode from existing tab or from selector
|
final effectiveTabMode = isEditMode
|
||||||
final privateTabMode = isEditMode
|
? existingTabState.tabMode
|
||||||
? existingTabState.isPrivate
|
|
||||||
: switch (selectedTabType.value) {
|
: switch (selectedTabType.value) {
|
||||||
TabType.regular => false,
|
TabType.regular => TabMode.regular,
|
||||||
TabType.private => true,
|
TabType.private => TabMode.private,
|
||||||
TabType.child => currentTabTabType == TabType.private,
|
TabType.isolated => TabMode.newIsolated(),
|
||||||
|
TabType.child => switch (currentTabTabType) {
|
||||||
|
TabType.private => TabMode.private,
|
||||||
|
TabType.isolated => TabMode.isolated(
|
||||||
|
ref.watch(selectedTabStateProvider)?.isolationContextId ??
|
||||||
|
newIsolatedContextId(),
|
||||||
|
),
|
||||||
|
_ => TabMode.regular,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
final privateTabMode = effectiveTabMode is PrivateTabMode;
|
||||||
|
|
||||||
final searchTextController = useTextEditingController(
|
final searchTextController = useTextEditingController(
|
||||||
text: initialSearchText,
|
text: initialSearchText,
|
||||||
);
|
);
|
||||||
@@ -240,7 +251,7 @@ class SearchScreen extends HookConsumerWidget {
|
|||||||
.read(tabRepositoryProvider.notifier)
|
.read(tabRepositoryProvider.notifier)
|
||||||
.addTab(
|
.addTab(
|
||||||
url: searchUri,
|
url: searchUri,
|
||||||
private: privateTabMode,
|
tabMode: effectiveTabMode,
|
||||||
parentId: (selectedTabType.value == TabType.child)
|
parentId: (selectedTabType.value == TabType.child)
|
||||||
? ref.read(selectedTabProvider)
|
? ref.read(selectedTabProvider)
|
||||||
: null,
|
: null,
|
||||||
@@ -279,7 +290,7 @@ class SearchScreen extends HookConsumerWidget {
|
|||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
flex: 3,
|
flex: 4,
|
||||||
child: Align(
|
child: Align(
|
||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
child: Focus(
|
child: Focus(
|
||||||
@@ -300,18 +311,24 @@ class SearchScreen extends HookConsumerWidget {
|
|||||||
TabType.regular => null,
|
TabType.regular => null,
|
||||||
TabType.private =>
|
TabType.private =>
|
||||||
appColors.privateSelectionOverlay,
|
appColors.privateSelectionOverlay,
|
||||||
|
TabType.isolated =>
|
||||||
|
appColors.isolatedSelectionOverlay,
|
||||||
TabType.child =>
|
TabType.child =>
|
||||||
(currentTabTabType ==
|
switch (currentTabTabType) {
|
||||||
TabType.private)
|
TabType.private =>
|
||||||
? appColors
|
appColors
|
||||||
.privateSelectionOverlay
|
.privateSelectionOverlay,
|
||||||
: null,
|
TabType.isolated =>
|
||||||
|
appColors
|
||||||
|
.isolatedSelectionOverlay,
|
||||||
|
_ => null,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 8),
|
||||||
Flexible(
|
Flexible(
|
||||||
flex: 2,
|
flex: 2,
|
||||||
child: Align(
|
child: Align(
|
||||||
@@ -390,7 +407,7 @@ class SearchScreen extends HookConsumerWidget {
|
|||||||
.read(tabRepositoryProvider.notifier)
|
.read(tabRepositoryProvider.notifier)
|
||||||
.addTab(
|
.addTab(
|
||||||
url: newUrl,
|
url: newUrl,
|
||||||
private: privateTabMode,
|
tabMode: effectiveTabMode,
|
||||||
parentId:
|
parentId:
|
||||||
(selectedTabType.value == TabType.child)
|
(selectedTabType.value == TabType.child)
|
||||||
? ref.read(selectedTabProvider)
|
? ref.read(selectedTabProvider)
|
||||||
@@ -447,7 +464,7 @@ class SearchScreen extends HookConsumerWidget {
|
|||||||
.read(tabRepositoryProvider.notifier)
|
.read(tabRepositoryProvider.notifier)
|
||||||
.addTab(
|
.addTab(
|
||||||
url: uri,
|
url: uri,
|
||||||
private: privateTabMode,
|
tabMode: effectiveTabMode,
|
||||||
parentId: (selectedTabType.value == TabType.child)
|
parentId: (selectedTabType.value == TabType.child)
|
||||||
? ref.read(selectedTabProvider)
|
? ref.read(selectedTabProvider)
|
||||||
: null,
|
: null,
|
||||||
|
|||||||
+9
@@ -72,6 +72,15 @@ class AnimatedTabTypeSwitcher extends StatelessWidget {
|
|||||||
selectedBackgroundColor: selectedBackgroundColor,
|
selectedBackgroundColor: selectedBackgroundColor,
|
||||||
onTap: () => onChanged(TabType.private),
|
onTap: () => onChanged(TabType.private),
|
||||||
),
|
),
|
||||||
|
_divider(borderColor),
|
||||||
|
_Segment(
|
||||||
|
tabType: TabType.isolated,
|
||||||
|
icon: MdiIcons.shieldLock,
|
||||||
|
label: 'Isolated',
|
||||||
|
isSelected: selected == TabType.isolated,
|
||||||
|
selectedBackgroundColor: selectedBackgroundColor,
|
||||||
|
onTap: () => onChanged(TabType.isolated),
|
||||||
|
),
|
||||||
if (showChildOption) ...[
|
if (showChildOption) ...[
|
||||||
_divider(borderColor),
|
_divider(borderColor),
|
||||||
_Segment(
|
_Segment(
|
||||||
|
|||||||
+2
-6
@@ -75,9 +75,7 @@ class FullSearchTermSuggestions extends HookConsumerWidget {
|
|||||||
: [
|
: [
|
||||||
if (searchTextIsNotEmpty) searchText,
|
if (searchTextIsNotEmpty) searchText,
|
||||||
if (searchSuggestions.value != null)
|
if (searchSuggestions.value != null)
|
||||||
...searchSuggestions.value!.whereNot(
|
...searchSuggestions.value!.whereNot((s) => s == searchText),
|
||||||
(s) => s == searchText,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
[showHistory, searchText, searchHistory.value, searchSuggestions.value],
|
[showHistory, searchText, searchHistory.value, searchSuggestions.value],
|
||||||
);
|
);
|
||||||
@@ -115,9 +113,7 @@ class FullSearchTermSuggestions extends HookConsumerWidget {
|
|||||||
onPressed: ref
|
onPressed: ref
|
||||||
.read(searchSuggestionsExpandedProvider.notifier)
|
.read(searchSuggestionsExpandedProvider.notifier)
|
||||||
.toggle,
|
.toggle,
|
||||||
icon: Icon(
|
icon: Icon(expanded ? Icons.unfold_less : Icons.unfold_more),
|
||||||
expanded ? Icons.unfold_less : Icons.unfold_more,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
+2
-6
@@ -105,18 +105,14 @@ class SearchModuleHeader extends StatelessWidget {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
isExpanded
|
isExpanded ? 'Show less' : 'Show all $totalCount',
|
||||||
? 'Show less'
|
|
||||||
: 'Show all $totalCount',
|
|
||||||
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
||||||
color: Theme.of(context).colorScheme.primary,
|
color: Theme.of(context).colorScheme.primary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
Icon(
|
Icon(
|
||||||
isExpanded
|
isExpanded ? Icons.expand_less : Icons.expand_more,
|
||||||
? Icons.expand_less
|
|
||||||
: Icons.expand_more,
|
|
||||||
size: 16,
|
size: 16,
|
||||||
color: Theme.of(context).colorScheme.primary,
|
color: Theme.of(context).colorScheme.primary,
|
||||||
),
|
),
|
||||||
|
|||||||
+112
-109
@@ -135,132 +135,135 @@ class TabSearch extends HookConsumerWidget {
|
|||||||
title: 'Tabs',
|
title: 'Tabs',
|
||||||
moduleType: SearchModuleType.tabs,
|
moduleType: SearchModuleType.tabs,
|
||||||
totalCount: filteredResultCount,
|
totalCount: filteredResultCount,
|
||||||
contentSliverBuilder: ({
|
contentSliverBuilder:
|
||||||
required bool isCollapsed,
|
({required bool isCollapsed, required int visibleCount}) => [
|
||||||
required int visibleCount,
|
if (!isCollapsed)
|
||||||
}) => [
|
SliverToBoxAdapter(
|
||||||
if (!isCollapsed)
|
child: Padding(
|
||||||
SliverToBoxAdapter(
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
child: Padding(
|
child: Column(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
child: Column(
|
children: [
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
ContainerChips(
|
||||||
children: [
|
displayMenu: false,
|
||||||
ContainerChips(
|
selectedContainer: selectedContainer.value,
|
||||||
displayMenu: false,
|
showUnassignedChip: containerIdsWithResults.value
|
||||||
selectedContainer: selectedContainer.value,
|
.containsKey(null),
|
||||||
showUnassignedChip: containerIdsWithResults.value
|
onSelected: (container) {
|
||||||
.containsKey(null),
|
selectedContainer.value = container;
|
||||||
onSelected: (container) {
|
},
|
||||||
selectedContainer.value = container;
|
onDeleted: (container) {
|
||||||
},
|
selectedContainer.value = null;
|
||||||
onDeleted: (container) {
|
},
|
||||||
selectedContainer.value = null;
|
containerFilter: (container) => containerIdsWithResults
|
||||||
},
|
.value
|
||||||
containerFilter: (container) =>
|
.containsKey(container.id),
|
||||||
containerIdsWithResults.value.containsKey(container.id),
|
containerBadgeCount: (container) =>
|
||||||
containerBadgeCount: (container) =>
|
containerIdsWithResults.value[container?.id] ?? 0,
|
||||||
containerIdsWithResults.value[container?.id] ?? 0,
|
searchTextListenable: searchTextListenable,
|
||||||
searchTextListenable: searchTextListenable,
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
if (!isCollapsed)
|
||||||
),
|
SliverList.builder(
|
||||||
if (!isCollapsed)
|
itemCount: visibleCount,
|
||||||
SliverList.builder(
|
itemBuilder: (context, index) {
|
||||||
itemCount: visibleCount,
|
final result = filteredTabs[index];
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final result = filteredTabs[index];
|
|
||||||
|
|
||||||
final content =
|
final content =
|
||||||
(result.extractedContent?.contains(_matchPrefix) == true)
|
(result.extractedContent?.contains(_matchPrefix) == true)
|
||||||
? result.extractedContent
|
? result.extractedContent
|
||||||
: result.fullContent;
|
: result.fullContent;
|
||||||
|
|
||||||
final titleHasMatch = result.title.contains(_matchPrefix);
|
final titleHasMatch = result.title.contains(_matchPrefix);
|
||||||
final urlHasMatch =
|
final urlHasMatch =
|
||||||
result.highlightedUrl?.contains(_matchPrefix) ?? false;
|
result.highlightedUrl?.contains(_matchPrefix) ?? false;
|
||||||
final bodyHasMatch = content?.contains(_matchPrefix) ?? false;
|
final bodyHasMatch = content?.contains(_matchPrefix) ?? false;
|
||||||
|
|
||||||
return ListTile(
|
return ListTile(
|
||||||
leading: RepaintBoundary(
|
leading: RepaintBoundary(
|
||||||
child:
|
child:
|
||||||
result.icon.mapNotNull(
|
result.icon.mapNotNull(
|
||||||
(icon) => SafeRawImage(
|
(icon) => SafeRawImage(
|
||||||
image: icon,
|
image: icon,
|
||||||
height: 24,
|
height: 24,
|
||||||
width: 24,
|
width: 24,
|
||||||
fallback: UrlIcon([result.url], iconSize: 24),
|
fallback: UrlIcon([result.url], iconSize: 24),
|
||||||
),
|
),
|
||||||
) ??
|
) ??
|
||||||
UrlIcon([result.url], iconSize: 24),
|
UrlIcon([result.url], iconSize: 24),
|
||||||
),
|
|
||||||
title: result.title.mapNotNull(
|
|
||||||
(title) => Text.rich(
|
|
||||||
buildHighlightedText(
|
|
||||||
title,
|
|
||||||
Theme.of(context).textTheme.bodyLarge?.copyWith(
|
|
||||||
color: Theme.of(context).colorScheme.onSurface,
|
|
||||||
),
|
|
||||||
Theme.of(context).textTheme.bodyLarge?.copyWith(
|
|
||||||
color: Theme.of(context).colorScheme.onSurface,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
_matchPrefix,
|
|
||||||
_matchSuffix,
|
|
||||||
),
|
),
|
||||||
maxLines: 2,
|
title: result.title.mapNotNull(
|
||||||
overflow: TextOverflow.ellipsis,
|
(title) => Text.rich(
|
||||||
),
|
|
||||||
),
|
|
||||||
subtitle: (bodyHasMatch || (urlHasMatch && !titleHasMatch))
|
|
||||||
? Text.rich(
|
|
||||||
buildHighlightedText(
|
buildHighlightedText(
|
||||||
(bodyHasMatch ? content! : result.highlightedUrl!),
|
title,
|
||||||
Theme.of(context).textTheme.bodyMedium?.copyWith(
|
Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||||
color: Theme.of(
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
context,
|
|
||||||
).colorScheme.onSurfaceVariant,
|
|
||||||
),
|
),
|
||||||
Theme.of(context).textTheme.bodyMedium?.copyWith(
|
Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||||
color: Theme.of(
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
context,
|
|
||||||
).colorScheme.onSurfaceVariant,
|
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
_matchPrefix,
|
_matchPrefix,
|
||||||
_matchSuffix,
|
_matchSuffix,
|
||||||
normalizeWhitespaces: true,
|
|
||||||
),
|
),
|
||||||
maxLines: 3,
|
maxLines: 2,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
)
|
),
|
||||||
: UriBreadcrumb(uri: result.url),
|
),
|
||||||
onTap: () async {
|
subtitle: (bodyHasMatch || (urlHasMatch && !titleHasMatch))
|
||||||
await ref
|
? Text.rich(
|
||||||
.read(tabRepositoryProvider.notifier)
|
buildHighlightedText(
|
||||||
.selectTab(result.id);
|
(bodyHasMatch
|
||||||
if (result.sourceSearchQuery.isNotEmpty &&
|
? content!
|
||||||
ref.read(findInPageControllerProvider(result.id)) ==
|
: result.highlightedUrl!),
|
||||||
FindInPageState.hidden()) {
|
Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
await ref
|
color: Theme.of(
|
||||||
.read(findInPageControllerProvider(result.id).notifier)
|
context,
|
||||||
.findAll(text: result.sourceSearchQuery!);
|
).colorScheme.onSurfaceVariant,
|
||||||
}
|
),
|
||||||
|
Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
|
color: Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.onSurfaceVariant,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
_matchPrefix,
|
||||||
|
_matchSuffix,
|
||||||
|
normalizeWhitespaces: true,
|
||||||
|
),
|
||||||
|
maxLines: 3,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
)
|
||||||
|
: UriBreadcrumb(uri: result.url),
|
||||||
|
onTap: () async {
|
||||||
|
await ref
|
||||||
|
.read(tabRepositoryProvider.notifier)
|
||||||
|
.selectTab(result.id);
|
||||||
|
if (result.sourceSearchQuery.isNotEmpty &&
|
||||||
|
ref.read(findInPageControllerProvider(result.id)) ==
|
||||||
|
FindInPageState.hidden()) {
|
||||||
|
await ref
|
||||||
|
.read(
|
||||||
|
findInPageControllerProvider(result.id).notifier,
|
||||||
|
)
|
||||||
|
.findAll(text: result.sourceSearchQuery!);
|
||||||
|
}
|
||||||
|
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
ref
|
ref
|
||||||
.read(bottomSheetControllerProvider.notifier)
|
.read(bottomSheetControllerProvider.notifier)
|
||||||
.requestDismiss();
|
.requestDismiss();
|
||||||
|
|
||||||
const BrowserRoute().go(context);
|
const BrowserRoute().go(context);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
},
|
},
|
||||||
);
|
),
|
||||||
},
|
],
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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/daos/container.drift.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/data/database/database.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/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/container_data.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/data/models/site_assignment.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({
|
Selectable<String> getAllTabIds({
|
||||||
bool includeRegular = true,
|
bool includeRegular = true,
|
||||||
bool includePrivate = true,
|
bool includePrivate = true,
|
||||||
|
bool includeIsolated = true,
|
||||||
}) {
|
}) {
|
||||||
final query = selectOnly(db.tab)..addColumns([db.tab.id]);
|
final query = selectOnly(db.tab)..addColumns([db.tab.id]);
|
||||||
|
|
||||||
if (!includeRegular) {
|
final excludedModes = <TabModeDbValue>[];
|
||||||
query.where(
|
if (!includeRegular) excludedModes.add(TabModeDbValue.regular);
|
||||||
db.tab.isPrivate.isNotNull() & db.tab.isPrivate.isNotValue(false),
|
if (!includePrivate) excludedModes.add(TabModeDbValue.private);
|
||||||
);
|
if (!includeIsolated) excludedModes.add(TabModeDbValue.isolated);
|
||||||
}
|
|
||||||
if (!includePrivate) {
|
if (excludedModes.isNotEmpty) {
|
||||||
query.where(
|
query.where(db.tab.tabMode.isNotInValues(excludedModes));
|
||||||
db.tab.isPrivate.isNotNull() & db.tab.isPrivate.isNotValue(true),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return query.map((row) => row.read(db.tab.id)!);
|
return query.map((row) => row.read(db.tab.id)!);
|
||||||
@@ -96,6 +96,7 @@ class ContainerDao extends DatabaseAccessor<TabDatabase>
|
|||||||
String? containerId, {
|
String? containerId, {
|
||||||
bool includeRegular = true,
|
bool includeRegular = true,
|
||||||
bool includePrivate = true,
|
bool includePrivate = true,
|
||||||
|
bool includeIsolated = true,
|
||||||
}) {
|
}) {
|
||||||
final query = selectOnly(db.tab)
|
final query = selectOnly(db.tab)
|
||||||
..addColumns([db.tab.id])
|
..addColumns([db.tab.id])
|
||||||
@@ -106,15 +107,13 @@ class ContainerDao extends DatabaseAccessor<TabDatabase>
|
|||||||
)
|
)
|
||||||
..orderBy([OrderingTerm.asc(db.tab.orderKey)]);
|
..orderBy([OrderingTerm.asc(db.tab.orderKey)]);
|
||||||
|
|
||||||
if (!includeRegular) {
|
final excludedModes = <TabModeDbValue>[];
|
||||||
query.where(
|
if (!includeRegular) excludedModes.add(TabModeDbValue.regular);
|
||||||
db.tab.isPrivate.isNotNull() & db.tab.isPrivate.isNotValue(false),
|
if (!includePrivate) excludedModes.add(TabModeDbValue.private);
|
||||||
);
|
if (!includeIsolated) excludedModes.add(TabModeDbValue.isolated);
|
||||||
}
|
|
||||||
if (!includePrivate) {
|
if (excludedModes.isNotEmpty) {
|
||||||
query.where(
|
query.where(db.tab.tabMode.isNotInValues(excludedModes));
|
||||||
db.tab.isPrivate.isNotNull() & db.tab.isPrivate.isNotValue(true),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return query.map((row) => row.read(db.tab.id)!);
|
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/daos/tab.drift.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/data/database/database.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/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/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/container_data.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/data/models/tab_query_result.dart';
|
import 'package:weblibre/features/geckoview/features/tabs/data/models/tab_query_result.dart';
|
||||||
|
|
||||||
@DriftAccessor()
|
@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 {
|
class TabDao extends DatabaseAccessor<TabDatabase> with $TabDaoMixin {
|
||||||
final _undoHistory = <String, TabData>{};
|
final _undoHistory = <String, TabData>{};
|
||||||
Timer? _clearHistoryTimer;
|
Timer? _clearHistoryTimer;
|
||||||
@@ -45,12 +56,25 @@ class TabDao extends DatabaseAccessor<TabDatabase> with $TabDaoMixin {
|
|||||||
SingleOrNullSelectable<TabData> getTabDataById(String id) =>
|
SingleOrNullSelectable<TabData> getTabDataById(String id) =>
|
||||||
db.tab.select()..where((t) => t.id.equals(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)
|
final query = selectOnly(db.tab)
|
||||||
..addColumns([db.tab.isPrivate])
|
..addColumns([db.tab.tabMode, db.tab.isolationContextId])
|
||||||
..where(db.tab.id.equals(tabId));
|
..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() {
|
Selectable<String> getAllTabIds() {
|
||||||
@@ -124,18 +148,24 @@ class TabDao extends DatabaseAccessor<TabDatabase> with $TabDaoMixin {
|
|||||||
|
|
||||||
Future<String> upsertTabTransactional(
|
Future<String> upsertTabTransactional(
|
||||||
Future<String> Function() createTab, {
|
Future<String> Function() createTab, {
|
||||||
required Value<bool?> isPrivate,
|
|
||||||
required Value<String?> parentId,
|
required Value<String?> parentId,
|
||||||
Value<String?> containerId = const Value.absent(),
|
Value<String?> containerId = const Value.absent(),
|
||||||
Value<String?> orderKey = const Value.absent(),
|
Value<String?> orderKey = const Value.absent(),
|
||||||
Value<Uri?> url = const Value.absent(),
|
Value<Uri?> url = const Value.absent(),
|
||||||
Value<String?> title = const Value.absent(),
|
Value<String?> title = const Value.absent(),
|
||||||
|
Value<TabMode> tabMode = const Value.absent(),
|
||||||
}) {
|
}) {
|
||||||
return db.transaction(() async {
|
return db.transaction(() async {
|
||||||
final tabId = await createTab();
|
final tabId = await createTab();
|
||||||
final currentOrderKey =
|
final currentOrderKey =
|
||||||
orderKey.value ??
|
orderKey.value ??
|
||||||
await _generateOrderKey(parentId: parentId, containerId: containerId);
|
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(
|
await db.tab.insertOne(
|
||||||
TabCompanion.insert(
|
TabCompanion.insert(
|
||||||
@@ -146,8 +176,9 @@ class TabDao extends DatabaseAccessor<TabDatabase> with $TabDaoMixin {
|
|||||||
containerId: containerId,
|
containerId: containerId,
|
||||||
url: url,
|
url: url,
|
||||||
title: title,
|
title: title,
|
||||||
isPrivate: isPrivate,
|
|
||||||
orderKey: currentOrderKey,
|
orderKey: currentOrderKey,
|
||||||
|
tabMode: persistedTabMode,
|
||||||
|
isolationContextId: isolationContextId,
|
||||||
),
|
),
|
||||||
onConflict: DoUpdate(
|
onConflict: DoUpdate(
|
||||||
(old) => TabCompanion(
|
(old) => TabCompanion(
|
||||||
@@ -156,8 +187,9 @@ class TabDao extends DatabaseAccessor<TabDatabase> with $TabDaoMixin {
|
|||||||
containerId: containerId,
|
containerId: containerId,
|
||||||
url: url,
|
url: url,
|
||||||
title: title,
|
title: title,
|
||||||
isPrivate: isPrivate,
|
|
||||||
orderKey: Value.absentIfNull(orderKey.value),
|
orderKey: Value.absentIfNull(orderKey.value),
|
||||||
|
tabMode: persistedTabMode,
|
||||||
|
isolationContextId: isolationContextId,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -170,17 +202,23 @@ class TabDao extends DatabaseAccessor<TabDatabase> with $TabDaoMixin {
|
|||||||
Future<String> insertTab(
|
Future<String> insertTab(
|
||||||
String tabId, {
|
String tabId, {
|
||||||
required TabSource source,
|
required TabSource source,
|
||||||
required Value<bool?> isPrivate,
|
|
||||||
required Value<String?> parentId,
|
required Value<String?> parentId,
|
||||||
Value<String?> containerId = const Value.absent(),
|
Value<String?> containerId = const Value.absent(),
|
||||||
Value<String?> orderKey = const Value.absent(),
|
Value<String?> orderKey = const Value.absent(),
|
||||||
Value<Uri?> url = const Value.absent(),
|
Value<Uri?> url = const Value.absent(),
|
||||||
Value<String?> title = const Value.absent(),
|
Value<String?> title = const Value.absent(),
|
||||||
|
Value<TabMode> tabMode = const Value.absent(),
|
||||||
}) {
|
}) {
|
||||||
return db.transaction(() async {
|
return db.transaction(() async {
|
||||||
final currentOrderKey =
|
final currentOrderKey =
|
||||||
orderKey.value ??
|
orderKey.value ??
|
||||||
await _generateOrderKey(parentId: parentId, containerId: containerId);
|
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(
|
await db.tab.insertOne(
|
||||||
TabCompanion.insert(
|
TabCompanion.insert(
|
||||||
@@ -190,9 +228,10 @@ class TabDao extends DatabaseAccessor<TabDatabase> with $TabDaoMixin {
|
|||||||
timestamp: DateTime.now(),
|
timestamp: DateTime.now(),
|
||||||
containerId: containerId,
|
containerId: containerId,
|
||||||
orderKey: currentOrderKey,
|
orderKey: currentOrderKey,
|
||||||
isPrivate: isPrivate,
|
|
||||||
url: url,
|
url: url,
|
||||||
title: title,
|
title: title,
|
||||||
|
tabMode: persistedTabMode,
|
||||||
|
isolationContextId: isolationContextId,
|
||||||
),
|
),
|
||||||
onConflict: DoUpdate(
|
onConflict: DoUpdate(
|
||||||
(old) => TabCompanion(
|
(old) => TabCompanion(
|
||||||
@@ -201,8 +240,9 @@ class TabDao extends DatabaseAccessor<TabDatabase> with $TabDaoMixin {
|
|||||||
containerId: containerId,
|
containerId: containerId,
|
||||||
url: url,
|
url: url,
|
||||||
title: title,
|
title: title,
|
||||||
isPrivate: isPrivate,
|
|
||||||
orderKey: Value.absentIfNull(orderKey.value),
|
orderKey: Value.absentIfNull(orderKey.value),
|
||||||
|
tabMode: persistedTabMode,
|
||||||
|
isolationContextId: isolationContextId,
|
||||||
),
|
),
|
||||||
where: (old) => old.source.isSmallerThanValue(source.index),
|
where: (old) => old.source.isSmallerThanValue(source.index),
|
||||||
),
|
),
|
||||||
@@ -301,7 +341,8 @@ class TabDao extends DatabaseAccessor<TabDatabase> with $TabDaoMixin {
|
|||||||
if (previousState == null ||
|
if (previousState == null ||
|
||||||
previousState.url != state.url ||
|
previousState.url != state.url ||
|
||||||
previousState.title != state.title ||
|
previousState.title != state.title ||
|
||||||
previousState.parentId != state.parentId) {
|
previousState.parentId != state.parentId ||
|
||||||
|
previousState.tabMode != state.tabMode) {
|
||||||
batch.update(
|
batch.update(
|
||||||
db.tab,
|
db.tab,
|
||||||
TabCompanion(
|
TabCompanion(
|
||||||
@@ -314,6 +355,12 @@ class TabDao extends DatabaseAccessor<TabDatabase> with $TabDaoMixin {
|
|||||||
title: (previousState?.title != state.title)
|
title: (previousState?.title != state.title)
|
||||||
? Value(state.title)
|
? Value(state.title)
|
||||||
: const Value.absent(),
|
: 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),
|
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 {
|
return db.transaction(() async {
|
||||||
final deleted =
|
final deleted =
|
||||||
await (db.tab.delete()..where((t) => t.id.isNotIn(retainTabIds)))
|
await (db.tab.delete()..where((t) => t.id.isNotIn(retainTabIds)))
|
||||||
.goAndReturn();
|
.goAndReturn();
|
||||||
|
|
||||||
|
final deletedIsolationContextIds = <String>{
|
||||||
|
for (final tab in deleted)
|
||||||
|
if (tab.isolationContextId != null) tab.isolationContextId!,
|
||||||
|
};
|
||||||
|
|
||||||
if (deleted.isNotEmpty) {
|
if (deleted.isNotEmpty) {
|
||||||
_clearHistoryTimer?.cancel();
|
_clearHistoryTimer?.cancel();
|
||||||
|
|
||||||
@@ -361,6 +415,11 @@ class TabDao extends DatabaseAccessor<TabDatabase> with $TabDaoMixin {
|
|||||||
}),
|
}),
|
||||||
onConflict: DoNothing(),
|
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) {
|
Future<List<String>> getUnassignedTabsOlderThan(DateTime threshold) {
|
||||||
final query = selectOnly(db.tab)
|
final query = selectOnly(db.tab)
|
||||||
..addColumns([db.tab.id])
|
..addColumns([db.tab.id])
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import 'package:weblibre/features/search/domain/fts_tokenizer.dart';
|
|||||||
@DriftDatabase(include: {'definitions.drift'}, daos: [ContainerDao, TabDao])
|
@DriftDatabase(include: {'definitions.drift'}, daos: [ContainerDao, TabDao])
|
||||||
class TabDatabase extends $TabDatabase with TrigramQueryBuilderMixin {
|
class TabDatabase extends $TabDatabase with TrigramQueryBuilderMixin {
|
||||||
@override
|
@override
|
||||||
final int schemaVersion = 5;
|
final int schemaVersion = 6;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
final int ftsTokenLimit = 10;
|
final int ftsTokenLimit = 10;
|
||||||
@@ -99,5 +99,21 @@ class TabDatabase extends $TabDatabase with TrigramQueryBuilderMixin {
|
|||||||
await m.drop(schema.tabMaintainParentChainOnDelete);
|
await m.drop(schema.tabMaintainParentChainOnDelete);
|
||||||
await m.create(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
|
// dart format width=80
|
||||||
import 'package:drift/internal/versioned_schema.dart' as i0;
|
import 'package:drift/internal/versioned_schema.dart' as i0;
|
||||||
import 'package:drift/drift.dart' as i1;
|
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 {
|
final class Schema3 extends i0.VersionedSchema {
|
||||||
Schema3({required super.database}) : super(version: 3);
|
Schema3({required super.database}) : super(version: 3);
|
||||||
@override
|
@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({
|
i0.MigrationStepWithVersion migrationSteps({
|
||||||
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
|
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, Schema4 schema) from3To4,
|
||||||
required Future<void> Function(i1.Migrator m, Schema5 schema) from4To5,
|
required Future<void> Function(i1.Migrator m, Schema5 schema) from4To5,
|
||||||
|
required Future<void> Function(i1.Migrator m, Schema6 schema) from5To6,
|
||||||
}) {
|
}) {
|
||||||
return (currentVersion, database) async {
|
return (currentVersion, database) async {
|
||||||
switch (currentVersion) {
|
switch (currentVersion) {
|
||||||
@@ -483,6 +617,11 @@ i0.MigrationStepWithVersion migrationSteps({
|
|||||||
final migrator = i1.Migrator(database, schema);
|
final migrator = i1.Migrator(database, schema);
|
||||||
await from4To5(migrator, schema);
|
await from4To5(migrator, schema);
|
||||||
return 5;
|
return 5;
|
||||||
|
case 5:
|
||||||
|
final schema = Schema6(database: database);
|
||||||
|
final migrator = i1.Migrator(database, schema);
|
||||||
|
await from5To6(migrator, schema);
|
||||||
|
return 6;
|
||||||
default:
|
default:
|
||||||
throw ArgumentError.value('Unknown migration from $currentVersion');
|
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, Schema3 schema) from2To3,
|
||||||
required Future<void> Function(i1.Migrator m, Schema4 schema) from3To4,
|
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, Schema5 schema) from4To5,
|
||||||
|
required Future<void> Function(i1.Migrator m, Schema6 schema) from5To6,
|
||||||
}) => i0.VersionedSchema.stepByStepHelper(
|
}) => i0.VersionedSchema.stepByStepHelper(
|
||||||
step: migrationSteps(
|
step: migrationSteps(
|
||||||
from2To3: from2To3,
|
from2To3: from2To3,
|
||||||
from3To4: from3To4,
|
from3To4: from3To4,
|
||||||
from4To5: from4To5,
|
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/site_assignment.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/data/models/tab_query_result.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/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';
|
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_source.dart';
|
||||||
|
|
||||||
CREATE TABLE container (
|
CREATE TABLE container (
|
||||||
@@ -21,13 +22,18 @@ CREATE TABLE tab(
|
|||||||
order_key TEXT NOT NULL,
|
order_key TEXT NOT NULL,
|
||||||
url TEXT MAPPED BY `const UriConverterNullable()`,
|
url TEXT MAPPED BY `const UriConverterNullable()`,
|
||||||
title TEXT,
|
title TEXT,
|
||||||
is_private BOOL,
|
tab_mode ENUM(TabModeDbValue) NOT NULL DEFAULT 0,
|
||||||
|
isolation_context_id TEXT,
|
||||||
is_probably_readerable BOOL,
|
is_probably_readerable BOOL,
|
||||||
extracted_content_markdown TEXT,
|
extracted_content_markdown TEXT,
|
||||||
extracted_content_plain TEXT,
|
extracted_content_plain TEXT,
|
||||||
full_content_markdown TEXT,
|
full_content_markdown TEXT,
|
||||||
full_content_plain 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
|
CREATE VIRTUAL TABLE tab_fts
|
||||||
@@ -150,7 +156,7 @@ queryTabsBasic WITH TabQueryResult:
|
|||||||
SELECT
|
SELECT
|
||||||
t.id,
|
t.id,
|
||||||
t.container_id,
|
t.container_id,
|
||||||
t.is_private,
|
t.tab_mode,
|
||||||
t.title,
|
t.title,
|
||||||
CAST(t.url AS TEXT) AS url,
|
CAST(t.url AS TEXT) AS url,
|
||||||
t.url AS clean_url,
|
t.url AS clean_url,
|
||||||
@@ -179,7 +185,7 @@ queryTabsFullContent WITH TabQueryResult:
|
|||||||
SELECT
|
SELECT
|
||||||
t.id,
|
t.id,
|
||||||
t.container_id,
|
t.container_id,
|
||||||
t.is_private,
|
t.tab_mode,
|
||||||
highlight(tab_fts, 0, :beforeMatch, :afterMatch) AS title,
|
highlight(tab_fts, 0, :beforeMatch, :afterMatch) AS title,
|
||||||
highlight(tab_fts, 1, :beforeMatch, :afterMatch) AS url,
|
highlight(tab_fts, 1, :beforeMatch, :afterMatch) AS url,
|
||||||
snippet(tab_fts, 2, :beforeMatch, :afterMatch, :ellipsis, :snippetLength) AS extracted_content,
|
snippet(tab_fts, 2, :beforeMatch, :afterMatch, :ellipsis, :snippetLength) AS extracted_content,
|
||||||
@@ -334,3 +340,16 @@ containersToClearOnExit:
|
|||||||
WHERE
|
WHERE
|
||||||
json_extract(container.metadata, '$.clearDataOnExit') = 1
|
json_extract(container.metadata, '$.clearDataOnExit') = 1
|
||||||
AND container.metadata ->> '$.contextualIdentity' IS NOT NULL;
|
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;
|
as i5;
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_source.dart'
|
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_source.dart'
|
||||||
as i6;
|
as i6;
|
||||||
import 'package:weblibre/data/database/converters/uri.dart' as i7;
|
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_mode.dart'
|
||||||
import 'package:drift/internal/modular.dart' as i8;
|
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'
|
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;
|
as i10;
|
||||||
|
import 'package:weblibre/features/geckoview/features/tabs/data/models/site_assignment.dart'
|
||||||
|
as i11;
|
||||||
|
|
||||||
typedef $ContainerCreateCompanionBuilder =
|
typedef $ContainerCreateCompanionBuilder =
|
||||||
i3.ContainerCompanion Function({
|
i3.ContainerCompanion Function({
|
||||||
@@ -47,10 +49,10 @@ final class $ContainerReferences
|
|||||||
static i0.MultiTypedResultKey<i3.Tab, List<i3.TabData>> _tabRefsTable(
|
static i0.MultiTypedResultKey<i3.Tab, List<i3.TabData>> _tabRefsTable(
|
||||||
i0.GeneratedDatabase db,
|
i0.GeneratedDatabase db,
|
||||||
) => i0.MultiTypedResultKey.fromTable(
|
) => i0.MultiTypedResultKey.fromTable(
|
||||||
i8.ReadDatabaseContainer(db).resultSet<i3.Tab>('tab'),
|
i9.ReadDatabaseContainer(db).resultSet<i3.Tab>('tab'),
|
||||||
aliasName: i0.$_aliasNameGenerator(
|
aliasName: i0.$_aliasNameGenerator(
|
||||||
i8.ReadDatabaseContainer(db).resultSet<i3.Container>('container').id,
|
i9.ReadDatabaseContainer(db).resultSet<i3.Container>('container').id,
|
||||||
i8.ReadDatabaseContainer(db).resultSet<i3.Tab>('tab').containerId,
|
i9.ReadDatabaseContainer(db).resultSet<i3.Tab>('tab').containerId,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -58,7 +60,7 @@ final class $ContainerReferences
|
|||||||
final manager = i3
|
final manager = i3
|
||||||
.$TabTableManager(
|
.$TabTableManager(
|
||||||
$_db,
|
$_db,
|
||||||
i8.ReadDatabaseContainer($_db).resultSet<i3.Tab>('tab'),
|
i9.ReadDatabaseContainer($_db).resultSet<i3.Tab>('tab'),
|
||||||
)
|
)
|
||||||
.filter((f) => f.containerId.id.sqlEquals($_itemColumn<String>('id')!));
|
.filter((f) => f.containerId.id.sqlEquals($_itemColumn<String>('id')!));
|
||||||
|
|
||||||
@@ -110,7 +112,7 @@ class $ContainerFilterComposer
|
|||||||
final i3.$TabFilterComposer composer = $composerBuilder(
|
final i3.$TabFilterComposer composer = $composerBuilder(
|
||||||
composer: this,
|
composer: this,
|
||||||
getCurrentColumn: (t) => t.id,
|
getCurrentColumn: (t) => t.id,
|
||||||
referencedTable: i8.ReadDatabaseContainer($db).resultSet<i3.Tab>('tab'),
|
referencedTable: i9.ReadDatabaseContainer($db).resultSet<i3.Tab>('tab'),
|
||||||
getReferencedColumn: (t) => t.containerId,
|
getReferencedColumn: (t) => t.containerId,
|
||||||
builder:
|
builder:
|
||||||
(
|
(
|
||||||
@@ -119,7 +121,7 @@ class $ContainerFilterComposer
|
|||||||
$removeJoinBuilderFromRootComposer,
|
$removeJoinBuilderFromRootComposer,
|
||||||
}) => i3.$TabFilterComposer(
|
}) => i3.$TabFilterComposer(
|
||||||
$db: $db,
|
$db: $db,
|
||||||
$table: i8.ReadDatabaseContainer($db).resultSet<i3.Tab>('tab'),
|
$table: i9.ReadDatabaseContainer($db).resultSet<i3.Tab>('tab'),
|
||||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||||
joinBuilder: joinBuilder,
|
joinBuilder: joinBuilder,
|
||||||
$removeJoinBuilderFromRootComposer:
|
$removeJoinBuilderFromRootComposer:
|
||||||
@@ -188,7 +190,7 @@ class $ContainerAnnotationComposer
|
|||||||
final i3.$TabAnnotationComposer composer = $composerBuilder(
|
final i3.$TabAnnotationComposer composer = $composerBuilder(
|
||||||
composer: this,
|
composer: this,
|
||||||
getCurrentColumn: (t) => t.id,
|
getCurrentColumn: (t) => t.id,
|
||||||
referencedTable: i8.ReadDatabaseContainer($db).resultSet<i3.Tab>('tab'),
|
referencedTable: i9.ReadDatabaseContainer($db).resultSet<i3.Tab>('tab'),
|
||||||
getReferencedColumn: (t) => t.containerId,
|
getReferencedColumn: (t) => t.containerId,
|
||||||
builder:
|
builder:
|
||||||
(
|
(
|
||||||
@@ -197,7 +199,7 @@ class $ContainerAnnotationComposer
|
|||||||
$removeJoinBuilderFromRootComposer,
|
$removeJoinBuilderFromRootComposer,
|
||||||
}) => i3.$TabAnnotationComposer(
|
}) => i3.$TabAnnotationComposer(
|
||||||
$db: $db,
|
$db: $db,
|
||||||
$table: i8.ReadDatabaseContainer($db).resultSet<i3.Tab>('tab'),
|
$table: i9.ReadDatabaseContainer($db).resultSet<i3.Tab>('tab'),
|
||||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||||
joinBuilder: joinBuilder,
|
joinBuilder: joinBuilder,
|
||||||
$removeJoinBuilderFromRootComposer:
|
$removeJoinBuilderFromRootComposer:
|
||||||
@@ -275,7 +277,7 @@ class $ContainerTableManager
|
|||||||
db: db,
|
db: db,
|
||||||
explicitlyWatchedTables: [
|
explicitlyWatchedTables: [
|
||||||
if (tabRefs)
|
if (tabRefs)
|
||||||
i8.ReadDatabaseContainer(db).resultSet<i3.Tab>('tab'),
|
i9.ReadDatabaseContainer(db).resultSet<i3.Tab>('tab'),
|
||||||
],
|
],
|
||||||
addJoins: null,
|
addJoins: null,
|
||||||
getPrefetchedDataCallback: (items) async {
|
getPrefetchedDataCallback: (items) async {
|
||||||
@@ -329,7 +331,8 @@ typedef $TabCreateCompanionBuilder =
|
|||||||
required String orderKey,
|
required String orderKey,
|
||||||
i0.Value<Uri?> url,
|
i0.Value<Uri?> url,
|
||||||
i0.Value<String?> title,
|
i0.Value<String?> title,
|
||||||
i0.Value<bool?> isPrivate,
|
i0.Value<i7.TabModeDbValue> tabMode,
|
||||||
|
i0.Value<String?> isolationContextId,
|
||||||
i0.Value<bool?> isProbablyReaderable,
|
i0.Value<bool?> isProbablyReaderable,
|
||||||
i0.Value<String?> extractedContentMarkdown,
|
i0.Value<String?> extractedContentMarkdown,
|
||||||
i0.Value<String?> extractedContentPlain,
|
i0.Value<String?> extractedContentPlain,
|
||||||
@@ -347,7 +350,8 @@ typedef $TabUpdateCompanionBuilder =
|
|||||||
i0.Value<String> orderKey,
|
i0.Value<String> orderKey,
|
||||||
i0.Value<Uri?> url,
|
i0.Value<Uri?> url,
|
||||||
i0.Value<String?> title,
|
i0.Value<String?> title,
|
||||||
i0.Value<bool?> isPrivate,
|
i0.Value<i7.TabModeDbValue> tabMode,
|
||||||
|
i0.Value<String?> isolationContextId,
|
||||||
i0.Value<bool?> isProbablyReaderable,
|
i0.Value<bool?> isProbablyReaderable,
|
||||||
i0.Value<String?> extractedContentMarkdown,
|
i0.Value<String?> extractedContentMarkdown,
|
||||||
i0.Value<String?> extractedContentPlain,
|
i0.Value<String?> extractedContentPlain,
|
||||||
@@ -362,12 +366,12 @@ final class $TabReferences
|
|||||||
$TabReferences(super.$_db, super.$_table, super.$_typedResult);
|
$TabReferences(super.$_db, super.$_table, super.$_typedResult);
|
||||||
|
|
||||||
static i3.Container _containerIdTable(i0.GeneratedDatabase db) =>
|
static i3.Container _containerIdTable(i0.GeneratedDatabase db) =>
|
||||||
i8.ReadDatabaseContainer(db)
|
i9.ReadDatabaseContainer(db)
|
||||||
.resultSet<i3.Container>('container')
|
.resultSet<i3.Container>('container')
|
||||||
.createAlias(
|
.createAlias(
|
||||||
i0.$_aliasNameGenerator(
|
i0.$_aliasNameGenerator(
|
||||||
i8.ReadDatabaseContainer(db).resultSet<i3.Tab>('tab').containerId,
|
i9.ReadDatabaseContainer(db).resultSet<i3.Tab>('tab').containerId,
|
||||||
i8.ReadDatabaseContainer(
|
i9.ReadDatabaseContainer(
|
||||||
db,
|
db,
|
||||||
).resultSet<i3.Container>('container').id,
|
).resultSet<i3.Container>('container').id,
|
||||||
),
|
),
|
||||||
@@ -379,7 +383,7 @@ final class $TabReferences
|
|||||||
final manager = i3
|
final manager = i3
|
||||||
.$ContainerTableManager(
|
.$ContainerTableManager(
|
||||||
$_db,
|
$_db,
|
||||||
i8.ReadDatabaseContainer($_db).resultSet<i3.Container>('container'),
|
i9.ReadDatabaseContainer($_db).resultSet<i3.Container>('container'),
|
||||||
)
|
)
|
||||||
.filter((f) => f.id.sqlEquals($_column));
|
.filter((f) => f.id.sqlEquals($_column));
|
||||||
final item = $_typedResult.readTableOrNull(_containerIdTable($_db));
|
final item = $_typedResult.readTableOrNull(_containerIdTable($_db));
|
||||||
@@ -430,8 +434,14 @@ class $TabFilterComposer extends i0.Composer<i0.GeneratedDatabase, i3.Tab> {
|
|||||||
builder: (column) => i0.ColumnFilters(column),
|
builder: (column) => i0.ColumnFilters(column),
|
||||||
);
|
);
|
||||||
|
|
||||||
i0.ColumnFilters<bool> get isPrivate => $composableBuilder(
|
i0.ColumnWithTypeConverterFilters<i7.TabModeDbValue, i7.TabModeDbValue, int>
|
||||||
column: $table.isPrivate,
|
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),
|
builder: (column) => i0.ColumnFilters(column),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -469,7 +479,7 @@ class $TabFilterComposer extends i0.Composer<i0.GeneratedDatabase, i3.Tab> {
|
|||||||
final i3.$ContainerFilterComposer composer = $composerBuilder(
|
final i3.$ContainerFilterComposer composer = $composerBuilder(
|
||||||
composer: this,
|
composer: this,
|
||||||
getCurrentColumn: (t) => t.containerId,
|
getCurrentColumn: (t) => t.containerId,
|
||||||
referencedTable: i8.ReadDatabaseContainer(
|
referencedTable: i9.ReadDatabaseContainer(
|
||||||
$db,
|
$db,
|
||||||
).resultSet<i3.Container>('container'),
|
).resultSet<i3.Container>('container'),
|
||||||
getReferencedColumn: (t) => t.id,
|
getReferencedColumn: (t) => t.id,
|
||||||
@@ -480,7 +490,7 @@ class $TabFilterComposer extends i0.Composer<i0.GeneratedDatabase, i3.Tab> {
|
|||||||
$removeJoinBuilderFromRootComposer,
|
$removeJoinBuilderFromRootComposer,
|
||||||
}) => i3.$ContainerFilterComposer(
|
}) => i3.$ContainerFilterComposer(
|
||||||
$db: $db,
|
$db: $db,
|
||||||
$table: i8.ReadDatabaseContainer(
|
$table: i9.ReadDatabaseContainer(
|
||||||
$db,
|
$db,
|
||||||
).resultSet<i3.Container>('container'),
|
).resultSet<i3.Container>('container'),
|
||||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||||
@@ -531,8 +541,13 @@ class $TabOrderingComposer extends i0.Composer<i0.GeneratedDatabase, i3.Tab> {
|
|||||||
builder: (column) => i0.ColumnOrderings(column),
|
builder: (column) => i0.ColumnOrderings(column),
|
||||||
);
|
);
|
||||||
|
|
||||||
i0.ColumnOrderings<bool> get isPrivate => $composableBuilder(
|
i0.ColumnOrderings<int> get tabMode => $composableBuilder(
|
||||||
column: $table.isPrivate,
|
column: $table.tabMode,
|
||||||
|
builder: (column) => i0.ColumnOrderings(column),
|
||||||
|
);
|
||||||
|
|
||||||
|
i0.ColumnOrderings<String> get isolationContextId => $composableBuilder(
|
||||||
|
column: $table.isolationContextId,
|
||||||
builder: (column) => i0.ColumnOrderings(column),
|
builder: (column) => i0.ColumnOrderings(column),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -570,7 +585,7 @@ class $TabOrderingComposer extends i0.Composer<i0.GeneratedDatabase, i3.Tab> {
|
|||||||
final i3.$ContainerOrderingComposer composer = $composerBuilder(
|
final i3.$ContainerOrderingComposer composer = $composerBuilder(
|
||||||
composer: this,
|
composer: this,
|
||||||
getCurrentColumn: (t) => t.containerId,
|
getCurrentColumn: (t) => t.containerId,
|
||||||
referencedTable: i8.ReadDatabaseContainer(
|
referencedTable: i9.ReadDatabaseContainer(
|
||||||
$db,
|
$db,
|
||||||
).resultSet<i3.Container>('container'),
|
).resultSet<i3.Container>('container'),
|
||||||
getReferencedColumn: (t) => t.id,
|
getReferencedColumn: (t) => t.id,
|
||||||
@@ -581,7 +596,7 @@ class $TabOrderingComposer extends i0.Composer<i0.GeneratedDatabase, i3.Tab> {
|
|||||||
$removeJoinBuilderFromRootComposer,
|
$removeJoinBuilderFromRootComposer,
|
||||||
}) => i3.$ContainerOrderingComposer(
|
}) => i3.$ContainerOrderingComposer(
|
||||||
$db: $db,
|
$db: $db,
|
||||||
$table: i8.ReadDatabaseContainer(
|
$table: i9.ReadDatabaseContainer(
|
||||||
$db,
|
$db,
|
||||||
).resultSet<i3.Container>('container'),
|
).resultSet<i3.Container>('container'),
|
||||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||||
@@ -620,8 +635,13 @@ class $TabAnnotationComposer extends i0.Composer<i0.GeneratedDatabase, i3.Tab> {
|
|||||||
i0.GeneratedColumn<String> get title =>
|
i0.GeneratedColumn<String> get title =>
|
||||||
$composableBuilder(column: $table.title, builder: (column) => column);
|
$composableBuilder(column: $table.title, builder: (column) => column);
|
||||||
|
|
||||||
i0.GeneratedColumn<bool> get isPrivate =>
|
i0.GeneratedColumnWithTypeConverter<i7.TabModeDbValue, int> get tabMode =>
|
||||||
$composableBuilder(column: $table.isPrivate, builder: (column) => column);
|
$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(
|
i0.GeneratedColumn<bool> get isProbablyReaderable => $composableBuilder(
|
||||||
column: $table.isProbablyReaderable,
|
column: $table.isProbablyReaderable,
|
||||||
@@ -655,7 +675,7 @@ class $TabAnnotationComposer extends i0.Composer<i0.GeneratedDatabase, i3.Tab> {
|
|||||||
final i3.$ContainerAnnotationComposer composer = $composerBuilder(
|
final i3.$ContainerAnnotationComposer composer = $composerBuilder(
|
||||||
composer: this,
|
composer: this,
|
||||||
getCurrentColumn: (t) => t.containerId,
|
getCurrentColumn: (t) => t.containerId,
|
||||||
referencedTable: i8.ReadDatabaseContainer(
|
referencedTable: i9.ReadDatabaseContainer(
|
||||||
$db,
|
$db,
|
||||||
).resultSet<i3.Container>('container'),
|
).resultSet<i3.Container>('container'),
|
||||||
getReferencedColumn: (t) => t.id,
|
getReferencedColumn: (t) => t.id,
|
||||||
@@ -666,7 +686,7 @@ class $TabAnnotationComposer extends i0.Composer<i0.GeneratedDatabase, i3.Tab> {
|
|||||||
$removeJoinBuilderFromRootComposer,
|
$removeJoinBuilderFromRootComposer,
|
||||||
}) => i3.$ContainerAnnotationComposer(
|
}) => i3.$ContainerAnnotationComposer(
|
||||||
$db: $db,
|
$db: $db,
|
||||||
$table: i8.ReadDatabaseContainer(
|
$table: i9.ReadDatabaseContainer(
|
||||||
$db,
|
$db,
|
||||||
).resultSet<i3.Container>('container'),
|
).resultSet<i3.Container>('container'),
|
||||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||||
@@ -714,7 +734,8 @@ class $TabTableManager
|
|||||||
i0.Value<String> orderKey = const i0.Value.absent(),
|
i0.Value<String> orderKey = const i0.Value.absent(),
|
||||||
i0.Value<Uri?> url = const i0.Value.absent(),
|
i0.Value<Uri?> url = const i0.Value.absent(),
|
||||||
i0.Value<String?> title = 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<bool?> isProbablyReaderable = const i0.Value.absent(),
|
||||||
i0.Value<String?> extractedContentMarkdown =
|
i0.Value<String?> extractedContentMarkdown =
|
||||||
const i0.Value.absent(),
|
const i0.Value.absent(),
|
||||||
@@ -732,7 +753,8 @@ class $TabTableManager
|
|||||||
orderKey: orderKey,
|
orderKey: orderKey,
|
||||||
url: url,
|
url: url,
|
||||||
title: title,
|
title: title,
|
||||||
isPrivate: isPrivate,
|
tabMode: tabMode,
|
||||||
|
isolationContextId: isolationContextId,
|
||||||
isProbablyReaderable: isProbablyReaderable,
|
isProbablyReaderable: isProbablyReaderable,
|
||||||
extractedContentMarkdown: extractedContentMarkdown,
|
extractedContentMarkdown: extractedContentMarkdown,
|
||||||
extractedContentPlain: extractedContentPlain,
|
extractedContentPlain: extractedContentPlain,
|
||||||
@@ -750,7 +772,8 @@ class $TabTableManager
|
|||||||
required String orderKey,
|
required String orderKey,
|
||||||
i0.Value<Uri?> url = const i0.Value.absent(),
|
i0.Value<Uri?> url = const i0.Value.absent(),
|
||||||
i0.Value<String?> title = 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<bool?> isProbablyReaderable = const i0.Value.absent(),
|
||||||
i0.Value<String?> extractedContentMarkdown =
|
i0.Value<String?> extractedContentMarkdown =
|
||||||
const i0.Value.absent(),
|
const i0.Value.absent(),
|
||||||
@@ -768,7 +791,8 @@ class $TabTableManager
|
|||||||
orderKey: orderKey,
|
orderKey: orderKey,
|
||||||
url: url,
|
url: url,
|
||||||
title: title,
|
title: title,
|
||||||
isPrivate: isPrivate,
|
tabMode: tabMode,
|
||||||
|
isolationContextId: isolationContextId,
|
||||||
isProbablyReaderable: isProbablyReaderable,
|
isProbablyReaderable: isProbablyReaderable,
|
||||||
extractedContentMarkdown: extractedContentMarkdown,
|
extractedContentMarkdown: extractedContentMarkdown,
|
||||||
extractedContentPlain: extractedContentPlain,
|
extractedContentPlain: extractedContentPlain,
|
||||||
@@ -1269,14 +1293,25 @@ class Tab extends i0.Table with i0.TableInfo<Tab, i3.TabData> {
|
|||||||
requiredDuringInsert: false,
|
requiredDuringInsert: false,
|
||||||
$customConstraints: '',
|
$customConstraints: '',
|
||||||
);
|
);
|
||||||
late final i0.GeneratedColumn<bool> isPrivate = i0.GeneratedColumn<bool>(
|
late final i0.GeneratedColumnWithTypeConverter<i7.TabModeDbValue, int>
|
||||||
'is_private',
|
tabMode = i0.GeneratedColumn<int>(
|
||||||
|
'tab_mode',
|
||||||
aliasedName,
|
aliasedName,
|
||||||
true,
|
false,
|
||||||
type: i0.DriftSqlType.bool,
|
type: i0.DriftSqlType.int,
|
||||||
requiredDuringInsert: false,
|
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 =
|
late final i0.GeneratedColumn<bool> isProbablyReaderable =
|
||||||
i0.GeneratedColumn<bool>(
|
i0.GeneratedColumn<bool>(
|
||||||
'is_probably_readerable',
|
'is_probably_readerable',
|
||||||
@@ -1340,7 +1375,8 @@ class Tab extends i0.Table with i0.TableInfo<Tab, i3.TabData> {
|
|||||||
orderKey,
|
orderKey,
|
||||||
url,
|
url,
|
||||||
title,
|
title,
|
||||||
isPrivate,
|
tabMode,
|
||||||
|
isolationContextId,
|
||||||
isProbablyReaderable,
|
isProbablyReaderable,
|
||||||
extractedContentMarkdown,
|
extractedContentMarkdown,
|
||||||
extractedContentPlain,
|
extractedContentPlain,
|
||||||
@@ -1391,9 +1427,15 @@ class Tab extends i0.Table with i0.TableInfo<Tab, i3.TabData> {
|
|||||||
i0.DriftSqlType.string,
|
i0.DriftSqlType.string,
|
||||||
data['${effectivePrefix}title'],
|
data['${effectivePrefix}title'],
|
||||||
),
|
),
|
||||||
isPrivate: attachedDatabase.typeMapping.read(
|
tabMode: i3.Tab.$convertertabMode.fromSql(
|
||||||
i0.DriftSqlType.bool,
|
attachedDatabase.typeMapping.read(
|
||||||
data['${effectivePrefix}is_private'],
|
i0.DriftSqlType.int,
|
||||||
|
data['${effectivePrefix}tab_mode'],
|
||||||
|
)!,
|
||||||
|
),
|
||||||
|
isolationContextId: attachedDatabase.typeMapping.read(
|
||||||
|
i0.DriftSqlType.string,
|
||||||
|
data['${effectivePrefix}isolation_context_id'],
|
||||||
),
|
),
|
||||||
isProbablyReaderable: attachedDatabase.typeMapping.read(
|
isProbablyReaderable: attachedDatabase.typeMapping.read(
|
||||||
i0.DriftSqlType.bool,
|
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 =
|
static i0.JsonTypeConverter2<i6.TabSource, int, int> $convertersource =
|
||||||
const i0.EnumIndexConverter<i6.TabSource>(i6.TabSource.values);
|
const i0.EnumIndexConverter<i6.TabSource>(i6.TabSource.values);
|
||||||
static i0.TypeConverter<Uri?, String?> $converterurl =
|
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
|
@override
|
||||||
bool get dontWriteConstraints => true;
|
bool get dontWriteConstraints => true;
|
||||||
}
|
}
|
||||||
@@ -1443,7 +1491,8 @@ class TabData extends i0.DataClass implements i0.Insertable<i3.TabData> {
|
|||||||
final String orderKey;
|
final String orderKey;
|
||||||
final Uri? url;
|
final Uri? url;
|
||||||
final String? title;
|
final String? title;
|
||||||
final bool? isPrivate;
|
final i7.TabModeDbValue tabMode;
|
||||||
|
final String? isolationContextId;
|
||||||
final bool? isProbablyReaderable;
|
final bool? isProbablyReaderable;
|
||||||
final String? extractedContentMarkdown;
|
final String? extractedContentMarkdown;
|
||||||
final String? extractedContentPlain;
|
final String? extractedContentPlain;
|
||||||
@@ -1458,7 +1507,8 @@ class TabData extends i0.DataClass implements i0.Insertable<i3.TabData> {
|
|||||||
required this.orderKey,
|
required this.orderKey,
|
||||||
this.url,
|
this.url,
|
||||||
this.title,
|
this.title,
|
||||||
this.isPrivate,
|
required this.tabMode,
|
||||||
|
this.isolationContextId,
|
||||||
this.isProbablyReaderable,
|
this.isProbablyReaderable,
|
||||||
this.extractedContentMarkdown,
|
this.extractedContentMarkdown,
|
||||||
this.extractedContentPlain,
|
this.extractedContentPlain,
|
||||||
@@ -1486,8 +1536,13 @@ class TabData extends i0.DataClass implements i0.Insertable<i3.TabData> {
|
|||||||
if (!nullToAbsent || title != null) {
|
if (!nullToAbsent || title != null) {
|
||||||
map['title'] = i0.Variable<String>(title);
|
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) {
|
if (!nullToAbsent || isProbablyReaderable != null) {
|
||||||
map['is_probably_readerable'] = i0.Variable<bool>(isProbablyReaderable);
|
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']),
|
orderKey: serializer.fromJson<String>(json['order_key']),
|
||||||
url: serializer.fromJson<Uri?>(json['url']),
|
url: serializer.fromJson<Uri?>(json['url']),
|
||||||
title: serializer.fromJson<String?>(json['title']),
|
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?>(
|
isProbablyReaderable: serializer.fromJson<bool?>(
|
||||||
json['is_probably_readerable'],
|
json['is_probably_readerable'],
|
||||||
),
|
),
|
||||||
@@ -1557,7 +1617,10 @@ class TabData extends i0.DataClass implements i0.Insertable<i3.TabData> {
|
|||||||
'order_key': serializer.toJson<String>(orderKey),
|
'order_key': serializer.toJson<String>(orderKey),
|
||||||
'url': serializer.toJson<Uri?>(url),
|
'url': serializer.toJson<Uri?>(url),
|
||||||
'title': serializer.toJson<String?>(title),
|
'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),
|
'is_probably_readerable': serializer.toJson<bool?>(isProbablyReaderable),
|
||||||
'extracted_content_markdown': serializer.toJson<String?>(
|
'extracted_content_markdown': serializer.toJson<String?>(
|
||||||
extractedContentMarkdown,
|
extractedContentMarkdown,
|
||||||
@@ -1579,7 +1642,8 @@ class TabData extends i0.DataClass implements i0.Insertable<i3.TabData> {
|
|||||||
String? orderKey,
|
String? orderKey,
|
||||||
i0.Value<Uri?> url = const i0.Value.absent(),
|
i0.Value<Uri?> url = const i0.Value.absent(),
|
||||||
i0.Value<String?> title = 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<bool?> isProbablyReaderable = const i0.Value.absent(),
|
||||||
i0.Value<String?> extractedContentMarkdown = const i0.Value.absent(),
|
i0.Value<String?> extractedContentMarkdown = const i0.Value.absent(),
|
||||||
i0.Value<String?> extractedContentPlain = 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,
|
orderKey: orderKey ?? this.orderKey,
|
||||||
url: url.present ? url.value : this.url,
|
url: url.present ? url.value : this.url,
|
||||||
title: title.present ? title.value : this.title,
|
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: isProbablyReaderable.present
|
||||||
? isProbablyReaderable.value
|
? isProbablyReaderable.value
|
||||||
: this.isProbablyReaderable,
|
: 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,
|
orderKey: data.orderKey.present ? data.orderKey.value : this.orderKey,
|
||||||
url: data.url.present ? data.url.value : this.url,
|
url: data.url.present ? data.url.value : this.url,
|
||||||
title: data.title.present ? data.title.value : this.title,
|
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
|
isProbablyReaderable: data.isProbablyReaderable.present
|
||||||
? data.isProbablyReaderable.value
|
? data.isProbablyReaderable.value
|
||||||
: this.isProbablyReaderable,
|
: this.isProbablyReaderable,
|
||||||
@@ -1653,7 +1723,8 @@ class TabData extends i0.DataClass implements i0.Insertable<i3.TabData> {
|
|||||||
..write('orderKey: $orderKey, ')
|
..write('orderKey: $orderKey, ')
|
||||||
..write('url: $url, ')
|
..write('url: $url, ')
|
||||||
..write('title: $title, ')
|
..write('title: $title, ')
|
||||||
..write('isPrivate: $isPrivate, ')
|
..write('tabMode: $tabMode, ')
|
||||||
|
..write('isolationContextId: $isolationContextId, ')
|
||||||
..write('isProbablyReaderable: $isProbablyReaderable, ')
|
..write('isProbablyReaderable: $isProbablyReaderable, ')
|
||||||
..write('extractedContentMarkdown: $extractedContentMarkdown, ')
|
..write('extractedContentMarkdown: $extractedContentMarkdown, ')
|
||||||
..write('extractedContentPlain: $extractedContentPlain, ')
|
..write('extractedContentPlain: $extractedContentPlain, ')
|
||||||
@@ -1673,7 +1744,8 @@ class TabData extends i0.DataClass implements i0.Insertable<i3.TabData> {
|
|||||||
orderKey,
|
orderKey,
|
||||||
url,
|
url,
|
||||||
title,
|
title,
|
||||||
isPrivate,
|
tabMode,
|
||||||
|
isolationContextId,
|
||||||
isProbablyReaderable,
|
isProbablyReaderable,
|
||||||
extractedContentMarkdown,
|
extractedContentMarkdown,
|
||||||
extractedContentPlain,
|
extractedContentPlain,
|
||||||
@@ -1692,7 +1764,8 @@ class TabData extends i0.DataClass implements i0.Insertable<i3.TabData> {
|
|||||||
other.orderKey == this.orderKey &&
|
other.orderKey == this.orderKey &&
|
||||||
other.url == this.url &&
|
other.url == this.url &&
|
||||||
other.title == this.title &&
|
other.title == this.title &&
|
||||||
other.isPrivate == this.isPrivate &&
|
other.tabMode == this.tabMode &&
|
||||||
|
other.isolationContextId == this.isolationContextId &&
|
||||||
other.isProbablyReaderable == this.isProbablyReaderable &&
|
other.isProbablyReaderable == this.isProbablyReaderable &&
|
||||||
other.extractedContentMarkdown == this.extractedContentMarkdown &&
|
other.extractedContentMarkdown == this.extractedContentMarkdown &&
|
||||||
other.extractedContentPlain == this.extractedContentPlain &&
|
other.extractedContentPlain == this.extractedContentPlain &&
|
||||||
@@ -1709,7 +1782,8 @@ class TabCompanion extends i0.UpdateCompanion<i3.TabData> {
|
|||||||
final i0.Value<String> orderKey;
|
final i0.Value<String> orderKey;
|
||||||
final i0.Value<Uri?> url;
|
final i0.Value<Uri?> url;
|
||||||
final i0.Value<String?> title;
|
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<bool?> isProbablyReaderable;
|
||||||
final i0.Value<String?> extractedContentMarkdown;
|
final i0.Value<String?> extractedContentMarkdown;
|
||||||
final i0.Value<String?> extractedContentPlain;
|
final i0.Value<String?> extractedContentPlain;
|
||||||
@@ -1725,7 +1799,8 @@ class TabCompanion extends i0.UpdateCompanion<i3.TabData> {
|
|||||||
this.orderKey = const i0.Value.absent(),
|
this.orderKey = const i0.Value.absent(),
|
||||||
this.url = const i0.Value.absent(),
|
this.url = const i0.Value.absent(),
|
||||||
this.title = 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.isProbablyReaderable = const i0.Value.absent(),
|
||||||
this.extractedContentMarkdown = const i0.Value.absent(),
|
this.extractedContentMarkdown = const i0.Value.absent(),
|
||||||
this.extractedContentPlain = const i0.Value.absent(),
|
this.extractedContentPlain = const i0.Value.absent(),
|
||||||
@@ -1742,7 +1817,8 @@ class TabCompanion extends i0.UpdateCompanion<i3.TabData> {
|
|||||||
required String orderKey,
|
required String orderKey,
|
||||||
this.url = const i0.Value.absent(),
|
this.url = const i0.Value.absent(),
|
||||||
this.title = 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.isProbablyReaderable = const i0.Value.absent(),
|
||||||
this.extractedContentMarkdown = const i0.Value.absent(),
|
this.extractedContentMarkdown = const i0.Value.absent(),
|
||||||
this.extractedContentPlain = 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>? orderKey,
|
||||||
i0.Expression<String>? url,
|
i0.Expression<String>? url,
|
||||||
i0.Expression<String>? title,
|
i0.Expression<String>? title,
|
||||||
i0.Expression<bool>? isPrivate,
|
i0.Expression<int>? tabMode,
|
||||||
|
i0.Expression<String>? isolationContextId,
|
||||||
i0.Expression<bool>? isProbablyReaderable,
|
i0.Expression<bool>? isProbablyReaderable,
|
||||||
i0.Expression<String>? extractedContentMarkdown,
|
i0.Expression<String>? extractedContentMarkdown,
|
||||||
i0.Expression<String>? extractedContentPlain,
|
i0.Expression<String>? extractedContentPlain,
|
||||||
@@ -1779,7 +1856,9 @@ class TabCompanion extends i0.UpdateCompanion<i3.TabData> {
|
|||||||
if (orderKey != null) 'order_key': orderKey,
|
if (orderKey != null) 'order_key': orderKey,
|
||||||
if (url != null) 'url': url,
|
if (url != null) 'url': url,
|
||||||
if (title != null) 'title': title,
|
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)
|
if (isProbablyReaderable != null)
|
||||||
'is_probably_readerable': isProbablyReaderable,
|
'is_probably_readerable': isProbablyReaderable,
|
||||||
if (extractedContentMarkdown != null)
|
if (extractedContentMarkdown != null)
|
||||||
@@ -1802,7 +1881,8 @@ class TabCompanion extends i0.UpdateCompanion<i3.TabData> {
|
|||||||
i0.Value<String>? orderKey,
|
i0.Value<String>? orderKey,
|
||||||
i0.Value<Uri?>? url,
|
i0.Value<Uri?>? url,
|
||||||
i0.Value<String?>? title,
|
i0.Value<String?>? title,
|
||||||
i0.Value<bool?>? isPrivate,
|
i0.Value<i7.TabModeDbValue>? tabMode,
|
||||||
|
i0.Value<String?>? isolationContextId,
|
||||||
i0.Value<bool?>? isProbablyReaderable,
|
i0.Value<bool?>? isProbablyReaderable,
|
||||||
i0.Value<String?>? extractedContentMarkdown,
|
i0.Value<String?>? extractedContentMarkdown,
|
||||||
i0.Value<String?>? extractedContentPlain,
|
i0.Value<String?>? extractedContentPlain,
|
||||||
@@ -1819,7 +1899,8 @@ class TabCompanion extends i0.UpdateCompanion<i3.TabData> {
|
|||||||
orderKey: orderKey ?? this.orderKey,
|
orderKey: orderKey ?? this.orderKey,
|
||||||
url: url ?? this.url,
|
url: url ?? this.url,
|
||||||
title: title ?? this.title,
|
title: title ?? this.title,
|
||||||
isPrivate: isPrivate ?? this.isPrivate,
|
tabMode: tabMode ?? this.tabMode,
|
||||||
|
isolationContextId: isolationContextId ?? this.isolationContextId,
|
||||||
isProbablyReaderable: isProbablyReaderable ?? this.isProbablyReaderable,
|
isProbablyReaderable: isProbablyReaderable ?? this.isProbablyReaderable,
|
||||||
extractedContentMarkdown:
|
extractedContentMarkdown:
|
||||||
extractedContentMarkdown ?? this.extractedContentMarkdown,
|
extractedContentMarkdown ?? this.extractedContentMarkdown,
|
||||||
@@ -1858,8 +1939,15 @@ class TabCompanion extends i0.UpdateCompanion<i3.TabData> {
|
|||||||
if (title.present) {
|
if (title.present) {
|
||||||
map['title'] = i0.Variable<String>(title.value);
|
map['title'] = i0.Variable<String>(title.value);
|
||||||
}
|
}
|
||||||
if (isPrivate.present) {
|
if (tabMode.present) {
|
||||||
map['is_private'] = i0.Variable<bool>(isPrivate.value);
|
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) {
|
if (isProbablyReaderable.present) {
|
||||||
map['is_probably_readerable'] = i0.Variable<bool>(
|
map['is_probably_readerable'] = i0.Variable<bool>(
|
||||||
@@ -1903,7 +1991,8 @@ class TabCompanion extends i0.UpdateCompanion<i3.TabData> {
|
|||||||
..write('orderKey: $orderKey, ')
|
..write('orderKey: $orderKey, ')
|
||||||
..write('url: $url, ')
|
..write('url: $url, ')
|
||||||
..write('title: $title, ')
|
..write('title: $title, ')
|
||||||
..write('isPrivate: $isPrivate, ')
|
..write('tabMode: $tabMode, ')
|
||||||
|
..write('isolationContextId: $isolationContextId, ')
|
||||||
..write('isProbablyReaderable: $isProbablyReaderable, ')
|
..write('isProbablyReaderable: $isProbablyReaderable, ')
|
||||||
..write('extractedContentMarkdown: $extractedContentMarkdown, ')
|
..write('extractedContentMarkdown: $extractedContentMarkdown, ')
|
||||||
..write('extractedContentPlain: $extractedContentPlain, ')
|
..write('extractedContentPlain: $extractedContentPlain, ')
|
||||||
@@ -2211,7 +2300,7 @@ i0.Trigger get tabAfterUpdate => i0.Trigger(
|
|||||||
'tab_after_update',
|
'tab_after_update',
|
||||||
);
|
);
|
||||||
|
|
||||||
class DefinitionsDrift extends i8.ModularAccessor {
|
class DefinitionsDrift extends i9.ModularAccessor {
|
||||||
DefinitionsDrift(i0.GeneratedDatabase db) : super(db);
|
DefinitionsDrift(i0.GeneratedDatabase db) : super(db);
|
||||||
Future<int> optimizeFtsIndex() {
|
Future<int> optimizeFtsIndex() {
|
||||||
return customInsert(
|
return customInsert(
|
||||||
@@ -2284,19 +2373,19 @@ class DefinitionsDrift extends i8.ModularAccessor {
|
|||||||
).map((i0.QueryRow row) => row.read<String>('_c0'));
|
).map((i0.QueryRow row) => row.read<String>('_c0'));
|
||||||
}
|
}
|
||||||
|
|
||||||
i0.Selectable<i9.TabQueryResult> queryTabsBasic({
|
i0.Selectable<i10.TabQueryResult> queryTabsBasic({
|
||||||
required String query,
|
required String query,
|
||||||
required int limit,
|
required int limit,
|
||||||
}) {
|
}) {
|
||||||
return customSelect(
|
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)],
|
variables: [i0.Variable<String>(query), i0.Variable<int>(limit)],
|
||||||
readsFrom: {tab, tabFts},
|
readsFrom: {tab, tabFts},
|
||||||
).map(
|
).map(
|
||||||
(i0.QueryRow row) => i9.TabQueryResult(
|
(i0.QueryRow row) => i10.TabQueryResult(
|
||||||
id: row.read<String>('id'),
|
id: row.read<String>('id'),
|
||||||
containerId: row.readNullable<String>('container_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'),
|
title: row.readNullable<String>('title'),
|
||||||
url: row.readNullable<String>('url'),
|
url: row.readNullable<String>('url'),
|
||||||
cleanUrl: i3.Tab.$converterurl.fromSql(
|
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 beforeMatch,
|
||||||
required String afterMatch,
|
required String afterMatch,
|
||||||
required String ellipsis,
|
required String ellipsis,
|
||||||
@@ -2316,7 +2405,7 @@ class DefinitionsDrift extends i8.ModularAccessor {
|
|||||||
required int limit,
|
required int limit,
|
||||||
}) {
|
}) {
|
||||||
return customSelect(
|
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: [
|
variables: [
|
||||||
i0.Variable<String>(beforeMatch),
|
i0.Variable<String>(beforeMatch),
|
||||||
i0.Variable<String>(afterMatch),
|
i0.Variable<String>(afterMatch),
|
||||||
@@ -2327,10 +2416,10 @@ class DefinitionsDrift extends i8.ModularAccessor {
|
|||||||
],
|
],
|
||||||
readsFrom: {tab, tabFts},
|
readsFrom: {tab, tabFts},
|
||||||
).map(
|
).map(
|
||||||
(i0.QueryRow row) => i9.TabQueryResult(
|
(i0.QueryRow row) => i10.TabQueryResult(
|
||||||
id: row.read<String>('id'),
|
id: row.read<String>('id'),
|
||||||
containerId: row.readNullable<String>('container_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'),
|
title: row.readNullable<String>('title'),
|
||||||
url: row.readNullable<String>('url'),
|
url: row.readNullable<String>('url'),
|
||||||
cleanUrl: i3.Tab.$converterurl.fromSql(
|
cleanUrl: i3.Tab.$converterurl.fromSql(
|
||||||
@@ -2443,13 +2532,13 @@ class DefinitionsDrift extends i8.ModularAccessor {
|
|||||||
).map((i0.QueryRow row) => row.read<String>('id'));
|
).map((i0.QueryRow row) => row.read<String>('id'));
|
||||||
}
|
}
|
||||||
|
|
||||||
i0.Selectable<i10.SiteAssignment> allAssignedSites() {
|
i0.Selectable<i11.SiteAssignment> allAssignedSites() {
|
||||||
return customSelect(
|
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',
|
'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: [],
|
variables: [],
|
||||||
readsFrom: {container},
|
readsFrom: {container},
|
||||||
).map(
|
).map(
|
||||||
(i0.QueryRow row) => i10.SiteAssignment(
|
(i0.QueryRow row) => i11.SiteAssignment(
|
||||||
id: row.read<String>('id'),
|
id: row.read<String>('id'),
|
||||||
contextualIdentity: row.read<String>('contextualIdentity'),
|
contextualIdentity: row.read<String>('contextualIdentity'),
|
||||||
assignedSite: row.readNullable<String>('assigned_site'),
|
assignedSite: row.readNullable<String>('assigned_site'),
|
||||||
@@ -2465,14 +2554,46 @@ class DefinitionsDrift extends i8.ModularAccessor {
|
|||||||
).map((i0.QueryRow row) => row.readNullable<String>('contextual_identity'));
|
).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,
|
attachedDatabase,
|
||||||
).resultSet<i3.TabFts>('tab_fts');
|
).resultSet<i3.TabFts>('tab_fts');
|
||||||
i3.Container get container => i8.ReadDatabaseContainer(
|
i3.Container get container => i9.ReadDatabaseContainer(
|
||||||
attachedDatabase,
|
attachedDatabase,
|
||||||
).resultSet<i3.Container>('container');
|
).resultSet<i3.Container>('container');
|
||||||
i3.Tab get tab =>
|
i3.Tab get tab =>
|
||||||
i8.ReadDatabaseContainer(attachedDatabase).resultSet<i3.Tab>('tab');
|
i9.ReadDatabaseContainer(attachedDatabase).resultSet<i3.Tab>('tab');
|
||||||
}
|
}
|
||||||
|
|
||||||
class TabTreesResult {
|
class TabTreesResult {
|
||||||
@@ -2493,3 +2614,12 @@ class UnorderedTabDescendantsResult {
|
|||||||
final String? parentId;
|
final String? parentId;
|
||||||
UnorderedTabDescendantsResult({required this.id, this.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/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
import 'package:fast_equatable/fast_equatable.dart';
|
import 'package:fast_equatable/fast_equatable.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_mode.dart';
|
||||||
|
|
||||||
class TabQueryResult with FastEquatable {
|
class TabQueryResult with FastEquatable {
|
||||||
final String id;
|
final String id;
|
||||||
final String? containerId;
|
final String? containerId;
|
||||||
final bool? isPrivate;
|
final TabModeDbValue tabMode;
|
||||||
|
|
||||||
final String? title;
|
final String? title;
|
||||||
final Uri? cleanUrl;
|
final Uri? cleanUrl;
|
||||||
@@ -36,7 +37,7 @@ class TabQueryResult with FastEquatable {
|
|||||||
TabQueryResult({
|
TabQueryResult({
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.containerId,
|
required this.containerId,
|
||||||
required this.isPrivate,
|
required this.tabMode,
|
||||||
required this.title,
|
required this.title,
|
||||||
required this.url,
|
required this.url,
|
||||||
required this.cleanUrl,
|
required this.cleanUrl,
|
||||||
@@ -49,7 +50,7 @@ class TabQueryResult with FastEquatable {
|
|||||||
List<Object?> get hashParameters => [
|
List<Object?> get hashParameters => [
|
||||||
id,
|
id,
|
||||||
containerId,
|
containerId,
|
||||||
isPrivate,
|
tabMode,
|
||||||
title,
|
title,
|
||||||
cleanUrl,
|
cleanUrl,
|
||||||
url,
|
url,
|
||||||
|
|||||||
@@ -155,6 +155,25 @@ Stream<List<SiteAssignment>> watchAllAssignedSites(Ref ref) {
|
|||||||
return db.containerDao.allAssignedSites().watch();
|
return db.containerDao.allAssignedSites().watch();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Watches distinct (isolationContextId, containerId) pairs for isolated tabs
|
||||||
|
/// assigned to containers. Used by ProxySettingsReplication to manage proxy
|
||||||
|
/// aliases for isolated contexts.
|
||||||
|
///
|
||||||
|
/// Returns a map from isolation context ID to the set of container IDs it
|
||||||
|
/// appears in. An isolation context needs a proxy alias if ANY of its
|
||||||
|
/// associated containers has useProxy enabled.
|
||||||
|
@Riverpod(keepAlive: true)
|
||||||
|
Stream<Map<String, Set<String>>> watchIsolatedContextContainerMap(Ref ref) {
|
||||||
|
final db = ref.watch(tabDatabaseProvider);
|
||||||
|
return db.tabDao.isolatedContextContainerPairs().watch().map((pairs) {
|
||||||
|
final map = <String, Set<String>>{};
|
||||||
|
for (final p in pairs) {
|
||||||
|
map.putIfAbsent(p.isolationContextId!, () => {}).add(p.containerId!);
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@Riverpod()
|
@Riverpod()
|
||||||
Stream<bool> watchIsCurrentSiteAssignedToContainer(Ref ref) {
|
Stream<bool> watchIsCurrentSiteAssignedToContainer(Ref ref) {
|
||||||
final currentUri = ref.watch(
|
final currentUri = ref.watch(
|
||||||
|
|||||||
@@ -871,6 +871,72 @@ final class WatchAllAssignedSitesProvider
|
|||||||
String _$watchAllAssignedSitesHash() =>
|
String _$watchAllAssignedSitesHash() =>
|
||||||
r'5f658b5733ee20192eb86d3aeb79aa9678dafba8';
|
r'5f658b5733ee20192eb86d3aeb79aa9678dafba8';
|
||||||
|
|
||||||
|
/// Watches distinct (isolationContextId, containerId) pairs for isolated tabs
|
||||||
|
/// assigned to containers. Used by ProxySettingsReplication to manage proxy
|
||||||
|
/// aliases for isolated contexts.
|
||||||
|
///
|
||||||
|
/// Returns a map from isolation context ID to the set of container IDs it
|
||||||
|
/// appears in. An isolation context needs a proxy alias if ANY of its
|
||||||
|
/// associated containers has useProxy enabled.
|
||||||
|
|
||||||
|
@ProviderFor(watchIsolatedContextContainerMap)
|
||||||
|
final watchIsolatedContextContainerMapProvider =
|
||||||
|
WatchIsolatedContextContainerMapProvider._();
|
||||||
|
|
||||||
|
/// Watches distinct (isolationContextId, containerId) pairs for isolated tabs
|
||||||
|
/// assigned to containers. Used by ProxySettingsReplication to manage proxy
|
||||||
|
/// aliases for isolated contexts.
|
||||||
|
///
|
||||||
|
/// Returns a map from isolation context ID to the set of container IDs it
|
||||||
|
/// appears in. An isolation context needs a proxy alias if ANY of its
|
||||||
|
/// associated containers has useProxy enabled.
|
||||||
|
|
||||||
|
final class WatchIsolatedContextContainerMapProvider
|
||||||
|
extends
|
||||||
|
$FunctionalProvider<
|
||||||
|
AsyncValue<Map<String, Set<String>>>,
|
||||||
|
Map<String, Set<String>>,
|
||||||
|
Stream<Map<String, Set<String>>>
|
||||||
|
>
|
||||||
|
with
|
||||||
|
$FutureModifier<Map<String, Set<String>>>,
|
||||||
|
$StreamProvider<Map<String, Set<String>>> {
|
||||||
|
/// Watches distinct (isolationContextId, containerId) pairs for isolated tabs
|
||||||
|
/// assigned to containers. Used by ProxySettingsReplication to manage proxy
|
||||||
|
/// aliases for isolated contexts.
|
||||||
|
///
|
||||||
|
/// Returns a map from isolation context ID to the set of container IDs it
|
||||||
|
/// appears in. An isolation context needs a proxy alias if ANY of its
|
||||||
|
/// associated containers has useProxy enabled.
|
||||||
|
WatchIsolatedContextContainerMapProvider._()
|
||||||
|
: super(
|
||||||
|
from: null,
|
||||||
|
argument: null,
|
||||||
|
retry: null,
|
||||||
|
name: r'watchIsolatedContextContainerMapProvider',
|
||||||
|
isAutoDispose: false,
|
||||||
|
dependencies: null,
|
||||||
|
$allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String debugGetCreateSourceHash() => _$watchIsolatedContextContainerMapHash();
|
||||||
|
|
||||||
|
@$internal
|
||||||
|
@override
|
||||||
|
$StreamProviderElement<Map<String, Set<String>>> $createElement(
|
||||||
|
$ProviderPointer pointer,
|
||||||
|
) => $StreamProviderElement(pointer);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Stream<Map<String, Set<String>>> create(Ref ref) {
|
||||||
|
return watchIsolatedContextContainerMap(ref);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$watchIsolatedContextContainerMapHash() =>
|
||||||
|
r'6275a6c508777c306e2ababca83e2792d6d5efe1';
|
||||||
|
|
||||||
@ProviderFor(watchIsCurrentSiteAssignedToContainer)
|
@ProviderFor(watchIsCurrentSiteAssignedToContainer)
|
||||||
final watchIsCurrentSiteAssignedToContainerProvider =
|
final watchIsCurrentSiteAssignedToContainerProvider =
|
||||||
WatchIsCurrentSiteAssignedToContainerProvider._();
|
WatchIsCurrentSiteAssignedToContainerProvider._();
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import 'package:weblibre/features/geckoview/domain/providers/selected_tab.dart';
|
|||||||
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
|
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
|
||||||
import 'package:weblibre/features/geckoview/domain/repositories/tab.dart';
|
import 'package:weblibre/features/geckoview/domain/repositories/tab.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/data/database/definitions.drift.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/container_data.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/data/providers.dart';
|
import 'package:weblibre/features/geckoview/features/tabs/data/providers.dart';
|
||||||
|
|
||||||
@@ -36,6 +37,16 @@ class TabDataRepository extends _$TabDataRepository {
|
|||||||
bool closeOldTab = true,
|
bool closeOldTab = true,
|
||||||
}) async {
|
}) async {
|
||||||
final selectedTabId = ref.read(selectedTabProvider);
|
final selectedTabId = ref.read(selectedTabProvider);
|
||||||
|
final tabState = ref.read(tabStatesProvider)[tabId];
|
||||||
|
|
||||||
|
// Isolated tabs: always do DB-only assignment (never recreate/recontext)
|
||||||
|
if (tabState != null && tabState.tabMode is IsolatedTabMode) {
|
||||||
|
await ref
|
||||||
|
.read(tabDatabaseProvider)
|
||||||
|
.tabDao
|
||||||
|
.assignContainer(tabId, containerId: targetContainer.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
final currentContainerData = await getTabContainerData(tabId);
|
final currentContainerData = await getTabContainerData(tabId);
|
||||||
|
|
||||||
@@ -46,7 +57,6 @@ class TabDataRepository extends _$TabDataRepository {
|
|||||||
.tabDao
|
.tabDao
|
||||||
.assignContainer(tabId, containerId: targetContainer.id);
|
.assignContainer(tabId, containerId: targetContainer.id);
|
||||||
} else {
|
} else {
|
||||||
final tabState = ref.read(tabStatesProvider)[tabId];
|
|
||||||
if (tabState != null) {
|
if (tabState != null) {
|
||||||
if (closeOldTab) {
|
if (closeOldTab) {
|
||||||
await ref.read(tabRepositoryProvider.notifier).closeTab(tabId);
|
await ref.read(tabRepositoryProvider.notifier).closeTab(tabId);
|
||||||
@@ -56,7 +66,7 @@ class TabDataRepository extends _$TabDataRepository {
|
|||||||
.read(tabRepositoryProvider.notifier)
|
.read(tabRepositoryProvider.notifier)
|
||||||
.addTab(
|
.addTab(
|
||||||
url: tabState.url,
|
url: tabState.url,
|
||||||
private: tabState.isPrivate,
|
tabMode: tabState.tabMode,
|
||||||
containerSelection: TabContainerSelection.specific(
|
containerSelection: TabContainerSelection.specific(
|
||||||
targetContainer,
|
targetContainer,
|
||||||
),
|
),
|
||||||
@@ -69,6 +79,16 @@ class TabDataRepository extends _$TabDataRepository {
|
|||||||
|
|
||||||
Future<void> unassignContainer(String tabId) async {
|
Future<void> unassignContainer(String tabId) async {
|
||||||
final selectedTabId = ref.read(selectedTabProvider);
|
final selectedTabId = ref.read(selectedTabProvider);
|
||||||
|
final tabState = ref.read(tabStatesProvider)[tabId];
|
||||||
|
|
||||||
|
// Isolated tabs: always do DB-only unassignment (never recreate/recontext)
|
||||||
|
if (tabState != null && tabState.tabMode is IsolatedTabMode) {
|
||||||
|
await ref
|
||||||
|
.read(tabDatabaseProvider)
|
||||||
|
.tabDao
|
||||||
|
.assignContainer(tabId, containerId: null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
final currentContainerData = await getTabContainerData(tabId);
|
final currentContainerData = await getTabContainerData(tabId);
|
||||||
|
|
||||||
@@ -78,7 +98,6 @@ class TabDataRepository extends _$TabDataRepository {
|
|||||||
.tabDao
|
.tabDao
|
||||||
.assignContainer(tabId, containerId: null);
|
.assignContainer(tabId, containerId: null);
|
||||||
} else {
|
} else {
|
||||||
final tabState = ref.read(tabStatesProvider)[tabId];
|
|
||||||
if (tabState != null) {
|
if (tabState != null) {
|
||||||
await ref.read(tabRepositoryProvider.notifier).closeTab(tabId);
|
await ref.read(tabRepositoryProvider.notifier).closeTab(tabId);
|
||||||
|
|
||||||
@@ -86,7 +105,7 @@ class TabDataRepository extends _$TabDataRepository {
|
|||||||
.read(tabRepositoryProvider.notifier)
|
.read(tabRepositoryProvider.notifier)
|
||||||
.addTab(
|
.addTab(
|
||||||
url: tabState.url,
|
url: tabState.url,
|
||||||
private: tabState.isPrivate,
|
tabMode: tabState.tabMode,
|
||||||
containerSelection: const TabContainerSelection.unassigned(),
|
containerSelection: const TabContainerSelection.unassigned(),
|
||||||
// parentId defaults to null - breaks parent chain when removing contextual identity
|
// parentId defaults to null - breaks parent chain when removing contextual identity
|
||||||
selectTab: selectedTabId == tabState.id,
|
selectTab: selectedTabId == tabState.id,
|
||||||
@@ -105,6 +124,7 @@ class TabDataRepository extends _$TabDataRepository {
|
|||||||
Future<int> closeAllTabs({
|
Future<int> closeAllTabs({
|
||||||
bool includeRegular = true,
|
bool includeRegular = true,
|
||||||
bool includePrivate = true,
|
bool includePrivate = true,
|
||||||
|
bool includeIsolated = true,
|
||||||
}) async {
|
}) async {
|
||||||
final tabIds = await ref
|
final tabIds = await ref
|
||||||
.read(tabDatabaseProvider)
|
.read(tabDatabaseProvider)
|
||||||
@@ -112,6 +132,7 @@ class TabDataRepository extends _$TabDataRepository {
|
|||||||
.getAllTabIds(
|
.getAllTabIds(
|
||||||
includeRegular: includeRegular,
|
includeRegular: includeRegular,
|
||||||
includePrivate: includePrivate,
|
includePrivate: includePrivate,
|
||||||
|
includeIsolated: includeIsolated,
|
||||||
)
|
)
|
||||||
.get();
|
.get();
|
||||||
|
|
||||||
@@ -126,6 +147,7 @@ class TabDataRepository extends _$TabDataRepository {
|
|||||||
String? containerId, {
|
String? containerId, {
|
||||||
bool includeRegular = true,
|
bool includeRegular = true,
|
||||||
bool includePrivate = true,
|
bool includePrivate = true,
|
||||||
|
bool includeIsolated = true,
|
||||||
}) async {
|
}) async {
|
||||||
final tabIds = await ref
|
final tabIds = await ref
|
||||||
.read(tabDatabaseProvider)
|
.read(tabDatabaseProvider)
|
||||||
@@ -134,6 +156,7 @@ class TabDataRepository extends _$TabDataRepository {
|
|||||||
containerId,
|
containerId,
|
||||||
includeRegular: includeRegular,
|
includeRegular: includeRegular,
|
||||||
includePrivate: includePrivate,
|
includePrivate: includePrivate,
|
||||||
|
includeIsolated: includeIsolated,
|
||||||
)
|
)
|
||||||
.get();
|
.get();
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ final class TabDataRepositoryProvider
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$tabDataRepositoryHash() => r'c1bdfbb576379a7d2ac7b2fe310dbec090e7fd4d';
|
String _$tabDataRepositoryHash() => r'41ca88333684db71f809e35874b0e5bdddde0f60';
|
||||||
|
|
||||||
abstract class _$TabDataRepository extends $Notifier<void> {
|
abstract class _$TabDataRepository extends $Notifier<void> {
|
||||||
void build();
|
void build();
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import 'package:weblibre/core/providers/defaults.dart';
|
|||||||
import 'package:weblibre/core/providers/router.dart';
|
import 'package:weblibre/core/providers/router.dart';
|
||||||
import 'package:weblibre/features/geckoview/domain/entities/tab_container_selection.dart';
|
import 'package:weblibre/features/geckoview/domain/entities/tab_container_selection.dart';
|
||||||
import 'package:weblibre/features/geckoview/domain/repositories/tab.dart';
|
import 'package:weblibre/features/geckoview/domain/repositories/tab.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_mode.dart';
|
||||||
import 'package:weblibre/features/onboarding/presentation/pages/abstract/i_form_page.dart';
|
import 'package:weblibre/features/onboarding/presentation/pages/abstract/i_form_page.dart';
|
||||||
import 'package:weblibre/features/onboarding/presentation/pages/ai_configuration.dart';
|
import 'package:weblibre/features/onboarding/presentation/pages/ai_configuration.dart';
|
||||||
import 'package:weblibre/features/onboarding/presentation/pages/default_search.dart';
|
import 'package:weblibre/features/onboarding/presentation/pages/default_search.dart';
|
||||||
@@ -147,7 +148,7 @@ class OnboardingScreen extends HookConsumerWidget {
|
|||||||
.read(tabRepositoryProvider.notifier)
|
.read(tabRepositoryProvider.notifier)
|
||||||
.addTab(
|
.addTab(
|
||||||
url: ref.read(docsUriProvider),
|
url: ref.read(docsUriProvider),
|
||||||
private: false,
|
tabMode: TabMode.regular,
|
||||||
containerSelection:
|
containerSelection:
|
||||||
const TabContainerSelection.unassigned(),
|
const TabContainerSelection.unassigned(),
|
||||||
selectTab: true,
|
selectTab: true,
|
||||||
|
|||||||
@@ -129,17 +129,27 @@ class _NewTabDefaultSection extends HookConsumerWidget {
|
|||||||
Center(
|
Center(
|
||||||
child: SegmentedButton(
|
child: SegmentedButton(
|
||||||
showSelectedIcon: false,
|
showSelectedIcon: false,
|
||||||
segments: const [
|
segments: [
|
||||||
ButtonSegment(
|
const ButtonSegment(
|
||||||
value: TabType.regular,
|
value: TabType.regular,
|
||||||
label: Text('Regular'),
|
label: Text('Regular'),
|
||||||
icon: Icon(MdiIcons.tab),
|
icon: Icon(MdiIcons.tab),
|
||||||
),
|
),
|
||||||
ButtonSegment(
|
const ButtonSegment(
|
||||||
value: TabType.private,
|
value: TabType.private,
|
||||||
label: Text('Private'),
|
label: Text('Private'),
|
||||||
icon: Icon(WebLibreIcons.privateTab),
|
icon: Icon(WebLibreIcons.privateTab),
|
||||||
),
|
),
|
||||||
|
ButtonSegment(
|
||||||
|
value: TabType.isolated,
|
||||||
|
label: const Text('Isolated'),
|
||||||
|
icon: Icon(
|
||||||
|
MdiIcons.shieldLock,
|
||||||
|
color: defaultCreateTabType == TabType.isolated
|
||||||
|
? null
|
||||||
|
: appColors.isolatedTabTeal,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
selected: {defaultCreateTabType},
|
selected: {defaultCreateTabType},
|
||||||
onSelectionChanged: (value) async {
|
onSelectionChanged: (value) async {
|
||||||
@@ -156,6 +166,9 @@ class _NewTabDefaultSection extends HookConsumerWidget {
|
|||||||
selectedBackgroundColor: appColors.privateSelectionOverlay,
|
selectedBackgroundColor: appColors.privateSelectionOverlay,
|
||||||
),
|
),
|
||||||
TabType.child => null,
|
TabType.child => null,
|
||||||
|
TabType.isolated => SegmentedButton.styleFrom(
|
||||||
|
selectedBackgroundColor: appColors.isolatedSelectionOverlay,
|
||||||
|
),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
|
|||||||
import 'package:weblibre/core/logger.dart';
|
import 'package:weblibre/core/logger.dart';
|
||||||
import 'package:weblibre/features/geckoview/domain/entities/tab_container_selection.dart';
|
import 'package:weblibre/features/geckoview/domain/entities/tab_container_selection.dart';
|
||||||
import 'package:weblibre/features/geckoview/domain/repositories/tab.dart';
|
import 'package:weblibre/features/geckoview/domain/repositories/tab.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_mode.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/container.dart';
|
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/container.dart';
|
||||||
import 'package:weblibre/features/sync/domain/entities/sync_repository_state.dart';
|
import 'package:weblibre/features/sync/domain/entities/sync_repository_state.dart';
|
||||||
import 'package:weblibre/features/sync/domain/entities/synced_tab_item.dart';
|
import 'package:weblibre/features/sync/domain/entities/synced_tab_item.dart';
|
||||||
@@ -281,7 +282,7 @@ class SyncRepository extends _$SyncRepository {
|
|||||||
.addTab(
|
.addTab(
|
||||||
url: uri,
|
url: uri,
|
||||||
selectTab: true,
|
selectTab: true,
|
||||||
private: false,
|
tabMode: TabMode.regular,
|
||||||
containerSelection: assignedContainer == null
|
containerSelection: assignedContainer == null
|
||||||
? const TabContainerSelection.unassigned()
|
? const TabContainerSelection.unassigned()
|
||||||
: TabContainerSelection.specific(assignedContainer),
|
: TabContainerSelection.specific(assignedContainer),
|
||||||
|
|||||||
@@ -336,7 +336,7 @@ final class SyncRepositoryProvider
|
|||||||
SyncRepository create() => SyncRepository();
|
SyncRepository create() => SyncRepository();
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$syncRepositoryHash() => r'5bfe6deacfea9b14981a0b9769857f23b7dd478c';
|
String _$syncRepositoryHash() => r'c2765421a6cea9620e752aa9cbb37f79baf5b1c3';
|
||||||
|
|
||||||
abstract class _$SyncRepository extends $AsyncNotifier<SyncRepositoryState> {
|
abstract class _$SyncRepository extends $AsyncNotifier<SyncRepositoryState> {
|
||||||
FutureOr<SyncRepositoryState> build();
|
FutureOr<SyncRepositoryState> build();
|
||||||
|
|||||||
@@ -2,9 +2,10 @@
|
|||||||
import 'package:drift/internal/versioned_schema.dart' as i0;
|
import 'package:drift/internal/versioned_schema.dart' as i0;
|
||||||
import 'package:drift/drift.dart' as i1;
|
import 'package:drift/drift.dart' as i1;
|
||||||
import 'dart:typed_data' as i2;
|
import 'dart:typed_data' as i2;
|
||||||
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 Schema2 extends i0.VersionedSchema {
|
final class Schema2 extends i0.VersionedSchema {
|
||||||
Schema2({required super.database}) : super(version: 2);
|
Schema2({required super.database}) : super(version: 2);
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -803,6 +803,7 @@ const _$TabTypeEnumMap = {
|
|||||||
TabType.regular: 'regular',
|
TabType.regular: 'regular',
|
||||||
TabType.private: 'private',
|
TabType.private: 'private',
|
||||||
TabType.child: 'child',
|
TabType.child: 'child',
|
||||||
|
TabType.isolated: 'isolated',
|
||||||
};
|
};
|
||||||
|
|
||||||
const _$TabIntentOpenSettingEnumMap = {
|
const _$TabIntentOpenSettingEnumMap = {
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import 'package:weblibre/core/providers/format.dart';
|
|||||||
import 'package:weblibre/core/routing/routes.dart';
|
import 'package:weblibre/core/routing/routes.dart';
|
||||||
import 'package:weblibre/features/geckoview/domain/entities/tab_container_selection.dart';
|
import 'package:weblibre/features/geckoview/domain/entities/tab_container_selection.dart';
|
||||||
import 'package:weblibre/features/geckoview/domain/repositories/tab.dart';
|
import 'package:weblibre/features/geckoview/domain/repositories/tab.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_mode.dart';
|
||||||
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
||||||
import 'package:weblibre/features/web_feed/data/models/feed_link.dart';
|
import 'package:weblibre/features/web_feed/data/models/feed_link.dart';
|
||||||
import 'package:weblibre/features/web_feed/domain/providers.dart';
|
import 'package:weblibre/features/web_feed/domain/providers.dart';
|
||||||
@@ -195,17 +196,17 @@ class FeedArticleScreen extends HookConsumerWidget {
|
|||||||
if (articleLink != null)
|
if (articleLink != null)
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
final isPrivate =
|
final tabMode = TabMode.fromTabType(
|
||||||
ref
|
ref
|
||||||
.read(generalSettingsWithDefaultsProvider)
|
.read(generalSettingsWithDefaultsProvider)
|
||||||
.defaultCreateTabType ==
|
.defaultCreateTabType,
|
||||||
TabType.private;
|
);
|
||||||
|
|
||||||
await ref
|
await ref
|
||||||
.read(tabRepositoryProvider.notifier)
|
.read(tabRepositoryProvider.notifier)
|
||||||
.addTab(
|
.addTab(
|
||||||
url: articleLink.uri,
|
url: articleLink.uri,
|
||||||
private: isPrivate,
|
tabMode: tabMode,
|
||||||
containerSelection:
|
containerSelection:
|
||||||
const TabContainerSelection.unassigned(),
|
const TabContainerSelection.unassigned(),
|
||||||
selectTab: true,
|
selectTab: true,
|
||||||
@@ -245,17 +246,17 @@ class FeedArticleScreen extends HookConsumerWidget {
|
|||||||
onTapLink: (text, href, title) async {
|
onTapLink: (text, href, title) async {
|
||||||
if (href.mapNotNull(Uri.tryParse)
|
if (href.mapNotNull(Uri.tryParse)
|
||||||
case final Uri url) {
|
case final Uri url) {
|
||||||
final isPrivate =
|
final tabMode = TabMode.fromTabType(
|
||||||
ref
|
ref
|
||||||
.read(generalSettingsWithDefaultsProvider)
|
.read(generalSettingsWithDefaultsProvider)
|
||||||
.defaultCreateTabType ==
|
.defaultCreateTabType,
|
||||||
TabType.private;
|
);
|
||||||
|
|
||||||
await ref
|
await ref
|
||||||
.read(tabRepositoryProvider.notifier)
|
.read(tabRepositoryProvider.notifier)
|
||||||
.addTab(
|
.addTab(
|
||||||
url: url,
|
url: url,
|
||||||
private: isPrivate,
|
tabMode: tabMode,
|
||||||
selectTab: true,
|
selectTab: true,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import 'package:weblibre/extensions/ref_cache.dart';
|
|||||||
import 'package:weblibre/extensions/uri.dart';
|
import 'package:weblibre/extensions/uri.dart';
|
||||||
import 'package:weblibre/features/geckoview/domain/entities/states/tab.dart';
|
import 'package:weblibre/features/geckoview/domain/entities/states/tab.dart';
|
||||||
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
|
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_mode.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart';
|
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart';
|
||||||
import 'package:weblibre/features/tor/domain/services/tor_proxy.dart';
|
import 'package:weblibre/features/tor/domain/services/tor_proxy.dart';
|
||||||
import 'package:weblibre/features/user/data/models/tor_settings.dart';
|
import 'package:weblibre/features/user/data/models/tor_settings.dart';
|
||||||
@@ -88,9 +89,10 @@ Future<WebPageInfo> pageInfo(
|
|||||||
final torSettings = ref.read(torSettingsWithDefaultsProvider);
|
final torSettings = ref.read(torSettingsWithDefaultsProvider);
|
||||||
|
|
||||||
if (containerData?.metadata.useProxy == true ||
|
if (containerData?.metadata.useProxy == true ||
|
||||||
(tabState.isPrivate == false &&
|
(tabState.tabMode is! PrivateTabMode &&
|
||||||
torSettings.proxyRegularTabsMode == TorRegularTabProxyMode.all) ||
|
torSettings.proxyRegularTabsMode == TorRegularTabProxyMode.all) ||
|
||||||
(tabState.isPrivate == true && torSettings.proxyPrivateTabsTor)) {
|
(tabState.tabMode is PrivateTabMode &&
|
||||||
|
torSettings.proxyPrivateTabsTor)) {
|
||||||
proxyPort = await ref.read(
|
proxyPort = await ref.read(
|
||||||
torProxyServiceProvider.selectAsync((value) => value.socksPort),
|
torProxyServiceProvider.selectAsync((value) => value.socksPort),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ final class PageInfoProvider
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$pageInfoHash() => r'19116a520df214240254f95989818c1ac3bde6f6';
|
String _$pageInfoHash() => r'c8b7a44b675742436999941b526e9b268ebd139e';
|
||||||
|
|
||||||
final class PageInfoFamily extends $Family
|
final class PageInfoFamily extends $Family
|
||||||
with
|
with
|
||||||
|
|||||||
@@ -236,6 +236,37 @@ void showTabUndoClose(
|
|||||||
ScaffoldMessenger.of(context).showSnackBar(snackBar);
|
ScaffoldMessenger.of(context).showSnackBar(snackBar);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Shows a confirmation dialog before closing isolated tabs whose data
|
||||||
|
/// will be permanently cleared. Returns `true` if the user confirms.
|
||||||
|
Future<bool> confirmIsolatedTabClose(
|
||||||
|
BuildContext context, {
|
||||||
|
int groupCount = 1,
|
||||||
|
}) async {
|
||||||
|
final message = groupCount == 1
|
||||||
|
? 'This will permanently clear all browsing data for this isolated session.'
|
||||||
|
: 'This will permanently clear browsing data for $groupCount isolated sessions.';
|
||||||
|
|
||||||
|
final result = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: const Text('Close isolated tabs?'),
|
||||||
|
content: Text(message),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(false),
|
||||||
|
child: const Text('Cancel'),
|
||||||
|
),
|
||||||
|
FilledButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(true),
|
||||||
|
child: const Text('Close'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return result ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
void showDismissOverrideMessage(
|
void showDismissOverrideMessage(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
VoidCallback onDismiss, {
|
VoidCallback onDismiss, {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// dart format width=80
|
// dart format width=80
|
||||||
// GENERATED CODE, DO NOT EDIT BY HAND.
|
// GENERATED BY drift_dev, DO NOT MODIFY.
|
||||||
// ignore_for_file: type=lint
|
// ignore_for_file: type=lint,unused_import
|
||||||
|
//
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:drift/internal/migrations.dart';
|
import 'package:drift/internal/migrations.dart';
|
||||||
import 'schema_v1.dart' as v1;
|
import 'schema_v1.dart' as v1;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// dart format width=80
|
// dart format width=80
|
||||||
// GENERATED CODE, DO NOT EDIT BY HAND.
|
// GENERATED BY drift_dev, DO NOT MODIFY.
|
||||||
// ignore_for_file: type=lint
|
// ignore_for_file: type=lint,unused_import
|
||||||
|
//
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
|
|
||||||
class Bang extends Table with TableInfo<Bang, BangData> {
|
class Bang extends Table with TableInfo<Bang, BangData> {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// dart format width=80
|
// dart format width=80
|
||||||
// GENERATED CODE, DO NOT EDIT BY HAND.
|
// GENERATED BY drift_dev, DO NOT MODIFY.
|
||||||
// ignore_for_file: type=lint
|
// ignore_for_file: type=lint,unused_import
|
||||||
|
//
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
|
|
||||||
class Bang extends Table with TableInfo<Bang, BangData> {
|
class Bang extends Table with TableInfo<Bang, BangData> {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// dart format width=80
|
// dart format width=80
|
||||||
// GENERATED CODE, DO NOT EDIT BY HAND.
|
// GENERATED BY drift_dev, DO NOT MODIFY.
|
||||||
// ignore_for_file: type=lint
|
// ignore_for_file: type=lint,unused_import
|
||||||
|
//
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
|
|
||||||
class Bang extends Table with TableInfo<Bang, BangData> {
|
class Bang extends Table with TableInfo<Bang, BangData> {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// dart format width=80
|
// dart format width=80
|
||||||
// GENERATED CODE, DO NOT EDIT BY HAND.
|
// GENERATED BY drift_dev, DO NOT MODIFY.
|
||||||
// ignore_for_file: type=lint
|
// ignore_for_file: type=lint,unused_import
|
||||||
|
//
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
|
|
||||||
class Bang extends Table with TableInfo<Bang, BangData> {
|
class Bang extends Table with TableInfo<Bang, BangData> {
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
// dart format width=80
|
// dart format width=80
|
||||||
// GENERATED CODE, DO NOT EDIT BY HAND.
|
// GENERATED BY drift_dev, DO NOT MODIFY.
|
||||||
// ignore_for_file: type=lint
|
// ignore_for_file: type=lint,unused_import
|
||||||
|
//
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:drift/internal/migrations.dart';
|
import 'package:drift/internal/migrations.dart';
|
||||||
import 'schema_v2.dart' as v2;
|
import 'schema_v2.dart' as v2;
|
||||||
import 'schema_v3.dart' as v3;
|
import 'schema_v3.dart' as v3;
|
||||||
import 'schema_v4.dart' as v4;
|
import 'schema_v4.dart' as v4;
|
||||||
import 'schema_v5.dart' as v5;
|
import 'schema_v5.dart' as v5;
|
||||||
|
import 'schema_v6.dart' as v6;
|
||||||
|
|
||||||
class GeneratedHelper implements SchemaInstantiationHelper {
|
class GeneratedHelper implements SchemaInstantiationHelper {
|
||||||
@override
|
@override
|
||||||
@@ -20,10 +22,12 @@ class GeneratedHelper implements SchemaInstantiationHelper {
|
|||||||
return v4.DatabaseAtV4(db);
|
return v4.DatabaseAtV4(db);
|
||||||
case 5:
|
case 5:
|
||||||
return v5.DatabaseAtV5(db);
|
return v5.DatabaseAtV5(db);
|
||||||
|
case 6:
|
||||||
|
return v6.DatabaseAtV6(db);
|
||||||
default:
|
default:
|
||||||
throw MissingSchemaException(version, versions);
|
throw MissingSchemaException(version, versions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static const versions = const [2, 3, 4, 5];
|
static const versions = const [2, 3, 4, 5, 6];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// dart format width=80
|
// dart format width=80
|
||||||
// GENERATED CODE, DO NOT EDIT BY HAND.
|
// GENERATED BY drift_dev, DO NOT MODIFY.
|
||||||
// ignore_for_file: type=lint
|
// ignore_for_file: type=lint,unused_import
|
||||||
|
//
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
|
|
||||||
class Container extends Table with TableInfo<Container, ContainerData> {
|
class Container extends Table with TableInfo<Container, ContainerData> {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// dart format width=80
|
// dart format width=80
|
||||||
// GENERATED CODE, DO NOT EDIT BY HAND.
|
// GENERATED BY drift_dev, DO NOT MODIFY.
|
||||||
// ignore_for_file: type=lint
|
// ignore_for_file: type=lint,unused_import
|
||||||
|
//
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
|
|
||||||
class Container extends Table with TableInfo<Container, ContainerData> {
|
class Container extends Table with TableInfo<Container, ContainerData> {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// dart format width=80
|
// dart format width=80
|
||||||
// GENERATED CODE, DO NOT EDIT BY HAND.
|
// GENERATED BY drift_dev, DO NOT MODIFY.
|
||||||
// ignore_for_file: type=lint
|
// ignore_for_file: type=lint,unused_import
|
||||||
|
//
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
|
|
||||||
class Container extends Table with TableInfo<Container, ContainerData> {
|
class Container extends Table with TableInfo<Container, ContainerData> {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// dart format width=80
|
// dart format width=80
|
||||||
// GENERATED CODE, DO NOT EDIT BY HAND.
|
// GENERATED BY drift_dev, DO NOT MODIFY.
|
||||||
// ignore_for_file: type=lint
|
// ignore_for_file: type=lint,unused_import
|
||||||
|
//
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
|
|
||||||
class Container extends Table with TableInfo<Container, ContainerData> {
|
class Container extends Table with TableInfo<Container, ContainerData> {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,7 @@
|
|||||||
// dart format width=80
|
// dart format width=80
|
||||||
// GENERATED CODE, DO NOT EDIT BY HAND.
|
// GENERATED BY drift_dev, DO NOT MODIFY.
|
||||||
// ignore_for_file: type=lint
|
// ignore_for_file: type=lint,unused_import
|
||||||
|
//
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:drift/internal/migrations.dart';
|
import 'package:drift/internal/migrations.dart';
|
||||||
import 'schema_v1.dart' as v1;
|
import 'schema_v1.dart' as v1;
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
// dart format width=80
|
// dart format width=80
|
||||||
import 'dart:typed_data' as i2;
|
import 'dart:typed_data' as i2;
|
||||||
// GENERATED CODE, DO NOT EDIT BY HAND.
|
// GENERATED BY drift_dev, DO NOT MODIFY.
|
||||||
// ignore_for_file: type=lint
|
// ignore_for_file: type=lint,unused_import
|
||||||
|
//
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
|
|
||||||
class Setting extends Table with TableInfo<Setting, SettingData> {
|
class Setting extends Table with TableInfo<Setting, SettingData> {
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
// dart format width=80
|
// dart format width=80
|
||||||
import 'dart:typed_data' as i2;
|
import 'dart:typed_data' as i2;
|
||||||
// GENERATED CODE, DO NOT EDIT BY HAND.
|
// GENERATED BY drift_dev, DO NOT MODIFY.
|
||||||
// ignore_for_file: type=lint
|
// ignore_for_file: type=lint,unused_import
|
||||||
|
//
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
|
|
||||||
class Setting extends Table with TableInfo<Setting, SettingData> {
|
class Setting extends Table with TableInfo<Setting, SettingData> {
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_mode.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('TabMode value semantics', () {
|
||||||
|
test('isolated modes with same context are equal and hash equally', () {
|
||||||
|
final first = TabMode.isolated('iso1_same');
|
||||||
|
final second = TabMode.isolated('iso1_same');
|
||||||
|
|
||||||
|
expect(first, equals(second));
|
||||||
|
expect(first.hashCode, equals(second.hashCode));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('isolated modes with different contexts are not equal', () {
|
||||||
|
final first = TabMode.isolated('iso1_a');
|
||||||
|
final second = TabMode.isolated('iso1_b');
|
||||||
|
|
||||||
|
expect(first, isNot(equals(second)));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -93,8 +93,38 @@ export class Store {
|
|||||||
return this.siteAssignments.has(uri.origin)
|
return this.siteAssignments.has(uri.origin)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the effective proxy relation for a context ID, mirroring the
|
||||||
|
* fallback logic in getProxiesForContainer: explicit relation first,
|
||||||
|
* then 'general' for non-private contexts, then empty.
|
||||||
|
*/
|
||||||
|
private getEffectiveRelation(contextId: string): string[] {
|
||||||
|
return this.relations[contextId]
|
||||||
|
?? ((contextId !== 'private') ? this.relations['general'] : undefined)
|
||||||
|
?? [];
|
||||||
|
}
|
||||||
|
|
||||||
isSiteOriginInSameContext(uri: URL, contextId: string): boolean {
|
isSiteOriginInSameContext(uri: URL, contextId: string): boolean {
|
||||||
return this.siteAssignments.get(uri.origin) === contextId;
|
const assignedContextId = this.siteAssignments.get(uri.origin);
|
||||||
|
if (assignedContextId === undefined) return false;
|
||||||
|
if (assignedContextId === contextId) return true;
|
||||||
|
|
||||||
|
// Context equivalence: compare effective proxy relations (including
|
||||||
|
// fallback to 'general') so that isolated tabs in non-proxied containers
|
||||||
|
// or containers relying on the general relation are treated as compatible.
|
||||||
|
const assignedRelation = this.getEffectiveRelation(assignedContextId);
|
||||||
|
const currentRelation = this.getEffectiveRelation(contextId);
|
||||||
|
|
||||||
|
// Only treat as equivalent if both have actual proxy relations —
|
||||||
|
// empty relations mean no proxy, and different non-proxied contexts
|
||||||
|
// should not be considered equivalent.
|
||||||
|
if (assignedRelation.length > 0 &&
|
||||||
|
assignedRelation.length === currentRelation.length &&
|
||||||
|
assignedRelation.every((id, i) => id === currentRelation[i])) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
getAllProxies(): ProxySettings[] {
|
getAllProxies(): ProxySettings[] {
|
||||||
|
|||||||
+27
@@ -164,4 +164,31 @@ describe('Store', () => {
|
|||||||
expect(relations.container2).to.be.deep.equal(['proxy2'])
|
expect(relations.container2).to.be.deep.equal(['proxy2'])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('isSiteOriginInSameContext', function () {
|
||||||
|
it('should allow proxy-equivalent isolated context', async () => {
|
||||||
|
store.setSiteAssignments(new Map([['https://example.com/page', 'container_ctx']]))
|
||||||
|
await store.setContainerProxyRelation('container_ctx', 'proxy_ctx')
|
||||||
|
await store.setContainerProxyRelation('iso1_ctx', 'proxy_ctx')
|
||||||
|
|
||||||
|
const result = store.isSiteOriginInSameContext(new URL('https://example.com/other'), 'iso1_ctx')
|
||||||
|
expect(result).to.be.equal(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should allow exact context match', async () => {
|
||||||
|
store.setSiteAssignments(new Map([['https://exact.example/path', 'iso1_exact']]))
|
||||||
|
|
||||||
|
const result = store.isSiteOriginInSameContext(new URL('https://exact.example/another'), 'iso1_exact')
|
||||||
|
expect(result).to.be.equal(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should block when contexts are not equivalent', async () => {
|
||||||
|
store.setSiteAssignments(new Map([['https://blocked.example/path', 'container_blocked']]))
|
||||||
|
await store.setContainerProxyRelation('container_blocked', 'proxy_one')
|
||||||
|
await store.setContainerProxyRelation('iso1_blocked', 'proxy_two')
|
||||||
|
|
||||||
|
const result = store.isSiteOriginInSameContext(new URL('https://blocked.example/another'), 'iso1_blocked')
|
||||||
|
expect(result).to.be.equal(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user