add tab view filter and sort options
This commit is contained in:
@@ -0,0 +1,467 @@
|
|||||||
|
{
|
||||||
|
"_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_pinned",
|
||||||
|
"getter_name": "isPinned",
|
||||||
|
"moor_type": "bool",
|
||||||
|
"nullable": false,
|
||||||
|
"customConstraints": "NOT NULL DEFAULT 0",
|
||||||
|
"default_dart": "const CustomExpression('0')",
|
||||||
|
"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_pinned\" INTEGER NOT NULL DEFAULT 0, \"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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
/*
|
||||||
|
* 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/// Reusable sort directions for list items that share common sortable fields
|
||||||
|
/// (title, URL, date).
|
||||||
|
enum SortField { titleAsc, titleDesc, urlAsc, urlDesc, dateAsc, dateDesc }
|
||||||
+24
-17
@@ -17,18 +17,22 @@
|
|||||||
* You should have received a copy of the GNU Affero General Public License
|
* 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/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
import 'package:weblibre/core/sort_field.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/bookmarks/domain/entities/bookmark_item.dart';
|
import 'package:weblibre/features/geckoview/features/bookmarks/domain/entities/bookmark_item.dart';
|
||||||
|
|
||||||
enum BookmarkSortType {
|
enum BookmarkSortType {
|
||||||
manual('Default'),
|
manual('Default', null),
|
||||||
titleAsc('Title A-Z'),
|
titleAsc('Title A-Z', SortField.titleAsc),
|
||||||
titleDesc('Title Z-A'),
|
titleDesc('Title Z-A', SortField.titleDesc),
|
||||||
urlAsc('URL A-Z'),
|
urlAsc('URL A-Z', SortField.urlAsc),
|
||||||
dateAddedDesc('Newest First');
|
urlDesc('URL Z-A', SortField.urlDesc),
|
||||||
|
dateAddedDesc('Newest First', SortField.dateDesc),
|
||||||
|
dateAddedAsc('Oldest First', SortField.dateAsc);
|
||||||
|
|
||||||
final String label;
|
final String label;
|
||||||
|
final SortField? sortField;
|
||||||
|
|
||||||
const BookmarkSortType(this.label);
|
const BookmarkSortType(this.label, this.sortField);
|
||||||
}
|
}
|
||||||
|
|
||||||
int compareBookmarkItems(
|
int compareBookmarkItems(
|
||||||
@@ -36,21 +40,24 @@ int compareBookmarkItems(
|
|||||||
BookmarkItem b,
|
BookmarkItem b,
|
||||||
BookmarkSortType sort,
|
BookmarkSortType sort,
|
||||||
) {
|
) {
|
||||||
return switch (sort) {
|
final sortField = sort.sortField;
|
||||||
BookmarkSortType.manual => 0,
|
if (sortField == null) {
|
||||||
BookmarkSortType.titleAsc => a.title.toLowerCase().compareTo(
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return switch (sortField) {
|
||||||
|
SortField.titleAsc => a.title.toLowerCase().compareTo(
|
||||||
b.title.toLowerCase(),
|
b.title.toLowerCase(),
|
||||||
),
|
),
|
||||||
BookmarkSortType.titleDesc => b.title.toLowerCase().compareTo(
|
SortField.titleDesc => b.title.toLowerCase().compareTo(
|
||||||
a.title.toLowerCase(),
|
a.title.toLowerCase(),
|
||||||
),
|
),
|
||||||
BookmarkSortType.urlAsc => _compareByUrl(a, b),
|
SortField.urlAsc => _urlKey(a).compareTo(_urlKey(b)),
|
||||||
BookmarkSortType.dateAddedDesc => b.dateAdded.compareTo(a.dateAdded),
|
SortField.urlDesc => _urlKey(b).compareTo(_urlKey(a)),
|
||||||
|
SortField.dateAsc => a.dateAdded.compareTo(b.dateAdded),
|
||||||
|
SortField.dateDesc => b.dateAdded.compareTo(a.dateAdded),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
int _compareByUrl(BookmarkItem a, BookmarkItem b) {
|
String _urlKey(BookmarkItem item) =>
|
||||||
final aUrl = a is BookmarkEntry ? a.url.toString() : a.title.toLowerCase();
|
item is BookmarkEntry ? item.url.toString() : item.title.toLowerCase();
|
||||||
final bUrl = b is BookmarkEntry ? b.url.toString() : b.title.toLowerCase();
|
|
||||||
return aUrl.compareTo(bUrl);
|
|
||||||
}
|
|
||||||
|
|||||||
+143
@@ -0,0 +1,143 @@
|
|||||||
|
/*
|
||||||
|
* 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:copy_with_extension/copy_with_extension.dart';
|
||||||
|
import 'package:fast_equatable/fast_equatable.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
import 'package:weblibre/core/sort_field.dart';
|
||||||
|
import 'package:weblibre/data/database/converters/date_time_range.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_mode.dart';
|
||||||
|
|
||||||
|
part 'tab_view_filter_options.g.dart';
|
||||||
|
|
||||||
|
enum TabTypeFilter {
|
||||||
|
all('All Tabs'),
|
||||||
|
regularOnly('Regular'),
|
||||||
|
privateOnly('Private'),
|
||||||
|
isolatedOnly('Isolated');
|
||||||
|
|
||||||
|
final String label;
|
||||||
|
|
||||||
|
const TabTypeFilter(this.label);
|
||||||
|
|
||||||
|
bool matches(TabMode? tabMode) => switch (this) {
|
||||||
|
all => true,
|
||||||
|
regularOnly => tabMode is RegularTabMode,
|
||||||
|
privateOnly => tabMode is PrivateTabMode,
|
||||||
|
isolatedOnly => tabMode is IsolatedTabMode,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
enum TabSortType {
|
||||||
|
manual('Default', null),
|
||||||
|
titleAsc('Title A-Z', SortField.titleAsc),
|
||||||
|
titleDesc('Title Z-A', SortField.titleDesc),
|
||||||
|
urlAsc('URL A-Z', SortField.urlAsc),
|
||||||
|
urlDesc('URL Z-A', SortField.urlDesc),
|
||||||
|
newestFirst('Newest First', SortField.dateDesc),
|
||||||
|
oldestFirst('Oldest First', SortField.dateAsc);
|
||||||
|
|
||||||
|
final String label;
|
||||||
|
final SortField? sortField;
|
||||||
|
|
||||||
|
const TabSortType(this.label, this.sortField);
|
||||||
|
}
|
||||||
|
|
||||||
|
enum TabQuickInterval {
|
||||||
|
last1h('Last Hour', Duration(hours: 1)),
|
||||||
|
last3h('Last 3 Hours', Duration(hours: 3)),
|
||||||
|
last8h('Last 8 Hours', Duration(hours: 8)),
|
||||||
|
last1d('Last Day', Duration(days: 1)),
|
||||||
|
last3d('Last 3 Days', Duration(days: 3)),
|
||||||
|
last1w('Last Week', Duration(days: 7)),
|
||||||
|
last1m('Last Month', Duration(days: 30));
|
||||||
|
|
||||||
|
final String label;
|
||||||
|
final Duration duration;
|
||||||
|
|
||||||
|
const TabQuickInterval(this.label, this.duration);
|
||||||
|
|
||||||
|
DateTimeRange<DateTime> toDateRange() {
|
||||||
|
final now = DateTime.now();
|
||||||
|
return DateTimeRange(start: now.subtract(duration), end: now);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
@CopyWith()
|
||||||
|
class TabViewFilterOptions with FastEquatable {
|
||||||
|
final TabTypeFilter tabTypeFilter;
|
||||||
|
final TabSortType sortType;
|
||||||
|
final bool sortPinnedFirst;
|
||||||
|
@DateTimeRangeConverter()
|
||||||
|
final DateTimeRange<DateTime>? dateRange;
|
||||||
|
final TabQuickInterval? quickInterval;
|
||||||
|
|
||||||
|
TabViewFilterOptions({
|
||||||
|
required this.tabTypeFilter,
|
||||||
|
required this.sortType,
|
||||||
|
required this.sortPinnedFirst,
|
||||||
|
required this.dateRange,
|
||||||
|
required this.quickInterval,
|
||||||
|
});
|
||||||
|
|
||||||
|
TabViewFilterOptions.withDefaults()
|
||||||
|
: this(
|
||||||
|
tabTypeFilter: TabTypeFilter.all,
|
||||||
|
sortType: TabSortType.manual,
|
||||||
|
sortPinnedFirst: true,
|
||||||
|
dateRange: null,
|
||||||
|
quickInterval: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
bool get hasActiveFilter =>
|
||||||
|
tabTypeFilter != TabTypeFilter.all ||
|
||||||
|
sortType != TabSortType.manual ||
|
||||||
|
dateRange != null ||
|
||||||
|
quickInterval != null;
|
||||||
|
|
||||||
|
DateTimeRange<DateTime>? get effectiveDateRange =>
|
||||||
|
quickInterval?.toDateRange() ?? dateRange;
|
||||||
|
|
||||||
|
bool matchesTab(TabMode? tabMode, DateTime? timestamp) {
|
||||||
|
if (!tabTypeFilter.matches(tabMode)) return false;
|
||||||
|
final range = effectiveDateRange;
|
||||||
|
if (range != null &&
|
||||||
|
timestamp != null &&
|
||||||
|
(timestamp.isBefore(range.start) || timestamp.isAfter(range.end))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get hashParameters => [
|
||||||
|
tabTypeFilter,
|
||||||
|
sortType,
|
||||||
|
sortPinnedFirst,
|
||||||
|
dateRange,
|
||||||
|
quickInterval,
|
||||||
|
];
|
||||||
|
|
||||||
|
factory TabViewFilterOptions.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$TabViewFilterOptionsFromJson(json);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => _$TabViewFilterOptionsToJson(this);
|
||||||
|
}
|
||||||
+169
@@ -0,0 +1,169 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'tab_view_filter_options.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// CopyWithGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
abstract class _$TabViewFilterOptionsCWProxy {
|
||||||
|
TabViewFilterOptions tabTypeFilter(TabTypeFilter tabTypeFilter);
|
||||||
|
|
||||||
|
TabViewFilterOptions sortType(TabSortType sortType);
|
||||||
|
|
||||||
|
TabViewFilterOptions sortPinnedFirst(bool sortPinnedFirst);
|
||||||
|
|
||||||
|
TabViewFilterOptions dateRange(DateTimeRange<DateTime>? dateRange);
|
||||||
|
|
||||||
|
TabViewFilterOptions quickInterval(TabQuickInterval? quickInterval);
|
||||||
|
|
||||||
|
/// Creates a new instance with the provided field values.
|
||||||
|
/// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `TabViewFilterOptions(...).copyWith.fieldName(value)`.
|
||||||
|
///
|
||||||
|
/// Example:
|
||||||
|
/// ```dart
|
||||||
|
/// TabViewFilterOptions(...).copyWith(id: 12, name: "My name")
|
||||||
|
/// ```
|
||||||
|
TabViewFilterOptions call({
|
||||||
|
TabTypeFilter tabTypeFilter,
|
||||||
|
TabSortType sortType,
|
||||||
|
bool sortPinnedFirst,
|
||||||
|
DateTimeRange<DateTime>? dateRange,
|
||||||
|
TabQuickInterval? quickInterval,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Callable proxy for `copyWith` functionality.
|
||||||
|
/// Use as `instanceOfTabViewFilterOptions.copyWith(...)` or call `instanceOfTabViewFilterOptions.copyWith.fieldName(value)` for a single field.
|
||||||
|
class _$TabViewFilterOptionsCWProxyImpl
|
||||||
|
implements _$TabViewFilterOptionsCWProxy {
|
||||||
|
const _$TabViewFilterOptionsCWProxyImpl(this._value);
|
||||||
|
|
||||||
|
final TabViewFilterOptions _value;
|
||||||
|
|
||||||
|
@override
|
||||||
|
TabViewFilterOptions tabTypeFilter(TabTypeFilter tabTypeFilter) =>
|
||||||
|
call(tabTypeFilter: tabTypeFilter);
|
||||||
|
|
||||||
|
@override
|
||||||
|
TabViewFilterOptions sortType(TabSortType sortType) =>
|
||||||
|
call(sortType: sortType);
|
||||||
|
|
||||||
|
@override
|
||||||
|
TabViewFilterOptions sortPinnedFirst(bool sortPinnedFirst) =>
|
||||||
|
call(sortPinnedFirst: sortPinnedFirst);
|
||||||
|
|
||||||
|
@override
|
||||||
|
TabViewFilterOptions dateRange(DateTimeRange<DateTime>? dateRange) =>
|
||||||
|
call(dateRange: dateRange);
|
||||||
|
|
||||||
|
@override
|
||||||
|
TabViewFilterOptions quickInterval(TabQuickInterval? quickInterval) =>
|
||||||
|
call(quickInterval: quickInterval);
|
||||||
|
|
||||||
|
@override
|
||||||
|
/// Creates a new instance with the provided field values.
|
||||||
|
/// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `TabViewFilterOptions(...).copyWith.fieldName(value)`.
|
||||||
|
///
|
||||||
|
/// Example:
|
||||||
|
/// ```dart
|
||||||
|
/// TabViewFilterOptions(...).copyWith(id: 12, name: "My name")
|
||||||
|
/// ```
|
||||||
|
TabViewFilterOptions call({
|
||||||
|
Object? tabTypeFilter = const $CopyWithPlaceholder(),
|
||||||
|
Object? sortType = const $CopyWithPlaceholder(),
|
||||||
|
Object? sortPinnedFirst = const $CopyWithPlaceholder(),
|
||||||
|
Object? dateRange = const $CopyWithPlaceholder(),
|
||||||
|
Object? quickInterval = const $CopyWithPlaceholder(),
|
||||||
|
}) {
|
||||||
|
return TabViewFilterOptions(
|
||||||
|
tabTypeFilter:
|
||||||
|
tabTypeFilter == const $CopyWithPlaceholder() || tabTypeFilter == null
|
||||||
|
? _value.tabTypeFilter
|
||||||
|
// ignore: cast_nullable_to_non_nullable
|
||||||
|
: tabTypeFilter as TabTypeFilter,
|
||||||
|
sortType: sortType == const $CopyWithPlaceholder() || sortType == null
|
||||||
|
? _value.sortType
|
||||||
|
// ignore: cast_nullable_to_non_nullable
|
||||||
|
: sortType as TabSortType,
|
||||||
|
sortPinnedFirst:
|
||||||
|
sortPinnedFirst == const $CopyWithPlaceholder() ||
|
||||||
|
sortPinnedFirst == null
|
||||||
|
? _value.sortPinnedFirst
|
||||||
|
// ignore: cast_nullable_to_non_nullable
|
||||||
|
: sortPinnedFirst as bool,
|
||||||
|
dateRange: dateRange == const $CopyWithPlaceholder()
|
||||||
|
? _value.dateRange
|
||||||
|
// ignore: cast_nullable_to_non_nullable
|
||||||
|
: dateRange as DateTimeRange<DateTime>?,
|
||||||
|
quickInterval: quickInterval == const $CopyWithPlaceholder()
|
||||||
|
? _value.quickInterval
|
||||||
|
// ignore: cast_nullable_to_non_nullable
|
||||||
|
: quickInterval as TabQuickInterval?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension $TabViewFilterOptionsCopyWith on TabViewFilterOptions {
|
||||||
|
/// Returns a callable class used to build a new instance with modified fields.
|
||||||
|
/// Example: `instanceOfTabViewFilterOptions.copyWith(...)` or `instanceOfTabViewFilterOptions.copyWith.fieldName(...)`.
|
||||||
|
// ignore: library_private_types_in_public_api
|
||||||
|
_$TabViewFilterOptionsCWProxy get copyWith =>
|
||||||
|
_$TabViewFilterOptionsCWProxyImpl(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
TabViewFilterOptions _$TabViewFilterOptionsFromJson(
|
||||||
|
Map<String, dynamic> json,
|
||||||
|
) => TabViewFilterOptions(
|
||||||
|
tabTypeFilter: $enumDecode(_$TabTypeFilterEnumMap, json['tabTypeFilter']),
|
||||||
|
sortType: $enumDecode(_$TabSortTypeEnumMap, json['sortType']),
|
||||||
|
sortPinnedFirst: json['sortPinnedFirst'] as bool,
|
||||||
|
dateRange: const DateTimeRangeConverter().fromJson(
|
||||||
|
json['dateRange'] as Map<String, dynamic>?,
|
||||||
|
),
|
||||||
|
quickInterval: $enumDecodeNullable(
|
||||||
|
_$TabQuickIntervalEnumMap,
|
||||||
|
json['quickInterval'],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$TabViewFilterOptionsToJson(
|
||||||
|
TabViewFilterOptions instance,
|
||||||
|
) => <String, dynamic>{
|
||||||
|
'tabTypeFilter': _$TabTypeFilterEnumMap[instance.tabTypeFilter]!,
|
||||||
|
'sortType': _$TabSortTypeEnumMap[instance.sortType]!,
|
||||||
|
'sortPinnedFirst': instance.sortPinnedFirst,
|
||||||
|
'dateRange': const DateTimeRangeConverter().toJson(instance.dateRange),
|
||||||
|
'quickInterval': _$TabQuickIntervalEnumMap[instance.quickInterval],
|
||||||
|
};
|
||||||
|
|
||||||
|
const _$TabTypeFilterEnumMap = {
|
||||||
|
TabTypeFilter.all: 'all',
|
||||||
|
TabTypeFilter.regularOnly: 'regularOnly',
|
||||||
|
TabTypeFilter.privateOnly: 'privateOnly',
|
||||||
|
TabTypeFilter.isolatedOnly: 'isolatedOnly',
|
||||||
|
};
|
||||||
|
|
||||||
|
const _$TabSortTypeEnumMap = {
|
||||||
|
TabSortType.manual: 'manual',
|
||||||
|
TabSortType.titleAsc: 'titleAsc',
|
||||||
|
TabSortType.titleDesc: 'titleDesc',
|
||||||
|
TabSortType.urlAsc: 'urlAsc',
|
||||||
|
TabSortType.urlDesc: 'urlDesc',
|
||||||
|
TabSortType.newestFirst: 'newestFirst',
|
||||||
|
TabSortType.oldestFirst: 'oldestFirst',
|
||||||
|
};
|
||||||
|
|
||||||
|
const _$TabQuickIntervalEnumMap = {
|
||||||
|
TabQuickInterval.last1h: 'last1h',
|
||||||
|
TabQuickInterval.last3h: 'last3h',
|
||||||
|
TabQuickInterval.last8h: 'last8h',
|
||||||
|
TabQuickInterval.last1d: 'last1d',
|
||||||
|
TabQuickInterval.last3d: 'last3d',
|
||||||
|
TabQuickInterval.last1w: 'last1w',
|
||||||
|
TabQuickInterval.last1m: 'last1m',
|
||||||
|
};
|
||||||
@@ -23,6 +23,7 @@ import 'package:flutter_mozilla_components/flutter_mozilla_components.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:weblibre/core/sort_field.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/bangs/data/models/bang_key.dart';
|
import 'package:weblibre/features/bangs/data/models/bang_key.dart';
|
||||||
import 'package:weblibre/features/bangs/domain/repositories/data.dart';
|
import 'package:weblibre/features/bangs/domain/repositories/data.dart';
|
||||||
@@ -30,6 +31,8 @@ import 'package:weblibre/features/geckoview/domain/entities/states/tab.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/entities/tab_view_filter_options.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/tab_view_controllers.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/search/domain/entities/tab_preview.dart';
|
import 'package:weblibre/features/geckoview/features/search/domain/entities/tab_preview.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/data/entities/container_filter.dart';
|
import 'package:weblibre/features/geckoview/features/tabs/data/entities/container_filter.dart';
|
||||||
@@ -46,6 +49,19 @@ part 'providers.g.dart';
|
|||||||
|
|
||||||
typedef TabStateWirthContainer = (TabState, ContainerData?);
|
typedef TabStateWirthContainer = (TabState, ContainerData?);
|
||||||
|
|
||||||
|
@Riverpod()
|
||||||
|
bool canManualTabReorder(Ref ref) {
|
||||||
|
final filterOptions = ref.watch(tabViewFilterControllerProvider);
|
||||||
|
|
||||||
|
final hasActiveSearch = ref.watch(
|
||||||
|
tabSearchRepositoryProvider(
|
||||||
|
TabSearchPartition.preview,
|
||||||
|
).select((value) => (value.value?.query ?? '').isNotEmpty),
|
||||||
|
);
|
||||||
|
|
||||||
|
return !filterOptions.hasActiveFilter && !hasActiveSearch;
|
||||||
|
}
|
||||||
|
|
||||||
@Riverpod(keepAlive: true)
|
@Riverpod(keepAlive: true)
|
||||||
class SelectedBangTrigger extends _$SelectedBangTrigger {
|
class SelectedBangTrigger extends _$SelectedBangTrigger {
|
||||||
// ignore: document_ignores api decision
|
// ignore: document_ignores api decision
|
||||||
@@ -100,6 +116,9 @@ EquatableValue<List<DefaultTabEntity>> containerTabEntities(
|
|||||||
).select((value) => value.value),
|
).select((value) => value.value),
|
||||||
);
|
);
|
||||||
final tabList = ref.watch(tabListProvider);
|
final tabList = ref.watch(tabListProvider);
|
||||||
|
final orderKeys = ref.watch(
|
||||||
|
watchTabOrderKeysProvider.select((value) => value.value),
|
||||||
|
);
|
||||||
|
|
||||||
final availableTabs =
|
final availableTabs =
|
||||||
containerTabs?.where((tabId) => tabList.value.contains(tabId)).toList() ??
|
containerTabs?.where((tabId) => tabList.value.contains(tabId)).toList() ??
|
||||||
@@ -112,6 +131,7 @@ EquatableValue<List<DefaultTabEntity>> containerTabEntities(
|
|||||||
.map(
|
.map(
|
||||||
(t) => DefaultTabEntity(
|
(t) => DefaultTabEntity(
|
||||||
tabId: t,
|
tabId: t,
|
||||||
|
orderKey: orderKeys?[t] ?? '',
|
||||||
containerId: containerFilter.containerId,
|
containerId: containerFilter.containerId,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -123,8 +143,11 @@ EquatableValue<List<DefaultTabEntity>> containerTabEntities(
|
|||||||
(value) => EquatableValue(
|
(value) => EquatableValue(
|
||||||
value.value?.entries
|
value.value?.entries
|
||||||
.map(
|
.map(
|
||||||
(e) =>
|
(e) => DefaultTabEntity(
|
||||||
DefaultTabEntity(tabId: e.key, containerId: e.value),
|
tabId: e.key,
|
||||||
|
orderKey: orderKeys?[e.key] ?? '',
|
||||||
|
containerId: e.value,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.toList() ??
|
.toList() ??
|
||||||
[],
|
[],
|
||||||
@@ -199,7 +222,13 @@ selectedContainerTabStatesWithContainer(Ref ref) {
|
|||||||
|
|
||||||
final tabStates = ref.watch(tabStatesProvider);
|
final tabStates = ref.watch(tabStatesProvider);
|
||||||
|
|
||||||
return EquatableValue([
|
final pinnedTabIds = ref.watch(
|
||||||
|
watchPinnedTabIdsProvider.select((value) => value.value),
|
||||||
|
);
|
||||||
|
|
||||||
|
final orderKeys = {for (final tab in sortedTabs) tab.tabId: tab.orderKey};
|
||||||
|
|
||||||
|
final items = [
|
||||||
for (final tabEntity in sortedTabs)
|
for (final tabEntity in sortedTabs)
|
||||||
if (tabStates.containsKey(tabEntity.tabId))
|
if (tabStates.containsKey(tabEntity.tabId))
|
||||||
(
|
(
|
||||||
@@ -208,7 +237,22 @@ selectedContainerTabStatesWithContainer(Ref ref) {
|
|||||||
(containerId) => containerData?[containerId],
|
(containerId) => containerData?[containerId],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
]);
|
];
|
||||||
|
|
||||||
|
items.sort((a, b) {
|
||||||
|
final aPinned = pinnedTabIds?.contains(a.$1.id) ?? false;
|
||||||
|
final bPinned = pinnedTabIds?.contains(b.$1.id) ?? false;
|
||||||
|
|
||||||
|
if (aPinned != bPinned) {
|
||||||
|
return aPinned ? -1 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
final aOrderKey = orderKeys[a.$1.id] ?? '';
|
||||||
|
final bOrderKey = orderKeys[b.$1.id] ?? '';
|
||||||
|
return aOrderKey.compareTo(bOrderKey);
|
||||||
|
});
|
||||||
|
|
||||||
|
return EquatableValue(items);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Riverpod()
|
@Riverpod()
|
||||||
@@ -324,6 +368,10 @@ EquatableValue<List<TabEntity>> suggestedTabEntities(
|
|||||||
).select((value) => EquatableValue(value.value)),
|
).select((value) => EquatableValue(value.value)),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final orderKeys = ref.watch(
|
||||||
|
watchTabOrderKeysProvider.select((value) => value.value),
|
||||||
|
);
|
||||||
|
|
||||||
final suggestions = ref.watch(
|
final suggestions = ref.watch(
|
||||||
containerTabSuggestionsProvider(containerId).select(
|
containerTabSuggestionsProvider(containerId).select(
|
||||||
(value) => EquatableValue(
|
(value) => EquatableValue(
|
||||||
@@ -335,6 +383,7 @@ EquatableValue<List<TabEntity>> suggestedTabEntities(
|
|||||||
.map(
|
.map(
|
||||||
(tabId) => DefaultTabEntity(
|
(tabId) => DefaultTabEntity(
|
||||||
tabId: tabId,
|
tabId: tabId,
|
||||||
|
orderKey: orderKeys?[tabId] ?? '',
|
||||||
containerId: containerId,
|
containerId: containerId,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -348,6 +397,103 @@ EquatableValue<List<TabEntity>> suggestedTabEntities(
|
|||||||
return suggestions;
|
return suggestions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<TabEntity> _applyTabFiltersAndSort(
|
||||||
|
List<TabEntity> entities,
|
||||||
|
TabViewFilterOptions filterOptions,
|
||||||
|
Map<String, TabState> tabStates,
|
||||||
|
Set<String> pinnedTabIds,
|
||||||
|
Map<String, DateTime>? tabTimestamps,
|
||||||
|
) {
|
||||||
|
final sortField = filterOptions.sortType.sortField;
|
||||||
|
final filteredRows = <_TabFilterRow>[];
|
||||||
|
var hasPinned = false;
|
||||||
|
|
||||||
|
for (final entity in entities) {
|
||||||
|
final tabState = tabStates[entity.tabId];
|
||||||
|
final timestamp = tabTimestamps?[entity.tabId];
|
||||||
|
|
||||||
|
if (!filterOptions.matchesTab(tabState?.tabMode, timestamp)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
final isPinned = pinnedTabIds.contains(entity.tabId);
|
||||||
|
hasPinned = hasPinned || isPinned;
|
||||||
|
|
||||||
|
filteredRows.add(
|
||||||
|
_TabFilterRow(
|
||||||
|
entity: entity,
|
||||||
|
isPinned: isPinned,
|
||||||
|
titleKey:
|
||||||
|
sortField == SortField.titleAsc || sortField == SortField.titleDesc
|
||||||
|
? (tabState?.titleOrAuthority ?? '').toLowerCase()
|
||||||
|
: null,
|
||||||
|
urlKey: sortField == SortField.urlAsc || sortField == SortField.urlDesc
|
||||||
|
? (tabState?.url.toString() ?? '')
|
||||||
|
: null,
|
||||||
|
dateKey:
|
||||||
|
sortField == SortField.dateAsc || sortField == SortField.dateDesc
|
||||||
|
? (timestamp ?? DateTime(0))
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sortField != null) {
|
||||||
|
filteredRows.sort((a, b) {
|
||||||
|
if (filterOptions.sortPinnedFirst && a.isPinned != b.isPinned) {
|
||||||
|
return b.isPinned ? 1 : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
final cmp = switch (sortField) {
|
||||||
|
SortField.titleAsc => a.titleKey!.compareTo(b.titleKey!),
|
||||||
|
SortField.titleDesc => b.titleKey!.compareTo(a.titleKey!),
|
||||||
|
SortField.urlAsc => a.urlKey!.compareTo(b.urlKey!),
|
||||||
|
SortField.urlDesc => b.urlKey!.compareTo(a.urlKey!),
|
||||||
|
SortField.dateAsc => a.dateKey!.compareTo(b.dateKey!),
|
||||||
|
SortField.dateDesc => b.dateKey!.compareTo(a.dateKey!),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (cmp == 0) return a.entity.orderKey.compareTo(b.entity.orderKey);
|
||||||
|
|
||||||
|
return cmp;
|
||||||
|
});
|
||||||
|
|
||||||
|
return filteredRows.map((row) => row.entity).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasPinned || !filterOptions.sortPinnedFirst) {
|
||||||
|
return filteredRows.map((row) => row.entity).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
final pinned = <TabEntity>[];
|
||||||
|
final unpinned = <TabEntity>[];
|
||||||
|
for (final row in filteredRows) {
|
||||||
|
if (row.isPinned) {
|
||||||
|
pinned.add(row.entity);
|
||||||
|
} else {
|
||||||
|
unpinned.add(row.entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...pinned, ...unpinned];
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TabFilterRow {
|
||||||
|
final TabEntity entity;
|
||||||
|
final bool isPinned;
|
||||||
|
final String? titleKey;
|
||||||
|
final String? urlKey;
|
||||||
|
final DateTime? dateKey;
|
||||||
|
|
||||||
|
const _TabFilterRow({
|
||||||
|
required this.entity,
|
||||||
|
required this.isPinned,
|
||||||
|
required this.titleKey,
|
||||||
|
required this.urlKey,
|
||||||
|
required this.dateKey,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@Riverpod()
|
@Riverpod()
|
||||||
EquatableValue<List<TabEntity>> seamlessFilteredTabEntities(
|
EquatableValue<List<TabEntity>> seamlessFilteredTabEntities(
|
||||||
Ref ref, {
|
Ref ref, {
|
||||||
@@ -355,6 +501,10 @@ EquatableValue<List<TabEntity>> seamlessFilteredTabEntities(
|
|||||||
required ContainerFilter containerFilter,
|
required ContainerFilter containerFilter,
|
||||||
required bool groupTrees,
|
required bool groupTrees,
|
||||||
}) {
|
}) {
|
||||||
|
final orderKeys = ref.watch(
|
||||||
|
watchTabOrderKeysProvider.select((value) => value.value),
|
||||||
|
);
|
||||||
|
|
||||||
final tabSearchResults = ref
|
final tabSearchResults = ref
|
||||||
.watch(
|
.watch(
|
||||||
tabSearchRepositoryProvider(searchPartition).select(
|
tabSearchRepositoryProvider(searchPartition).select(
|
||||||
@@ -364,6 +514,7 @@ EquatableValue<List<TabEntity>> seamlessFilteredTabEntities(
|
|||||||
.map(
|
.map(
|
||||||
(tab) => SearchResultTabEntity(
|
(tab) => SearchResultTabEntity(
|
||||||
tabId: tab.id,
|
tabId: tab.id,
|
||||||
|
orderKey: orderKeys?[tab.id] ?? '',
|
||||||
containerId: tab.containerId,
|
containerId: tab.containerId,
|
||||||
searchQuery: result.query,
|
searchQuery: result.query,
|
||||||
),
|
),
|
||||||
@@ -379,53 +530,98 @@ EquatableValue<List<TabEntity>> seamlessFilteredTabEntities(
|
|||||||
containerTabEntitiesProvider(containerFilter),
|
containerTabEntitiesProvider(containerFilter),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (tabSearchResults == null) {
|
// Tree mode: no filtering/sorting, return as-is
|
||||||
if (groupTrees) {
|
if (groupTrees && tabSearchResults == null) {
|
||||||
final trees = ref.watch(
|
final trees = ref.watch(
|
||||||
watchTabTreesProvider.select(
|
watchTabTreesProvider.select(
|
||||||
(value) => EquatableValue(
|
(value) => EquatableValue(
|
||||||
value.value?.map((tree) {
|
value.value?.map((tree) {
|
||||||
// Find the container ID for the latest tab
|
// Find the container ID for the latest tab
|
||||||
final containerForTab = availableTabs.value
|
final containerForTab = availableTabs.value
|
||||||
.where((t) => t.tabId == tree.latestTabId)
|
.where((t) => t.tabId == tree.latestTabId)
|
||||||
.firstOrNull
|
.firstOrNull
|
||||||
?.containerId;
|
?.containerId;
|
||||||
|
|
||||||
return TabTreeEntity(
|
return TabTreeEntity(
|
||||||
tabId: tree.latestTabId,
|
tabId: tree.latestTabId,
|
||||||
containerId: containerForTab,
|
orderKey: orderKeys?[tree.latestTabId] ?? '',
|
||||||
rootId: tree.rootTabId,
|
containerId: containerForTab,
|
||||||
totalTabs: tree.totalTabs,
|
rootId: tree.rootTabId,
|
||||||
);
|
totalTabs: tree.totalTabs,
|
||||||
}).toList() ??
|
);
|
||||||
[],
|
}).toList() ??
|
||||||
),
|
[],
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return EquatableValue(
|
||||||
|
trees.value
|
||||||
|
.where(
|
||||||
|
(tree) => availableTabs.value.any(
|
||||||
|
(available) => available.tabId == tree.tabId,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final tabStates = ref.watch(tabStatesProvider);
|
||||||
|
|
||||||
|
final filterOptions = ref.watch(tabViewFilterControllerProvider);
|
||||||
|
|
||||||
|
final pinnedTabIds = ref.watch(
|
||||||
|
watchPinnedTabIdsProvider.select(
|
||||||
|
(value) => value.value ?? const <String>{},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Only pull timestamps from DB when date filtering/sorting is active
|
||||||
|
final needsTimestamps =
|
||||||
|
filterOptions.effectiveDateRange != null ||
|
||||||
|
filterOptions.sortType.sortField == SortField.dateAsc ||
|
||||||
|
filterOptions.sortType.sortField == SortField.dateDesc;
|
||||||
|
final tabTimestamps = needsTimestamps
|
||||||
|
? ref.watch(watchTabTimestampsProvider.select((value) => value.value))
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (tabSearchResults == null) {
|
||||||
|
if (filterOptions.hasActiveFilter || pinnedTabIds.isNotEmpty) {
|
||||||
return EquatableValue(
|
return EquatableValue(
|
||||||
trees.value
|
_applyTabFiltersAndSort(
|
||||||
.where(
|
availableTabs.value,
|
||||||
(tree) => availableTabs.value.any(
|
filterOptions,
|
||||||
(available) => available.tabId == tree.tabId,
|
tabStates,
|
||||||
),
|
pinnedTabIds,
|
||||||
)
|
tabTimestamps,
|
||||||
.toList(),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return availableTabs;
|
return availableTabs;
|
||||||
}
|
}
|
||||||
|
|
||||||
return EquatableValue(
|
final searchFiltered = tabSearchResults
|
||||||
tabSearchResults
|
.where(
|
||||||
.where(
|
(tab) => availableTabs.value.any(
|
||||||
(tab) => availableTabs.value.any(
|
(available) => available.tabId == tab.tabId,
|
||||||
(available) => available.tabId == tab.tabId,
|
),
|
||||||
),
|
)
|
||||||
)
|
.toList();
|
||||||
.toList(),
|
|
||||||
);
|
if (filterOptions.hasActiveFilter || pinnedTabIds.isNotEmpty) {
|
||||||
|
return EquatableValue(
|
||||||
|
_applyTabFiltersAndSort(
|
||||||
|
searchFiltered,
|
||||||
|
filterOptions,
|
||||||
|
tabStates,
|
||||||
|
pinnedTabIds,
|
||||||
|
tabTimestamps,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return EquatableValue(searchFiltered);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Riverpod()
|
@Riverpod()
|
||||||
|
|||||||
@@ -9,6 +9,48 @@ part of 'providers.dart';
|
|||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
// ignore_for_file: type=lint, type=warning
|
// ignore_for_file: type=lint, type=warning
|
||||||
|
|
||||||
|
@ProviderFor(canManualTabReorder)
|
||||||
|
final canManualTabReorderProvider = CanManualTabReorderProvider._();
|
||||||
|
|
||||||
|
final class CanManualTabReorderProvider
|
||||||
|
extends $FunctionalProvider<bool, bool, bool>
|
||||||
|
with $Provider<bool> {
|
||||||
|
CanManualTabReorderProvider._()
|
||||||
|
: super(
|
||||||
|
from: null,
|
||||||
|
argument: null,
|
||||||
|
retry: null,
|
||||||
|
name: r'canManualTabReorderProvider',
|
||||||
|
isAutoDispose: true,
|
||||||
|
dependencies: null,
|
||||||
|
$allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String debugGetCreateSourceHash() => _$canManualTabReorderHash();
|
||||||
|
|
||||||
|
@$internal
|
||||||
|
@override
|
||||||
|
$ProviderElement<bool> $createElement($ProviderPointer pointer) =>
|
||||||
|
$ProviderElement(pointer);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool create(Ref ref) {
|
||||||
|
return canManualTabReorder(ref);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// {@macro riverpod.override_with_value}
|
||||||
|
Override overrideWithValue(bool value) {
|
||||||
|
return $ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
providerOverride: $SyncValueProvider<bool>(value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$canManualTabReorderHash() =>
|
||||||
|
r'ba5d961933464b6e005d7945802908a9a4ae034b';
|
||||||
|
|
||||||
@ProviderFor(SelectedBangTrigger)
|
@ProviderFor(SelectedBangTrigger)
|
||||||
final selectedBangTriggerProvider = SelectedBangTriggerFamily._();
|
final selectedBangTriggerProvider = SelectedBangTriggerFamily._();
|
||||||
|
|
||||||
@@ -269,7 +311,7 @@ final class ContainerTabEntitiesProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _$containerTabEntitiesHash() =>
|
String _$containerTabEntitiesHash() =>
|
||||||
r'350b3e2a2672ab0a303344c7d8301af24c991ee5';
|
r'bcd932968bae46fb5e27a60819ea7aad7a8e41e7';
|
||||||
|
|
||||||
final class ContainerTabEntitiesFamily extends $Family
|
final class ContainerTabEntitiesFamily extends $Family
|
||||||
with
|
with
|
||||||
@@ -487,7 +529,7 @@ final class SelectedContainerTabStatesWithContainerProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _$selectedContainerTabStatesWithContainerHash() =>
|
String _$selectedContainerTabStatesWithContainerHash() =>
|
||||||
r'2fd5e12595b3aced1e235c9626073c34655d57fa';
|
r'e2dc85fc72ee29736d8fdaabb1abadd23cc4bac0';
|
||||||
|
|
||||||
@ProviderFor(quickTabSwitcherTabStates)
|
@ProviderFor(quickTabSwitcherTabStates)
|
||||||
final quickTabSwitcherTabStatesProvider = QuickTabSwitcherTabStatesFamily._();
|
final quickTabSwitcherTabStatesProvider = QuickTabSwitcherTabStatesFamily._();
|
||||||
@@ -816,7 +858,7 @@ final class SuggestedTabEntitiesProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _$suggestedTabEntitiesHash() =>
|
String _$suggestedTabEntitiesHash() =>
|
||||||
r'1c3d9a4f85db60301fe5c5b55fb55512d1b59696';
|
r'b140a1d0badad3d976b91ab9154300873a8bd4e9';
|
||||||
|
|
||||||
final class SuggestedTabEntitiesFamily extends $Family
|
final class SuggestedTabEntitiesFamily extends $Family
|
||||||
with $FunctionalFamilyOverride<EquatableValue<List<TabEntity>>, String?> {
|
with $FunctionalFamilyOverride<EquatableValue<List<TabEntity>>, String?> {
|
||||||
@@ -920,7 +962,7 @@ final class SeamlessFilteredTabEntitiesProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _$seamlessFilteredTabEntitiesHash() =>
|
String _$seamlessFilteredTabEntitiesHash() =>
|
||||||
r'b0477aa6297183575264d54a82575dfb01fa4c32';
|
r'bc6833f975e9af3b2117268d7644dc56131c85a3';
|
||||||
|
|
||||||
final class SeamlessFilteredTabEntitiesFamily extends $Family
|
final class SeamlessFilteredTabEntitiesFamily extends $Family
|
||||||
with
|
with
|
||||||
|
|||||||
+42
@@ -22,8 +22,10 @@ import 'dart:convert';
|
|||||||
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:riverpod/experimental/persist.dart';
|
import 'package:riverpod/experimental/persist.dart';
|
||||||
|
import 'package:riverpod_annotation/experimental/json_persist.dart';
|
||||||
import 'package:riverpod_annotation/experimental/persist.dart';
|
import 'package:riverpod_annotation/experimental/persist.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/browser/domain/entities/tab_view_filter_options.dart';
|
||||||
import 'package:weblibre/features/user/data/providers.dart';
|
import 'package:weblibre/features/user/data/providers.dart';
|
||||||
|
|
||||||
part 'tab_view_controllers.g.dart';
|
part 'tab_view_controllers.g.dart';
|
||||||
@@ -63,6 +65,46 @@ class TabsViewModeController extends _$TabsViewModeController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Riverpod(keepAlive: true)
|
||||||
|
@JsonPersist()
|
||||||
|
class TabViewFilterController extends _$TabViewFilterController {
|
||||||
|
void setTabTypeFilter(TabTypeFilter filter) {
|
||||||
|
state = state.copyWith.tabTypeFilter(filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setSortType(TabSortType sort) {
|
||||||
|
state = state.copyWith.sortType(sort);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setSortPinnedFirst(bool value) {
|
||||||
|
state = state.copyWith(sortPinnedFirst: value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setDateRange(DateTimeRange<DateTime>? range) {
|
||||||
|
// ignore: avoid_redundant_argument_values
|
||||||
|
state = state.copyWith(dateRange: range, quickInterval: null);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setQuickInterval(TabQuickInterval? interval) {
|
||||||
|
// ignore: avoid_redundant_argument_values
|
||||||
|
state = state.copyWith(quickInterval: interval, dateRange: null);
|
||||||
|
}
|
||||||
|
|
||||||
|
void reset() {
|
||||||
|
state = TabViewFilterOptions.withDefaults();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
TabViewFilterOptions build() {
|
||||||
|
persist(
|
||||||
|
ref.watch(riverpodDatabaseStorageProvider),
|
||||||
|
key: 'TabViewFilterOptions',
|
||||||
|
);
|
||||||
|
|
||||||
|
return stateOrNull ?? TabViewFilterOptions.withDefaults();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Riverpod()
|
@Riverpod()
|
||||||
class TabsReorderableController extends _$TabsReorderableController {
|
class TabsReorderableController extends _$TabsReorderableController {
|
||||||
void toggle() {
|
void toggle() {
|
||||||
|
|||||||
+94
@@ -62,6 +62,63 @@ abstract class _$TabsViewModeController extends $Notifier<TabsViewMode> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ProviderFor(TabViewFilterController)
|
||||||
|
@JsonPersist()
|
||||||
|
final tabViewFilterControllerProvider = TabViewFilterControllerProvider._();
|
||||||
|
|
||||||
|
@JsonPersist()
|
||||||
|
final class TabViewFilterControllerProvider
|
||||||
|
extends $NotifierProvider<TabViewFilterController, TabViewFilterOptions> {
|
||||||
|
TabViewFilterControllerProvider._()
|
||||||
|
: super(
|
||||||
|
from: null,
|
||||||
|
argument: null,
|
||||||
|
retry: null,
|
||||||
|
name: r'tabViewFilterControllerProvider',
|
||||||
|
isAutoDispose: false,
|
||||||
|
dependencies: null,
|
||||||
|
$allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String debugGetCreateSourceHash() => _$tabViewFilterControllerHash();
|
||||||
|
|
||||||
|
@$internal
|
||||||
|
@override
|
||||||
|
TabViewFilterController create() => TabViewFilterController();
|
||||||
|
|
||||||
|
/// {@macro riverpod.override_with_value}
|
||||||
|
Override overrideWithValue(TabViewFilterOptions value) {
|
||||||
|
return $ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
providerOverride: $SyncValueProvider<TabViewFilterOptions>(value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$tabViewFilterControllerHash() =>
|
||||||
|
r'95e8a03d60ebe05e0c5df38f810f951a4fe2ed78';
|
||||||
|
|
||||||
|
@JsonPersist()
|
||||||
|
abstract class _$TabViewFilterControllerBase
|
||||||
|
extends $Notifier<TabViewFilterOptions> {
|
||||||
|
TabViewFilterOptions build();
|
||||||
|
@$mustCallSuper
|
||||||
|
@override
|
||||||
|
void runBuild() {
|
||||||
|
final ref = this.ref as $Ref<TabViewFilterOptions, TabViewFilterOptions>;
|
||||||
|
final element =
|
||||||
|
ref.element
|
||||||
|
as $ClassProviderElement<
|
||||||
|
AnyNotifier<TabViewFilterOptions, TabViewFilterOptions>,
|
||||||
|
TabViewFilterOptions,
|
||||||
|
Object?,
|
||||||
|
Object?
|
||||||
|
>;
|
||||||
|
element.handleCreate(ref, build);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ProviderFor(TabsReorderableController)
|
@ProviderFor(TabsReorderableController)
|
||||||
final tabsReorderableControllerProvider = TabsReorderableControllerProvider._();
|
final tabsReorderableControllerProvider = TabsReorderableControllerProvider._();
|
||||||
|
|
||||||
@@ -114,3 +171,40 @@ abstract class _$TabsReorderableController extends $Notifier<bool> {
|
|||||||
element.handleCreate(ref, build);
|
element.handleCreate(ref, build);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
abstract class _$TabViewFilterController extends _$TabViewFilterControllerBase {
|
||||||
|
/// The default key used by [persist].
|
||||||
|
String get key {
|
||||||
|
const resolvedKey = "TabViewFilterController";
|
||||||
|
return resolvedKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A variant of [persist], for JSON-specific encoding.
|
||||||
|
///
|
||||||
|
/// You can override [key] to customize the key used for storage.
|
||||||
|
PersistResult persist(
|
||||||
|
FutureOr<Storage<String, String>> storage, {
|
||||||
|
String? key,
|
||||||
|
String Function(TabViewFilterOptions state)? encode,
|
||||||
|
TabViewFilterOptions Function(String encoded)? decode,
|
||||||
|
StorageOptions options = const StorageOptions(),
|
||||||
|
}) {
|
||||||
|
return NotifierPersistX(this).persist<String, String>(
|
||||||
|
storage,
|
||||||
|
key: key ?? this.key,
|
||||||
|
encode: encode ?? $jsonCodex.encode,
|
||||||
|
decode:
|
||||||
|
decode ??
|
||||||
|
(encoded) {
|
||||||
|
final e = $jsonCodex.decode(encoded);
|
||||||
|
return TabViewFilterOptions.fromJson(e as Map<String, Object?>);
|
||||||
|
},
|
||||||
|
options: options,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -291,7 +291,6 @@ class BrowserScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
ref.listen(overlayControllerProvider, (previous, next) {
|
ref.listen(overlayControllerProvider, (previous, next) {
|
||||||
if (next != null) {
|
if (next != null) {
|
||||||
overlayController.show();
|
overlayController.show();
|
||||||
@@ -872,7 +871,10 @@ class _Browser extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Dismiss modal routes (e.g. showModalBottomSheet)
|
// Dismiss modal routes (e.g. showModalBottomSheet)
|
||||||
final rootNavigator = Navigator.of(context, rootNavigator: true);
|
final rootNavigator = Navigator.of(
|
||||||
|
context,
|
||||||
|
rootNavigator: true,
|
||||||
|
);
|
||||||
if (rootNavigator.canPop()) {
|
if (rootNavigator.canPop()) {
|
||||||
rootNavigator.pop();
|
rootNavigator.pop();
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
+10
-3
@@ -1368,7 +1368,9 @@ class _ExtensionsCard extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final addonService = ref.watch(addonServiceProvider);
|
final addonService = ref.watch(addonServiceProvider);
|
||||||
final extensionsExpanded = ref.watch(persistedBoolProvider(PersistedBoolKey.extensionsExpanded));
|
final extensionsExpanded = ref.watch(
|
||||||
|
persistedBoolProvider(PersistedBoolKey.extensionsExpanded),
|
||||||
|
);
|
||||||
final pageExtensions = ref.watch(
|
final pageExtensions = ref.watch(
|
||||||
webExtensionsStateProvider(
|
webExtensionsStateProvider(
|
||||||
WebExtensionActionType.page,
|
WebExtensionActionType.page,
|
||||||
@@ -1393,8 +1395,13 @@ class _ExtensionsCard extends HookConsumerWidget {
|
|||||||
leading: const Icon(MdiIcons.puzzle),
|
leading: const Icon(MdiIcons.puzzle),
|
||||||
title: const Text('Extensions'),
|
title: const Text('Extensions'),
|
||||||
initiallyExpanded: extensionsExpanded,
|
initiallyExpanded: extensionsExpanded,
|
||||||
onExpansionChanged: (_) =>
|
onExpansionChanged: (_) => ref
|
||||||
ref.read(persistedBoolProvider(PersistedBoolKey.extensionsExpanded).notifier).toggle(),
|
.read(
|
||||||
|
persistedBoolProvider(
|
||||||
|
PersistedBoolKey.extensionsExpanded,
|
||||||
|
).notifier,
|
||||||
|
)
|
||||||
|
.toggle(),
|
||||||
children: [
|
children: [
|
||||||
// Page extensions
|
// Page extensions
|
||||||
if (pageExtensions.isNotEmpty) ...[
|
if (pageExtensions.isNotEmpty) ...[
|
||||||
|
|||||||
+57
-11
@@ -20,6 +20,7 @@
|
|||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:fast_equatable/fast_equatable.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
@@ -369,15 +370,39 @@ class BrowserTabBar extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
typedef _QuickTabItem = ({
|
class _QuickTabItem with FastEquatable {
|
||||||
Color? color,
|
final Color? color;
|
||||||
String id,
|
final String id;
|
||||||
TabMode tabMode,
|
final TabMode tabMode;
|
||||||
bool isHistory,
|
final bool isHistory;
|
||||||
String title,
|
final bool isPinned;
|
||||||
Uri url,
|
final String title;
|
||||||
TabState? tabState,
|
final Uri url;
|
||||||
});
|
final TabState? tabState;
|
||||||
|
|
||||||
|
_QuickTabItem({
|
||||||
|
required this.color,
|
||||||
|
required this.id,
|
||||||
|
required this.tabMode,
|
||||||
|
required this.isHistory,
|
||||||
|
required this.isPinned,
|
||||||
|
required this.title,
|
||||||
|
required this.url,
|
||||||
|
required this.tabState,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get hashParameters => [
|
||||||
|
color,
|
||||||
|
id,
|
||||||
|
tabMode,
|
||||||
|
isHistory,
|
||||||
|
isPinned,
|
||||||
|
title,
|
||||||
|
url,
|
||||||
|
tabState,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
class ContextualToolbar extends HookConsumerWidget {
|
class ContextualToolbar extends HookConsumerWidget {
|
||||||
const ContextualToolbar({
|
const ContextualToolbar({
|
||||||
@@ -443,6 +468,14 @@ class QuickTabSwitcher extends HookConsumerWidget {
|
|||||||
(s) => s.quickTabSwitcherShowTitles,
|
(s) => s.quickTabSwitcherShowTitles,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
final effectiveMode = ref.watch(
|
||||||
|
generalSettingsWithDefaultsProvider.select(
|
||||||
|
(settings) => settings.effectiveUiQuickTabSwitcherMode(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final pinnedTabIds = ref.watch(
|
||||||
|
watchPinnedTabIdsProvider.select((value) => value.value),
|
||||||
|
);
|
||||||
final tabStates = ref.watch(
|
final tabStates = ref.watch(
|
||||||
quickTabSwitcherTabStatesProvider(quickTabSwitcherMode),
|
quickTabSwitcherTabStatesProvider(quickTabSwitcherMode),
|
||||||
);
|
);
|
||||||
@@ -451,11 +484,12 @@ class QuickTabSwitcher extends HookConsumerWidget {
|
|||||||
.value;
|
.value;
|
||||||
final availableItems = tabStates.value
|
final availableItems = tabStates.value
|
||||||
.map<_QuickTabItem>(
|
.map<_QuickTabItem>(
|
||||||
(state) => (
|
(state) => _QuickTabItem(
|
||||||
id: state.$1.id,
|
id: state.$1.id,
|
||||||
title: state.$1.titleOrAuthority,
|
title: state.$1.titleOrAuthority,
|
||||||
tabMode: state.$1.tabMode,
|
tabMode: state.$1.tabMode,
|
||||||
isHistory: false,
|
isHistory: false,
|
||||||
|
isPinned: pinnedTabIds?.contains(state.$1.id) ?? false,
|
||||||
url: state.$1.url,
|
url: state.$1.url,
|
||||||
color: state.$2?.color,
|
color: state.$2?.color,
|
||||||
tabState: state.$1,
|
tabState: state.$1,
|
||||||
@@ -465,11 +499,12 @@ class QuickTabSwitcher extends HookConsumerWidget {
|
|||||||
(historySuggestions ?? []).map<_QuickTabItem>((state) {
|
(historySuggestions ?? []).map<_QuickTabItem>((state) {
|
||||||
final url = Uri.parse(state.url);
|
final url = Uri.parse(state.url);
|
||||||
|
|
||||||
return (
|
return _QuickTabItem(
|
||||||
id: state.url,
|
id: state.url,
|
||||||
title: state.title ?? url.authority,
|
title: state.title ?? url.authority,
|
||||||
tabMode: TabMode.regular,
|
tabMode: TabMode.regular,
|
||||||
isHistory: true,
|
isHistory: true,
|
||||||
|
isPinned: false,
|
||||||
url: url,
|
url: url,
|
||||||
color: null,
|
color: null,
|
||||||
tabState: null,
|
tabState: null,
|
||||||
@@ -496,6 +531,7 @@ class QuickTabSwitcher extends HookConsumerWidget {
|
|||||||
labelPadding: (item) =>
|
labelPadding: (item) =>
|
||||||
(!showTitles &&
|
(!showTitles &&
|
||||||
!item.isHistory &&
|
!item.isHistory &&
|
||||||
|
!item.isPinned &&
|
||||||
item.tabMode is! PrivateTabMode &&
|
item.tabMode is! PrivateTabMode &&
|
||||||
item.tabMode is! IsolatedTabMode)
|
item.tabMode is! IsolatedTabMode)
|
||||||
? EdgeInsets.zero
|
? EdgeInsets.zero
|
||||||
@@ -527,6 +563,15 @@ class QuickTabSwitcher extends HookConsumerWidget {
|
|||||||
size: 20,
|
size: 20,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (item.isPinned)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 8.0),
|
||||||
|
child: Icon(
|
||||||
|
MdiIcons.pin,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
),
|
||||||
if (item.isHistory)
|
if (item.isHistory)
|
||||||
const Padding(
|
const Padding(
|
||||||
padding: EdgeInsets.only(left: 8.0),
|
padding: EdgeInsets.only(left: 8.0),
|
||||||
@@ -575,6 +620,7 @@ class QuickTabSwitcher extends HookConsumerWidget {
|
|||||||
enableReloadButton: false,
|
enableReloadButton: false,
|
||||||
enableNavigationButtons: false,
|
enableNavigationButtons: false,
|
||||||
enableAddToHomeScreen: false,
|
enableAddToHomeScreen: false,
|
||||||
|
enablePinTab: effectiveMode == QuickTabSwitcherMode.containerTabs,
|
||||||
builder: (context, controller, _) {
|
builder: (context, controller, _) {
|
||||||
return InkWell(
|
return InkWell(
|
||||||
onLongPress: () {
|
onLongPress: () {
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ class TabMenu extends HookConsumerWidget {
|
|||||||
final bool enableShare;
|
final bool enableShare;
|
||||||
final bool enableExport;
|
final bool enableExport;
|
||||||
final bool enableCloseTab;
|
final bool enableCloseTab;
|
||||||
|
final bool enablePinTab;
|
||||||
final bool enableReloadButton;
|
final bool enableReloadButton;
|
||||||
final bool enableNavigationButtons;
|
final bool enableNavigationButtons;
|
||||||
|
|
||||||
@@ -87,6 +88,7 @@ class TabMenu extends HookConsumerWidget {
|
|||||||
this.enableShare = true,
|
this.enableShare = true,
|
||||||
this.enableExport = true,
|
this.enableExport = true,
|
||||||
this.enableCloseTab = true,
|
this.enableCloseTab = true,
|
||||||
|
this.enablePinTab = true,
|
||||||
this.enableReloadButton = true,
|
this.enableReloadButton = true,
|
||||||
this.enableNavigationButtons = true,
|
this.enableNavigationButtons = true,
|
||||||
});
|
});
|
||||||
@@ -579,6 +581,31 @@ class TabMenu extends HookConsumerWidget {
|
|||||||
leadingIcon: const Icon(MdiIcons.fileExport),
|
leadingIcon: const Icon(MdiIcons.fileExport),
|
||||||
child: const Text('Export'),
|
child: const Text('Export'),
|
||||||
),
|
),
|
||||||
|
if (enablePinTab)
|
||||||
|
Consumer(
|
||||||
|
builder: (context, childRef, child) {
|
||||||
|
final isPinned = childRef.watch(
|
||||||
|
watchPinnedTabIdsProvider.select(
|
||||||
|
(v) => v.value?.contains(selectedTabId) ?? false,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return MenuItemButton(
|
||||||
|
closeOnActivate: false,
|
||||||
|
onPressed: () async {
|
||||||
|
await childRef
|
||||||
|
.read(tabDataRepositoryProvider.notifier)
|
||||||
|
.setPinned(selectedTabId, pinned: !isPinned);
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
|
MenuController.maybeOf(context)?.close();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
leadingIcon: Icon(isPinned ? MdiIcons.pinOff : MdiIcons.pin),
|
||||||
|
child: Text(isPinned ? 'Unpin tab' : 'Pin tab'),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
if (enableCloseTab)
|
if (enableCloseTab)
|
||||||
MenuItemButton(
|
MenuItemButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
|
|||||||
+225
@@ -0,0 +1,225 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2024-2026 Fabian Freund.
|
||||||
|
*
|
||||||
|
* This file is part of WebLibre
|
||||||
|
* (see https://weblibre.eu).
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/gestures.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:weblibre/data/models/drag_data.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_menu.dart';
|
||||||
|
import 'package:weblibre/presentation/hooks/menu_controller.dart';
|
||||||
|
|
||||||
|
/// Chrome-like tab context menu + drag coordination.
|
||||||
|
///
|
||||||
|
/// On long press: shows a context menu via [TabMenu].
|
||||||
|
/// If user moves finger: closes menu, starts drag.
|
||||||
|
/// If user releases without moving: menu persists.
|
||||||
|
///
|
||||||
|
/// For non-reorderable mode: wraps with [LongPressDraggable].
|
||||||
|
/// For reorderable mode: uses manual long-press timer (drag handled externally).
|
||||||
|
class TabContextMenuDraggable extends HookConsumerWidget {
|
||||||
|
final String tabId;
|
||||||
|
final TabDragData? data;
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
/// Size for the drag feedback widget.
|
||||||
|
final Size feedbackSize;
|
||||||
|
|
||||||
|
/// Whether drag is handled externally (reorderable mode).
|
||||||
|
/// When true, no [LongPressDraggable] is used; only menu + manual timer.
|
||||||
|
final bool externalDrag;
|
||||||
|
|
||||||
|
const TabContextMenuDraggable({
|
||||||
|
required this.tabId,
|
||||||
|
required this.child,
|
||||||
|
required this.feedbackSize,
|
||||||
|
this.data,
|
||||||
|
this.externalDrag = false,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final menuController = useMenuController();
|
||||||
|
final isDragMoving = useMemoized(() => ValueNotifier(false));
|
||||||
|
final startPosition = useRef(Offset.zero);
|
||||||
|
final isDragging = useRef(false);
|
||||||
|
final longPressTimer = useRef<Timer?>(null);
|
||||||
|
|
||||||
|
// Clean up ValueNotifier
|
||||||
|
useEffect(() => isDragMoving.dispose, [isDragMoving]);
|
||||||
|
|
||||||
|
if (externalDrag) {
|
||||||
|
return _buildReorderableMode(
|
||||||
|
context,
|
||||||
|
menuController: menuController,
|
||||||
|
startPosition: startPosition,
|
||||||
|
longPressTimer: longPressTimer,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return _buildDraggableMode(
|
||||||
|
context,
|
||||||
|
menuController: menuController,
|
||||||
|
isDragMoving: isDragMoving,
|
||||||
|
startPosition: startPosition,
|
||||||
|
isDragging: isDragging,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTabMenu({
|
||||||
|
required MenuController menuController,
|
||||||
|
required Widget Function(BuildContext, MenuController, Widget?) builder,
|
||||||
|
}) {
|
||||||
|
return TabMenu(
|
||||||
|
selectedTabId: tabId,
|
||||||
|
controller: menuController,
|
||||||
|
enableFindInPage: false,
|
||||||
|
enableFetchFeeds: false,
|
||||||
|
enableDesktopMode: false,
|
||||||
|
enableReaderMode: false,
|
||||||
|
enableReloadButton: false,
|
||||||
|
enableNavigationButtons: false,
|
||||||
|
enableAddToHomeScreen: false,
|
||||||
|
enableCloseTab: false,
|
||||||
|
builder: builder,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Non-reorderable mode: Listener + TabMenu + LongPressDraggable
|
||||||
|
Widget _buildDraggableMode(
|
||||||
|
BuildContext context, {
|
||||||
|
required MenuController menuController,
|
||||||
|
required ValueNotifier<bool> isDragMoving,
|
||||||
|
required ObjectRef<Offset> startPosition,
|
||||||
|
required ObjectRef<bool> isDragging,
|
||||||
|
}) {
|
||||||
|
return Listener(
|
||||||
|
onPointerDown: (event) {
|
||||||
|
startPosition.value = event.position;
|
||||||
|
},
|
||||||
|
onPointerMove: (event) {
|
||||||
|
if (isDragging.value &&
|
||||||
|
!isDragMoving.value &&
|
||||||
|
(event.position - startPosition.value).distance > kTouchSlop) {
|
||||||
|
isDragMoving.value = true;
|
||||||
|
if (menuController.isOpen) {
|
||||||
|
menuController.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onPointerUp: (_) {
|
||||||
|
// Reset drag tracking on pointer up (for cases where drag wasn't started)
|
||||||
|
isDragging.value = false;
|
||||||
|
},
|
||||||
|
child: _buildTabMenu(
|
||||||
|
menuController: menuController,
|
||||||
|
builder: (context, controller, _) {
|
||||||
|
return LongPressDraggable<TabDragData>(
|
||||||
|
data: data,
|
||||||
|
onDragStarted: () {
|
||||||
|
isDragging.value = true;
|
||||||
|
isDragMoving.value = false;
|
||||||
|
menuController.open();
|
||||||
|
},
|
||||||
|
onDragEnd: (_) {
|
||||||
|
isDragging.value = false;
|
||||||
|
isDragMoving.value = false;
|
||||||
|
},
|
||||||
|
onDraggableCanceled: (_, _) {
|
||||||
|
isDragging.value = false;
|
||||||
|
isDragMoving.value = false;
|
||||||
|
},
|
||||||
|
feedback: ValueListenableBuilder<bool>(
|
||||||
|
valueListenable: isDragMoving,
|
||||||
|
builder: (_, moving, feedbackChild) {
|
||||||
|
if (!moving) return const SizedBox.shrink();
|
||||||
|
return feedbackChild!;
|
||||||
|
},
|
||||||
|
child: Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: Transform.scale(
|
||||||
|
scale: 1.05,
|
||||||
|
child: SizedBox(
|
||||||
|
height: feedbackSize.height,
|
||||||
|
width: feedbackSize.width,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
childWhenDragging: ValueListenableBuilder<bool>(
|
||||||
|
valueListenable: isDragMoving,
|
||||||
|
builder: (_, moving, _) {
|
||||||
|
if (!moving) return child;
|
||||||
|
return SizedBox(
|
||||||
|
height: feedbackSize.height,
|
||||||
|
width: feedbackSize.width,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reorderable mode: Listener + TabMenu (drag handled externally)
|
||||||
|
Widget _buildReorderableMode(
|
||||||
|
BuildContext context, {
|
||||||
|
required MenuController menuController,
|
||||||
|
required ObjectRef<Offset> startPosition,
|
||||||
|
required ObjectRef<Timer?> longPressTimer,
|
||||||
|
}) {
|
||||||
|
return Listener(
|
||||||
|
onPointerDown: (event) {
|
||||||
|
startPosition.value = event.position;
|
||||||
|
longPressTimer.value?.cancel();
|
||||||
|
longPressTimer.value = Timer(kLongPressTimeout, () {
|
||||||
|
menuController.open();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onPointerMove: (event) {
|
||||||
|
if ((event.position - startPosition.value).distance > kTouchSlop) {
|
||||||
|
// User started moving - cancel menu timer and close menu if open
|
||||||
|
longPressTimer.value?.cancel();
|
||||||
|
longPressTimer.value = null;
|
||||||
|
if (menuController.isOpen) {
|
||||||
|
menuController.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onPointerUp: (_) {
|
||||||
|
longPressTimer.value?.cancel();
|
||||||
|
longPressTimer.value = null;
|
||||||
|
},
|
||||||
|
onPointerCancel: (_) {
|
||||||
|
longPressTimer.value?.cancel();
|
||||||
|
longPressTimer.value = null;
|
||||||
|
},
|
||||||
|
child: _buildTabMenu(
|
||||||
|
menuController: menuController,
|
||||||
|
builder: (context, controller, _) => child,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+56
-32
@@ -37,6 +37,7 @@ import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
|
|||||||
import 'package:weblibre/features/geckoview/features/browser/domain/providers.dart';
|
import 'package:weblibre/features/geckoview/features/browser/domain/providers.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/tab_view_controllers.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/tab_view_controllers.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/draggable_scrollable_header.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/draggable_scrollable_header.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_view/tab_context_menu_draggable.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_view/tab_drop_target.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_view/tab_drop_target.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_view/tab_preview.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_view/tab_preview.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_view/tab_view_header.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_view/tab_view_header.dart';
|
||||||
@@ -141,6 +142,8 @@ class _TabGridView extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final screenWidth = MediaQuery.of(context).size.width;
|
final screenWidth = MediaQuery.of(context).size.width;
|
||||||
|
final canManualReorder = ref.watch(canManualTabReorderProvider);
|
||||||
|
final reorderEnabled = tabsReorderable && canManualReorder;
|
||||||
|
|
||||||
final containerId = ref.watch(selectedContainerProvider);
|
final containerId = ref.watch(selectedContainerProvider);
|
||||||
|
|
||||||
@@ -154,7 +157,9 @@ class _TabGridView extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
final tabSuggestionsEnabled = ref.watch(persistedBoolProvider(PersistedBoolKey.tabSuggestions));
|
final tabSuggestionsEnabled = ref.watch(
|
||||||
|
persistedBoolProvider(PersistedBoolKey.tabSuggestions),
|
||||||
|
);
|
||||||
|
|
||||||
final suggestedTabEntities = tabSuggestionsEnabled
|
final suggestedTabEntities = tabSuggestionsEnabled
|
||||||
? ref.watch(suggestedTabEntitiesProvider(containerId))
|
? ref.watch(suggestedTabEntitiesProvider(containerId))
|
||||||
@@ -164,6 +169,9 @@ class _TabGridView extends HookConsumerWidget {
|
|||||||
filteredTabEntities.value.length +
|
filteredTabEntities.value.length +
|
||||||
//Limit to 3 sugegstions for now
|
//Limit to 3 sugegstions for now
|
||||||
math.min<int>(suggestedTabEntities.value.length, 3);
|
math.min<int>(suggestedTabEntities.value.length, 3);
|
||||||
|
final displayItemCount = reorderEnabled
|
||||||
|
? filteredTabEntities.value.length
|
||||||
|
: itemCount;
|
||||||
|
|
||||||
final activeTab = ref.watch(selectedTabProvider);
|
final activeTab = ref.watch(selectedTabProvider);
|
||||||
|
|
||||||
@@ -239,34 +247,22 @@ class _TabGridView extends HookConsumerWidget {
|
|||||||
fadingSize: 5,
|
fadingSize: 5,
|
||||||
controller: scrollController,
|
controller: scrollController,
|
||||||
builder: (context, controller) {
|
builder: (context, controller) {
|
||||||
return !tabsReorderable
|
return !reorderEnabled
|
||||||
? _TabGrid(
|
? _TabGrid(
|
||||||
key: ValueKey(crossAxisCount),
|
key: ValueKey(crossAxisCount),
|
||||||
crossAxisCount: crossAxisCount,
|
crossAxisCount: crossAxisCount,
|
||||||
itemCount: itemCount,
|
itemCount: displayItemCount,
|
||||||
scrollController: controller,
|
scrollController: controller,
|
||||||
itemBuilder: (widget, _) {
|
itemBuilder: (widget, _) {
|
||||||
if (widget is CustomDraggable) {
|
if (widget is CustomDraggable) {
|
||||||
return LongPressDraggable(
|
if (widget.data case final TabDragData dragData) {
|
||||||
feedback: Material(
|
return TabContextMenuDraggable(
|
||||||
color: Colors
|
tabId: dragData.tabId,
|
||||||
.transparent, // removes white corners when having shadow
|
data: dragData,
|
||||||
child: Transform.scale(
|
feedbackSize: itemSize,
|
||||||
scale: 1.05,
|
child: widget.child,
|
||||||
child: SizedBox(
|
);
|
||||||
height: itemSize.height,
|
}
|
||||||
width: itemSize.width,
|
|
||||||
child: widget.child,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
data: widget.data,
|
|
||||||
childWhenDragging: SizedBox(
|
|
||||||
height: itemSize.height,
|
|
||||||
width: itemSize.width,
|
|
||||||
),
|
|
||||||
child: widget.child,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return widget;
|
return widget;
|
||||||
@@ -280,7 +276,7 @@ class _TabGridView extends HookConsumerWidget {
|
|||||||
//Rebuild when cross axis count changes
|
//Rebuild when cross axis count changes
|
||||||
key: ValueKey(crossAxisCount),
|
key: ValueKey(crossAxisCount),
|
||||||
scrollController: controller,
|
scrollController: controller,
|
||||||
itemCount: itemCount,
|
itemCount: displayItemCount,
|
||||||
onDragStarted: (index) {
|
onDragStarted: (index) {
|
||||||
ref.read(willAcceptDropProvider.notifier).clear();
|
ref.read(willAcceptDropProvider.notifier).clear();
|
||||||
},
|
},
|
||||||
@@ -307,25 +303,35 @@ class _TabGridView extends HookConsumerWidget {
|
|||||||
.read(tabDataRepositoryProvider.notifier)
|
.read(tabDataRepositoryProvider.notifier)
|
||||||
.getTabContainerId(tabId);
|
.getTabContainerId(tabId);
|
||||||
|
|
||||||
|
var targetIndex = newIndex;
|
||||||
|
if (targetIndex > oldIndex) {
|
||||||
|
targetIndex -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
targetIndex = targetIndex.clamp(
|
||||||
|
0,
|
||||||
|
filteredTabEntities.value.length - 1,
|
||||||
|
);
|
||||||
|
|
||||||
final String key;
|
final String key;
|
||||||
if (newIndex <= 0) {
|
if (targetIndex <= 0) {
|
||||||
key = await containerRepository.getLeadingOrderKey(
|
key = await containerRepository.getLeadingOrderKey(
|
||||||
containerId,
|
containerId,
|
||||||
);
|
);
|
||||||
} else if (newIndex >=
|
} else if (targetIndex >=
|
||||||
filteredTabEntities.value.length - 1) {
|
filteredTabEntities.value.length - 1) {
|
||||||
key = await containerRepository.getTrailingOrderKey(
|
key = await containerRepository.getTrailingOrderKey(
|
||||||
containerId,
|
containerId,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
if (newIndex < oldIndex) {
|
if (targetIndex < oldIndex) {
|
||||||
key = (await containerRepository.getOrderKeyAfterTab(
|
key = (await containerRepository.getOrderKeyAfterTab(
|
||||||
filteredTabEntities.value[newIndex - 1].tabId,
|
filteredTabEntities.value[targetIndex - 1].tabId,
|
||||||
containerId,
|
containerId,
|
||||||
))!;
|
))!;
|
||||||
} else {
|
} else {
|
||||||
key = await containerRepository.getOrderKeyBeforeTab(
|
key = await containerRepository.getOrderKeyBeforeTab(
|
||||||
filteredTabEntities.value[newIndex + 1].tabId,
|
filteredTabEntities.value[targetIndex + 1].tabId,
|
||||||
containerId,
|
containerId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -335,12 +341,30 @@ class _TabGridView extends HookConsumerWidget {
|
|||||||
.read(tabDataRepositoryProvider.notifier)
|
.read(tabDataRepositoryProvider.notifier)
|
||||||
.assignOrderKey(tabId, key);
|
.assignOrderKey(tabId, key);
|
||||||
},
|
},
|
||||||
childBuilder: (itemBuilder) {
|
childBuilder: (reorderableItemBuilder) {
|
||||||
return _TabGrid(
|
return _TabGrid(
|
||||||
crossAxisCount: crossAxisCount,
|
crossAxisCount: crossAxisCount,
|
||||||
itemCount: itemCount,
|
itemCount: displayItemCount,
|
||||||
scrollController: controller,
|
scrollController: controller,
|
||||||
itemBuilder: itemBuilder,
|
itemBuilder: (widget, index) {
|
||||||
|
// Wrap with context menu before passing to reorderable
|
||||||
|
Widget wrapped = widget;
|
||||||
|
if (widget is CustomDraggable) {
|
||||||
|
if (widget.data case final TabDragData dragData) {
|
||||||
|
wrapped = CustomDraggable(
|
||||||
|
key: widget.key!,
|
||||||
|
data: widget.data,
|
||||||
|
child: TabContextMenuDraggable(
|
||||||
|
tabId: dragData.tabId,
|
||||||
|
feedbackSize: Size.zero,
|
||||||
|
externalDrag: true,
|
||||||
|
child: widget.child,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return reorderableItemBuilder(wrapped, index);
|
||||||
|
},
|
||||||
suggestedContainerId: containerId,
|
suggestedContainerId: containerId,
|
||||||
filteredTabEntities: filteredTabEntities,
|
filteredTabEntities: filteredTabEntities,
|
||||||
suggestedTabEntities: suggestedTabEntities,
|
suggestedTabEntities: suggestedTabEntities,
|
||||||
|
|||||||
+49
-53
@@ -36,6 +36,7 @@ import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
|
|||||||
import 'package:weblibre/features/geckoview/features/browser/domain/providers.dart';
|
import 'package:weblibre/features/geckoview/features/browser/domain/providers.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/tab_view_controllers.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/tab_view_controllers.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/draggable_scrollable_header.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/draggable_scrollable_header.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_view/tab_context_menu_draggable.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_view/tab_drop_target.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_view/tab_drop_target.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_view/tab_preview.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_view/tab_preview.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_view/tab_view_header.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_view/tab_view_header.dart';
|
||||||
@@ -147,7 +148,7 @@ class _TabListView extends HookConsumerWidget {
|
|||||||
required this.onClose,
|
required this.onClose,
|
||||||
});
|
});
|
||||||
|
|
||||||
static const _itemHeight = 80.0;
|
static const _itemHeight = 86.0;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
@@ -204,6 +205,8 @@ class _TabListView extends HookConsumerWidget {
|
|||||||
|
|
||||||
Widget _buildLocalTabsView(BuildContext context, WidgetRef ref) {
|
Widget _buildLocalTabsView(BuildContext context, WidgetRef ref) {
|
||||||
final containerId = ref.watch(selectedContainerProvider);
|
final containerId = ref.watch(selectedContainerProvider);
|
||||||
|
final canManualReorder = ref.watch(canManualTabReorderProvider);
|
||||||
|
final reorderEnabled = tabsReorderable && canManualReorder;
|
||||||
|
|
||||||
final filteredTabEntities = ref.watch(
|
final filteredTabEntities = ref.watch(
|
||||||
seamlessFilteredTabEntitiesProvider(
|
seamlessFilteredTabEntitiesProvider(
|
||||||
@@ -215,7 +218,9 @@ class _TabListView extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
final tabSuggestionsEnabled = ref.watch(persistedBoolProvider(PersistedBoolKey.tabSuggestions));
|
final tabSuggestionsEnabled = ref.watch(
|
||||||
|
persistedBoolProvider(PersistedBoolKey.tabSuggestions),
|
||||||
|
);
|
||||||
|
|
||||||
final suggestedTabEntities = tabSuggestionsEnabled
|
final suggestedTabEntities = tabSuggestionsEnabled
|
||||||
? ref.watch(suggestedTabEntitiesProvider(containerId))
|
? ref.watch(suggestedTabEntitiesProvider(containerId))
|
||||||
@@ -225,6 +230,9 @@ class _TabListView extends HookConsumerWidget {
|
|||||||
filteredTabEntities.value.length +
|
filteredTabEntities.value.length +
|
||||||
//Limit to 3 sugegstions for now
|
//Limit to 3 sugegstions for now
|
||||||
math.min<int>(suggestedTabEntities.value.length, 3);
|
math.min<int>(suggestedTabEntities.value.length, 3);
|
||||||
|
final displayItemCount = reorderEnabled
|
||||||
|
? filteredTabEntities.value.length
|
||||||
|
: itemCount;
|
||||||
|
|
||||||
final activeTab = ref.watch(selectedTabProvider);
|
final activeTab = ref.watch(selectedTabProvider);
|
||||||
|
|
||||||
@@ -282,11 +290,11 @@ class _TabListView extends HookConsumerWidget {
|
|||||||
fadingSize: 5,
|
fadingSize: 5,
|
||||||
controller: scrollController,
|
controller: scrollController,
|
||||||
builder: (context, controller) {
|
builder: (context, controller) {
|
||||||
return !tabsReorderable
|
return !reorderEnabled
|
||||||
? ListView.builder(
|
? ListView.builder(
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
controller: scrollController,
|
controller: scrollController,
|
||||||
itemCount: itemCount,
|
itemCount: displayItemCount,
|
||||||
itemExtent: _itemHeight,
|
itemExtent: _itemHeight,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final TabEntity entity;
|
final TabEntity entity;
|
||||||
@@ -322,29 +330,23 @@ class _TabListView extends HookConsumerWidget {
|
|||||||
return TabDropTarget(
|
return TabDropTarget(
|
||||||
targetTabId: entity.tabId,
|
targetTabId: entity.tabId,
|
||||||
enabled: suggestedId == null,
|
enabled: suggestedId == null,
|
||||||
child: LongPressDraggable(
|
child: tab.data is TabDragData
|
||||||
feedback: Material(
|
? TabContextMenuDraggable(
|
||||||
color: Colors
|
tabId: (tab.data! as TabDragData).tabId,
|
||||||
.transparent, // removes white corners when having shadow
|
data: tab.data! as TabDragData,
|
||||||
child: Transform.scale(
|
feedbackSize: Size(
|
||||||
scale: 1.05,
|
MediaQuery.of(context).size.width,
|
||||||
child: SizedBox(
|
_itemHeight,
|
||||||
height: _itemHeight,
|
),
|
||||||
width: MediaQuery.of(context).size.width,
|
|
||||||
child: tab.child,
|
child: tab.child,
|
||||||
),
|
)
|
||||||
),
|
: tab.child,
|
||||||
),
|
|
||||||
data: tab.data,
|
|
||||||
childWhenDragging: const SizedBox(height: _itemHeight),
|
|
||||||
child: tab.child,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
: ReorderableListView.builder(
|
: ReorderableListView.builder(
|
||||||
scrollController: controller,
|
scrollController: controller,
|
||||||
itemCount: itemCount,
|
itemCount: displayItemCount,
|
||||||
itemExtent: _itemHeight,
|
itemExtent: _itemHeight,
|
||||||
onReorderStart: (index) {
|
onReorderStart: (index) {
|
||||||
ref.read(willAcceptDropProvider.notifier).clear();
|
ref.read(willAcceptDropProvider.notifier).clear();
|
||||||
@@ -364,25 +366,35 @@ class _TabListView extends HookConsumerWidget {
|
|||||||
.read(tabDataRepositoryProvider.notifier)
|
.read(tabDataRepositoryProvider.notifier)
|
||||||
.getTabContainerId(tabId);
|
.getTabContainerId(tabId);
|
||||||
|
|
||||||
|
var targetIndex = newIndex;
|
||||||
|
if (targetIndex > oldIndex) {
|
||||||
|
targetIndex -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
targetIndex = targetIndex.clamp(
|
||||||
|
0,
|
||||||
|
filteredTabEntities.value.length - 1,
|
||||||
|
);
|
||||||
|
|
||||||
final String key;
|
final String key;
|
||||||
if (newIndex <= 0) {
|
if (targetIndex <= 0) {
|
||||||
key = await containerRepository.getLeadingOrderKey(
|
key = await containerRepository.getLeadingOrderKey(
|
||||||
containerId,
|
containerId,
|
||||||
);
|
);
|
||||||
} else if (newIndex >=
|
} else if (targetIndex >=
|
||||||
filteredTabEntities.value.length - 1) {
|
filteredTabEntities.value.length - 1) {
|
||||||
key = await containerRepository.getTrailingOrderKey(
|
key = await containerRepository.getTrailingOrderKey(
|
||||||
containerId,
|
containerId,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
if (newIndex < oldIndex) {
|
if (targetIndex < oldIndex) {
|
||||||
key = (await containerRepository.getOrderKeyAfterTab(
|
key = (await containerRepository.getOrderKeyAfterTab(
|
||||||
filteredTabEntities.value[newIndex - 1].tabId,
|
filteredTabEntities.value[targetIndex - 1].tabId,
|
||||||
containerId,
|
containerId,
|
||||||
))!;
|
))!;
|
||||||
} else {
|
} else {
|
||||||
key = await containerRepository.getOrderKeyBeforeTab(
|
key = await containerRepository.getOrderKeyBeforeTab(
|
||||||
filteredTabEntities.value[newIndex + 1].tabId,
|
filteredTabEntities.value[targetIndex + 1].tabId,
|
||||||
containerId,
|
containerId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -393,17 +405,20 @@ class _TabListView extends HookConsumerWidget {
|
|||||||
.assignOrderKey(tabId, key);
|
.assignOrderKey(tabId, key);
|
||||||
},
|
},
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final CustomDraggable tab;
|
|
||||||
|
|
||||||
if (index < filteredTabEntities.value.length) {
|
if (index < filteredTabEntities.value.length) {
|
||||||
final entity = filteredTabEntities.value[index];
|
final entity = filteredTabEntities.value[index];
|
||||||
tab = CustomDraggable(
|
return CustomDraggable(
|
||||||
key: Key(entity.tabId),
|
key: Key(entity.tabId),
|
||||||
data: TabDragData(entity.tabId),
|
data: TabDragData(entity.tabId),
|
||||||
child: _TabDraggable(
|
child: TabContextMenuDraggable(
|
||||||
entity: entity,
|
tabId: entity.tabId,
|
||||||
onClose: onClose,
|
feedbackSize: Size.zero,
|
||||||
height: _itemHeight,
|
externalDrag: true,
|
||||||
|
child: _TabDraggable(
|
||||||
|
entity: entity,
|
||||||
|
onClose: onClose,
|
||||||
|
height: _itemHeight,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -411,7 +426,7 @@ class _TabListView extends HookConsumerWidget {
|
|||||||
index - filteredTabEntities.value.length;
|
index - filteredTabEntities.value.length;
|
||||||
final entity = suggestedTabEntities.value[suggestedIndex];
|
final entity = suggestedTabEntities.value[suggestedIndex];
|
||||||
|
|
||||||
tab = CustomDraggable(
|
return CustomDraggable(
|
||||||
key: Key('suggested_${entity.tabId}'),
|
key: Key('suggested_${entity.tabId}'),
|
||||||
child: _TabDraggable(
|
child: _TabDraggable(
|
||||||
entity: entity,
|
entity: entity,
|
||||||
@@ -421,25 +436,6 @@ class _TabListView extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// return LongPressDraggable(
|
|
||||||
// key: tab.key,
|
|
||||||
// feedback: Material(
|
|
||||||
// color: Colors
|
|
||||||
// .transparent, // removes white corners when having shadow
|
|
||||||
// child: Transform.scale(
|
|
||||||
// scale: 1.05,
|
|
||||||
// child: SizedBox(
|
|
||||||
// height: _itemHeight,
|
|
||||||
// width: MediaQuery.of(context).size.width,
|
|
||||||
// child: tab.child,
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// data: tab.data,
|
|
||||||
// child: tab.child,
|
|
||||||
// );
|
|
||||||
return tab;
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
+388
-223
@@ -32,6 +32,7 @@ import 'package:weblibre/features/geckoview/features/browser/presentation/widget
|
|||||||
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/data/entities/tab_mode.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/tabs/domain/providers.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';
|
||||||
@@ -73,29 +74,46 @@ class GridTabItemContainer extends StatelessWidget {
|
|||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
static const borderRadius = BorderRadius.all(Radius.circular(12.0));
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
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) {
|
final bgColor = switch (tabMode) {
|
||||||
PrivateTabMode() => appColors.privateTabBackground,
|
PrivateTabMode() => appColors.privateTabBackground.withAlpha(80),
|
||||||
IsolatedTabMode() => appColors.isolatedTabBackground,
|
IsolatedTabMode() => appColors.isolatedTabBackground.withAlpha(80),
|
||||||
RegularTabMode() => colorScheme.surfaceContainerHighest,
|
RegularTabMode() => colorScheme.surfaceContainerHigh,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
final borderWidth = isActive ? 4.0 : 3.0;
|
||||||
|
final innerRadius = BorderRadius.all(Radius.circular(12.0 - borderWidth));
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: isActive ? colorScheme.primary : colorScheme.outline,
|
color: isActive
|
||||||
width: isActive ? 2.0 : 1.0,
|
? colorScheme.primary
|
||||||
|
: colorScheme.outline.withAlpha(40),
|
||||||
|
width: borderWidth,
|
||||||
),
|
),
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(16.0)),
|
borderRadius: borderRadius,
|
||||||
|
boxShadow: isActive
|
||||||
|
? [
|
||||||
|
BoxShadow(
|
||||||
|
color: colorScheme.primary.withAlpha(60),
|
||||||
|
blurRadius: 8.0,
|
||||||
|
spreadRadius: 1.0,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
child: Material(
|
child: Material(
|
||||||
color: bgColor,
|
color: colorScheme.surface,
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(14.0)),
|
borderRadius: innerRadius,
|
||||||
child: child,
|
clipBehavior: Clip.antiAlias,
|
||||||
|
child: Material(color: bgColor, child: child),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -107,10 +125,11 @@ class GridTabPreview extends HookConsumerWidget {
|
|||||||
|
|
||||||
final VoidCallback? onTap;
|
final VoidCallback? onTap;
|
||||||
final VoidCallback? onDoubleTap;
|
final VoidCallback? onDoubleTap;
|
||||||
final VoidCallback? onLongPress;
|
|
||||||
final VoidCallback? onDelete;
|
final VoidCallback? onDelete;
|
||||||
final void Function(String host)? onDeleteAll;
|
final void Function(String host)? onDeleteAll;
|
||||||
|
|
||||||
|
final bool showPinBadge;
|
||||||
|
|
||||||
final Widget? trailingChild;
|
final Widget? trailingChild;
|
||||||
|
|
||||||
const GridTabPreview({
|
const GridTabPreview({
|
||||||
@@ -118,15 +137,17 @@ class GridTabPreview extends HookConsumerWidget {
|
|||||||
required this.isActive,
|
required this.isActive,
|
||||||
this.onTap,
|
this.onTap,
|
||||||
this.onDoubleTap,
|
this.onDoubleTap,
|
||||||
this.onLongPress,
|
|
||||||
this.onDelete,
|
this.onDelete,
|
||||||
this.onDeleteAll,
|
this.onDeleteAll,
|
||||||
|
this.showPinBadge = false,
|
||||||
this.trailingChild,
|
this.trailingChild,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
final textTheme = Theme.of(context).textTheme;
|
||||||
final appColors = AppColors.of(context);
|
final appColors = AppColors.of(context);
|
||||||
|
|
||||||
final tabState =
|
final tabState =
|
||||||
@@ -139,137 +160,232 @@ class GridTabPreview extends HookConsumerWidget {
|
|||||||
|
|
||||||
final extendedDeleteMenuController = useMenuController();
|
final extendedDeleteMenuController = useMenuController();
|
||||||
|
|
||||||
|
// ignore: avoid_bool_literals_in_conditional_expressions
|
||||||
|
final isPinned = showPinBadge
|
||||||
|
? ref.watch(
|
||||||
|
watchPinnedTabIdsProvider.select(
|
||||||
|
(v) => v.value?.contains(tabId) ?? false,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: false;
|
||||||
|
|
||||||
final modeTextColor = switch (tabState.tabMode) {
|
final modeTextColor = switch (tabState.tabMode) {
|
||||||
PrivateTabMode() => appColors.privateTabForeground,
|
PrivateTabMode() => appColors.privateTabForeground,
|
||||||
IsolatedTabMode() => appColors.isolatedTabForeground,
|
IsolatedTabMode() => appColors.isolatedTabForeground,
|
||||||
RegularTabMode() => null,
|
RegularTabMode() => null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
final subtitleColor = switch (tabState.tabMode) {
|
||||||
|
PrivateTabMode() => Color.lerp(
|
||||||
|
appColors.privateTabPurple,
|
||||||
|
Colors.white,
|
||||||
|
0.4,
|
||||||
|
)!,
|
||||||
|
IsolatedTabMode() => Color.lerp(
|
||||||
|
appColors.isolatedTabTeal,
|
||||||
|
Colors.white,
|
||||||
|
0.4,
|
||||||
|
)!,
|
||||||
|
RegularTabMode() => colorScheme.onSurfaceVariant,
|
||||||
|
};
|
||||||
|
|
||||||
|
final (modeBadgeIcon, modeBadgeColor) = switch (tabState.tabMode) {
|
||||||
|
PrivateTabMode() => (MdiIcons.dominoMask, appColors.privateTabPurple),
|
||||||
|
IsolatedTabMode() => (MdiIcons.snowflake, appColors.isolatedTabTeal),
|
||||||
|
RegularTabMode() => (null, null),
|
||||||
|
};
|
||||||
|
|
||||||
return GridTabItemContainer(
|
return GridTabItemContainer(
|
||||||
isActive: isActive,
|
isActive: isActive,
|
||||||
tabMode: tabState.tabMode,
|
tabMode: tabState.tabMode,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(14.0)),
|
borderRadius: GridTabItemContainer.borderRadius,
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
onDoubleTap: onDoubleTap,
|
onDoubleTap: onDoubleTap,
|
||||||
onLongPress: onLongPress,
|
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Row(
|
// Thumbnail or icon area
|
||||||
children: [
|
Expanded(
|
||||||
Expanded(
|
child: Stack(
|
||||||
child: Padding(
|
fit: StackFit.expand,
|
||||||
padding: const EdgeInsets.only(left: 6.0, top: 2.0),
|
children: [
|
||||||
child: Text(
|
if (tabState.thumbnail != null &&
|
||||||
overflow: TextOverflow.ellipsis,
|
!tabState.thumbnail!.isDisposed)
|
||||||
tabState.titleOrAuthority,
|
RepaintBoundary(
|
||||||
maxLines: 2,
|
|
||||||
style: modeTextColor != null
|
|
||||||
? TextStyle(color: modeTextColor)
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (onDelete != null || onDeleteAll != null)
|
|
||||||
MenuAnchor(
|
|
||||||
controller: extendedDeleteMenuController,
|
|
||||||
builder: (context, controller, child) {
|
|
||||||
return child!;
|
|
||||||
},
|
|
||||||
menuChildren: [
|
|
||||||
MenuItemButton(
|
|
||||||
onPressed: () {
|
|
||||||
onDeleteAll?.call(tabState.url.host);
|
|
||||||
},
|
|
||||||
|
|
||||||
leadingIcon: const Icon(MdiIcons.closeBoxMultiple),
|
|
||||||
child: Text('Close all from ${tabState.url.host}'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
child: IconButton(
|
|
||||||
visualDensity: const VisualDensity(
|
|
||||||
horizontal: -4.0,
|
|
||||||
vertical: -4.0,
|
|
||||||
),
|
|
||||||
onPressed: onDelete,
|
|
||||||
onLongPress: onDeleteAll != null
|
|
||||||
? () {
|
|
||||||
if (extendedDeleteMenuController.isOpen) {
|
|
||||||
extendedDeleteMenuController.close();
|
|
||||||
} else {
|
|
||||||
extendedDeleteMenuController.open();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
icon: const Icon(Icons.close),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
?trailingChild,
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
const SizedBox(width: 6.0),
|
|
||||||
TabIcon(tabState: tabState, iconSize: 16.0),
|
|
||||||
const SizedBox(width: 6.0),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
tabState.url.authority,
|
|
||||||
style: Theme.of(
|
|
||||||
context,
|
|
||||||
).textTheme.bodySmall?.copyWith(color: modeTextColor),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (tabState.tabMode is PrivateTabMode ||
|
|
||||||
tabState.tabMode is IsolatedTabMode) ...[
|
|
||||||
const SizedBox(width: 6.0),
|
|
||||||
SizedBox(
|
|
||||||
height: 16,
|
|
||||||
width: 24,
|
|
||||||
child: Stack(
|
|
||||||
fit: StackFit.expand,
|
|
||||||
children: [
|
|
||||||
Positioned(
|
|
||||||
top: -4,
|
|
||||||
child: Icon(
|
|
||||||
tabState.tabMode is IsolatedTabMode
|
|
||||||
? MdiIcons.snowflake
|
|
||||||
: MdiIcons.dominoMask,
|
|
||||||
color: tabState.tabMode is IsolatedTabMode
|
|
||||||
? appColors.isolatedTabTeal
|
|
||||||
: appColors.privateTabPurple,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
const SizedBox(width: 8.0),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 6),
|
|
||||||
if (tabState.thumbnail != null && !tabState.thumbnail!.isDisposed)
|
|
||||||
Expanded(
|
|
||||||
child: ClipRRect(
|
|
||||||
borderRadius: const BorderRadius.only(
|
|
||||||
bottomLeft: Radius.circular(14.0),
|
|
||||||
bottomRight: Radius.circular(14.0),
|
|
||||||
),
|
|
||||||
child: SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: RepaintBoundary(
|
|
||||||
child: SafeRawImage(
|
child: SafeRawImage(
|
||||||
image: tabState.thumbnail,
|
image: tabState.thumbnail,
|
||||||
fit: BoxFit.fitWidth,
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Center(child: TabIcon(tabState: tabState, iconSize: 48)),
|
||||||
|
// Close button overlay
|
||||||
|
if (onDelete != null || onDeleteAll != null)
|
||||||
|
Positioned(
|
||||||
|
top: 6.0,
|
||||||
|
right: 6.0,
|
||||||
|
child: MenuAnchor(
|
||||||
|
controller: extendedDeleteMenuController,
|
||||||
|
builder: (context, controller, child) {
|
||||||
|
return child!;
|
||||||
|
},
|
||||||
|
menuChildren: [
|
||||||
|
MenuItemButton(
|
||||||
|
onPressed: () {
|
||||||
|
onDeleteAll?.call(tabState.url.host);
|
||||||
|
},
|
||||||
|
leadingIcon: const Icon(MdiIcons.closeBoxMultiple),
|
||||||
|
child: Text('Close all from ${tabState.url.host}'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
child: SizedBox(
|
||||||
|
width: 28,
|
||||||
|
height: 28,
|
||||||
|
child: Material(
|
||||||
|
color: colorScheme.surfaceContainerHighest
|
||||||
|
.withAlpha(200),
|
||||||
|
borderRadius: const BorderRadius.all(
|
||||||
|
Radius.circular(8.0),
|
||||||
|
),
|
||||||
|
child: InkWell(
|
||||||
|
borderRadius: const BorderRadius.all(
|
||||||
|
Radius.circular(8.0),
|
||||||
|
),
|
||||||
|
onTap: onDelete,
|
||||||
|
onLongPress: onDeleteAll != null
|
||||||
|
? () {
|
||||||
|
if (extendedDeleteMenuController.isOpen) {
|
||||||
|
extendedDeleteMenuController.close();
|
||||||
|
} else {
|
||||||
|
extendedDeleteMenuController.open();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
child: Icon(
|
||||||
|
Icons.close,
|
||||||
|
size: 16,
|
||||||
|
color: colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
if (trailingChild != null || isPinned)
|
||||||
),
|
Positioned(
|
||||||
)
|
top: 6.0,
|
||||||
else
|
left: 6.0,
|
||||||
Expanded(
|
child: Row(
|
||||||
child: Center(child: TabIcon(tabState: tabState, iconSize: 48)),
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
if (trailingChild != null) trailingChild!,
|
||||||
|
if (isPinned)
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.only(
|
||||||
|
left: trailingChild != null ? 4.0 : 0,
|
||||||
|
),
|
||||||
|
child: SizedBox(
|
||||||
|
width: 28,
|
||||||
|
height: 28,
|
||||||
|
child: Material(
|
||||||
|
color: colorScheme.surfaceContainerHighest
|
||||||
|
.withAlpha(200),
|
||||||
|
borderRadius: const BorderRadius.all(
|
||||||
|
Radius.circular(8.0),
|
||||||
|
),
|
||||||
|
child: InkWell(
|
||||||
|
borderRadius: const BorderRadius.all(
|
||||||
|
Radius.circular(8.0),
|
||||||
|
),
|
||||||
|
onTap: () async {
|
||||||
|
await ref
|
||||||
|
.read(
|
||||||
|
tabDataRepositoryProvider.notifier,
|
||||||
|
)
|
||||||
|
.setPinned(tabId, pinned: false);
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
|
ui_helper.showInfoMessage(
|
||||||
|
context,
|
||||||
|
'Tab unpinned',
|
||||||
|
action: SnackBarAction(
|
||||||
|
label: 'Undo',
|
||||||
|
onPressed: () async {
|
||||||
|
await ref
|
||||||
|
.read(
|
||||||
|
tabDataRepositoryProvider
|
||||||
|
.notifier,
|
||||||
|
)
|
||||||
|
.setPinned(
|
||||||
|
tabId,
|
||||||
|
pinned: true,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Icon(
|
||||||
|
MdiIcons.pin,
|
||||||
|
size: 16,
|
||||||
|
color: colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
// Bottom info band
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 10.0,
|
||||||
|
vertical: 8.0,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: colorScheme.surfaceContainerHighest.withAlpha(120),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
tabState.titleOrAuthority,
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: textTheme.bodyMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: modeTextColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 2.0),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
TabIcon(tabState: tabState, iconSize: 14.0),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
tabState.url.authority,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: textTheme.bodySmall?.copyWith(
|
||||||
|
color: subtitleColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (modeBadgeIcon != null) ...[
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Icon(modeBadgeIcon, color: modeBadgeColor, size: 14),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -282,19 +398,20 @@ class ListTabPreview extends HookConsumerWidget {
|
|||||||
final bool isActive;
|
final bool isActive;
|
||||||
|
|
||||||
final VoidCallback? onTap;
|
final VoidCallback? onTap;
|
||||||
final VoidCallback? onLongPress;
|
|
||||||
final VoidCallback? onDelete;
|
final VoidCallback? onDelete;
|
||||||
final void Function(String host)? onDeleteAll;
|
final void Function(String host)? onDeleteAll;
|
||||||
|
|
||||||
|
final bool showPinBadge;
|
||||||
|
|
||||||
final Widget? trailingChild;
|
final Widget? trailingChild;
|
||||||
|
|
||||||
const ListTabPreview({
|
const ListTabPreview({
|
||||||
required this.tabId,
|
required this.tabId,
|
||||||
required this.isActive,
|
required this.isActive,
|
||||||
this.onTap,
|
this.onTap,
|
||||||
this.onLongPress,
|
|
||||||
this.onDelete,
|
this.onDelete,
|
||||||
this.onDeleteAll,
|
this.onDeleteAll,
|
||||||
|
this.showPinBadge = false,
|
||||||
this.trailingChild,
|
this.trailingChild,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
@@ -302,6 +419,7 @@ class ListTabPreview extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final colorScheme = Theme.of(context).colorScheme;
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
final textTheme = Theme.of(context).textTheme;
|
||||||
final appColors = AppColors.of(context);
|
final appColors = AppColors.of(context);
|
||||||
|
|
||||||
final tabState =
|
final tabState =
|
||||||
@@ -318,17 +436,30 @@ class ListTabPreview extends HookConsumerWidget {
|
|||||||
|
|
||||||
final extendedDeleteMenuController = useMenuController();
|
final extendedDeleteMenuController = useMenuController();
|
||||||
|
|
||||||
|
// ignore: avoid_bool_literals_in_conditional_expressions
|
||||||
|
final isPinned = showPinBadge
|
||||||
|
? ref.watch(
|
||||||
|
watchPinnedTabIdsProvider.select(
|
||||||
|
(v) => v.value?.contains(tabId) ?? false,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: false;
|
||||||
|
|
||||||
final leadingWidget = switch ((tabListShowFavicons, tabState.thumbnail)) {
|
final leadingWidget = switch ((tabListShowFavicons, tabState.thumbnail)) {
|
||||||
(false, final thumbnail?) when !thumbnail.isDisposed => RepaintBoundary(
|
(false, final thumbnail?) when !thumbnail.isDisposed => ClipRRect(
|
||||||
child: SafeRawImage(image: thumbnail, fit: BoxFit.fitHeight),
|
borderRadius: const BorderRadius.all(Radius.circular(8.0)),
|
||||||
|
child: RepaintBoundary(
|
||||||
|
child: SafeRawImage(image: thumbnail, fit: BoxFit.fitHeight),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
_ => TabIcon(tabState: tabState, iconSize: 32),
|
_ => TabIcon(tabState: tabState, iconSize: 32),
|
||||||
};
|
};
|
||||||
|
|
||||||
final listBgColor = switch (tabState.tabMode) {
|
final listBgColor = switch (tabState.tabMode) {
|
||||||
PrivateTabMode() => appColors.privateTabBackground,
|
PrivateTabMode() => appColors.privateTabBackground.withAlpha(80),
|
||||||
IsolatedTabMode() => appColors.isolatedTabBackground,
|
IsolatedTabMode() => appColors.isolatedTabBackground.withAlpha(80),
|
||||||
RegularTabMode() => null,
|
RegularTabMode() when isActive => colorScheme.primary.withAlpha(20),
|
||||||
|
RegularTabMode() => Colors.transparent,
|
||||||
};
|
};
|
||||||
final listTextColor = switch (tabState.tabMode) {
|
final listTextColor = switch (tabState.tabMode) {
|
||||||
PrivateTabMode() => appColors.privateTabForeground,
|
PrivateTabMode() => appColors.privateTabForeground,
|
||||||
@@ -341,84 +472,128 @@ class ListTabPreview extends HookConsumerWidget {
|
|||||||
RegularTabMode() => (null, null),
|
RegularTabMode() => (null, null),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
final subtitleColor = switch (tabState.tabMode) {
|
||||||
|
PrivateTabMode() => Color.lerp(
|
||||||
|
appColors.privateTabPurple,
|
||||||
|
Colors.white,
|
||||||
|
0.4,
|
||||||
|
)!,
|
||||||
|
IsolatedTabMode() => Color.lerp(
|
||||||
|
appColors.isolatedTabTeal,
|
||||||
|
Colors.white,
|
||||||
|
0.4,
|
||||||
|
)!,
|
||||||
|
RegularTabMode() => colorScheme.onSurfaceVariant,
|
||||||
|
};
|
||||||
|
|
||||||
|
const borderRadius = BorderRadius.all(Radius.circular(12.0));
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
|
margin: const EdgeInsets.symmetric(vertical: 3.0, horizontal: 4.0),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: listBgColor,
|
color: listBgColor,
|
||||||
border: isActive ? Border.all(color: colorScheme.primary) : null,
|
borderRadius: borderRadius,
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(4.0)),
|
border: isActive
|
||||||
|
? Border(left: BorderSide(color: colorScheme.primary, width: 4.0))
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Material(
|
||||||
children: [
|
color: Colors.transparent,
|
||||||
Expanded(
|
borderRadius: borderRadius,
|
||||||
child: ListTile(
|
clipBehavior: Clip.antiAlias,
|
||||||
key: ValueKey(tabState.id),
|
child: InkWell(
|
||||||
onTap: onTap,
|
borderRadius: borderRadius,
|
||||||
onLongPress: onLongPress,
|
onTap: onTap,
|
||||||
contentPadding: const EdgeInsets.only(left: 4),
|
child: Padding(
|
||||||
leading: leadingWidget,
|
padding: const EdgeInsets.only(left: 12.0, top: 10.0, bottom: 10.0),
|
||||||
title: Text(
|
child: Row(
|
||||||
overflow: TextOverflow.ellipsis,
|
children: [
|
||||||
tabState.titleOrAuthority,
|
leadingWidget,
|
||||||
maxLines: 2,
|
const SizedBox(width: 14.0),
|
||||||
style: listTextColor != null
|
Expanded(
|
||||||
? TextStyle(color: listTextColor)
|
child: Column(
|
||||||
: null,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
),
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
subtitle: Row(
|
children: [
|
||||||
children: [
|
Text(
|
||||||
if (modeBadgeIcon != null) ...[
|
tabState.titleOrAuthority,
|
||||||
Icon(modeBadgeIcon, color: modeBadgeColor, size: 14),
|
maxLines: 2,
|
||||||
const SizedBox(width: 4),
|
overflow: TextOverflow.ellipsis,
|
||||||
],
|
style: textTheme.bodyMedium?.copyWith(
|
||||||
Expanded(
|
fontWeight: FontWeight.w600,
|
||||||
child: UriBreadcrumb(
|
color: listTextColor,
|
||||||
uri: tabState.url,
|
),
|
||||||
style: Theme.of(
|
),
|
||||||
context,
|
const SizedBox(height: 3.0),
|
||||||
).textTheme.bodySmall?.copyWith(color: listTextColor),
|
Row(
|
||||||
),
|
children: [
|
||||||
),
|
if (isPinned) ...[
|
||||||
],
|
Icon(
|
||||||
),
|
MdiIcons.pin,
|
||||||
),
|
color: colorScheme.primary,
|
||||||
),
|
size: 14,
|
||||||
Row(
|
),
|
||||||
mainAxisSize: MainAxisSize.min,
|
const SizedBox(width: 4),
|
||||||
children: [
|
],
|
||||||
if (onDelete != null || onDeleteAll != null)
|
if (modeBadgeIcon != null) ...[
|
||||||
MenuAnchor(
|
Icon(
|
||||||
controller: extendedDeleteMenuController,
|
modeBadgeIcon,
|
||||||
builder: (context, controller, child) {
|
color: modeBadgeColor,
|
||||||
return child!;
|
size: 14,
|
||||||
},
|
),
|
||||||
menuChildren: [
|
const SizedBox(width: 4),
|
||||||
MenuItemButton(
|
],
|
||||||
onPressed: () {
|
Expanded(
|
||||||
onDeleteAll?.call(tabState.url.host);
|
child: UriBreadcrumb(
|
||||||
},
|
uri: tabState.url,
|
||||||
|
style: textTheme.bodySmall?.copyWith(
|
||||||
leadingIcon: const Icon(MdiIcons.closeBoxMultiple),
|
color: subtitleColor,
|
||||||
child: Text('Close all from ${tabState.url.host}'),
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
child: IconButton(
|
],
|
||||||
onPressed: onDelete,
|
),
|
||||||
onLongPress: onDeleteAll != null
|
],
|
||||||
? () {
|
|
||||||
if (extendedDeleteMenuController.isOpen) {
|
|
||||||
extendedDeleteMenuController.close();
|
|
||||||
} else {
|
|
||||||
extendedDeleteMenuController.open();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
icon: const Icon(Icons.close),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
?trailingChild,
|
if (onDelete != null || onDeleteAll != null)
|
||||||
],
|
MenuAnchor(
|
||||||
|
controller: extendedDeleteMenuController,
|
||||||
|
builder: (context, controller, child) {
|
||||||
|
return child!;
|
||||||
|
},
|
||||||
|
menuChildren: [
|
||||||
|
MenuItemButton(
|
||||||
|
onPressed: () {
|
||||||
|
onDeleteAll?.call(tabState.url.host);
|
||||||
|
},
|
||||||
|
leadingIcon: const Icon(MdiIcons.closeBoxMultiple),
|
||||||
|
child: Text('Close all from ${tabState.url.host}'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
child: IconButton(
|
||||||
|
onPressed: onDelete,
|
||||||
|
onLongPress: onDeleteAll != null
|
||||||
|
? () {
|
||||||
|
if (extendedDeleteMenuController.isOpen) {
|
||||||
|
extendedDeleteMenuController.close();
|
||||||
|
} else {
|
||||||
|
extendedDeleteMenuController.open();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
icon: Icon(
|
||||||
|
Icons.close,
|
||||||
|
size: 20,
|
||||||
|
color: colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
?trailingChild,
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -530,6 +705,7 @@ class SingleGridTabPreview extends HookConsumerWidget {
|
|||||||
child: GridTabPreview(
|
child: GridTabPreview(
|
||||||
tabId: tabId,
|
tabId: tabId,
|
||||||
isActive: tabId == activeTabId,
|
isActive: tabId == activeTabId,
|
||||||
|
showPinBadge: true,
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
if (tabId != activeTabId) {
|
if (tabId != activeTabId) {
|
||||||
//Close first to avoid rebuilds
|
//Close first to avoid rebuilds
|
||||||
@@ -563,15 +739,6 @@ class SingleGridTabPreview extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// onDoubleTap: () {
|
|
||||||
// ref.read(overlayDialogControllerProvider.notifier).show(
|
|
||||||
// TabActionDialog(
|
|
||||||
// initialTab: tab,
|
|
||||||
// onDismiss:
|
|
||||||
// ref.read(overlayDialogControllerProvider.notifier).dismiss,
|
|
||||||
// ),
|
|
||||||
// );
|
|
||||||
// },
|
|
||||||
onDelete: () async {
|
onDelete: () async {
|
||||||
onBeforeDelete?.call();
|
onBeforeDelete?.call();
|
||||||
|
|
||||||
@@ -655,6 +822,7 @@ class SingleListTabPreview extends HookConsumerWidget {
|
|||||||
child: ListTabPreview(
|
child: ListTabPreview(
|
||||||
tabId: tabId,
|
tabId: tabId,
|
||||||
isActive: tabId == activeTabId,
|
isActive: tabId == activeTabId,
|
||||||
|
showPinBadge: true,
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
if (tabId != activeTabId) {
|
if (tabId != activeTabId) {
|
||||||
//Close first to avoid rebuilds
|
//Close first to avoid rebuilds
|
||||||
@@ -688,15 +856,6 @@ class SingleListTabPreview extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// onDoubleTap: () {
|
|
||||||
// ref.read(overlayDialogControllerProvider.notifier).show(
|
|
||||||
// TabActionDialog(
|
|
||||||
// initialTab: tab,
|
|
||||||
// onDismiss:
|
|
||||||
// ref.read(overlayDialogControllerProvider.notifier).dismiss,
|
|
||||||
// ),
|
|
||||||
// );
|
|
||||||
// },
|
|
||||||
onDelete: () async {
|
onDelete: () async {
|
||||||
onBeforeDelete?.call();
|
onBeforeDelete?.call();
|
||||||
|
|
||||||
@@ -741,10 +900,16 @@ class SuggestedSingleGridTabPreview extends StatelessWidget {
|
|||||||
tabId: tabId,
|
tabId: tabId,
|
||||||
isActive: tabId == activeTabId,
|
isActive: tabId == activeTabId,
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
trailingChild: const IconButton(
|
trailingChild: SizedBox(
|
||||||
visualDensity: VisualDensity(horizontal: -4.0, vertical: -4.0),
|
width: 28,
|
||||||
icon: Icon(MdiIcons.creation),
|
height: 28,
|
||||||
onPressed: null,
|
child: Material(
|
||||||
|
color: Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.surfaceContainerHighest.withAlpha(200),
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(8.0)),
|
||||||
|
child: const Icon(MdiIcons.creation, size: 16),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
+2
-13
@@ -97,24 +97,13 @@ class _TabTreePreview extends HookConsumerWidget {
|
|||||||
Badge.count(
|
Badge.count(
|
||||||
isLabelVisible: entity.totalTabs > 1,
|
isLabelVisible: entity.totalTabs > 1,
|
||||||
count: entity.totalTabs,
|
count: entity.totalTabs,
|
||||||
alignment: AlignmentDirectional.bottomEnd,
|
alignment: AlignmentDirectional.topStart,
|
||||||
offset: const Offset(-8, -24),
|
offset: const Offset(8, 8),
|
||||||
backgroundColor: Theme.of(context).colorScheme.primaryContainer,
|
backgroundColor: Theme.of(context).colorScheme.primaryContainer,
|
||||||
textColor: Theme.of(context).colorScheme.onPrimaryContainer,
|
textColor: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||||
child: GridTabPreview(
|
child: GridTabPreview(
|
||||||
tabId: entity.tabId,
|
tabId: entity.tabId,
|
||||||
isActive: entity.tabId == activeTabId,
|
isActive: entity.tabId == activeTabId,
|
||||||
onLongPress: () async {
|
|
||||||
if (entity.tabId != activeTabId) {
|
|
||||||
//Close first to avoid rebuilds
|
|
||||||
onClose();
|
|
||||||
await ref
|
|
||||||
.read(tabRepositoryProvider.notifier)
|
|
||||||
.selectTab(entity.tabId);
|
|
||||||
} else {
|
|
||||||
onClose();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
if (entity.totalTabs > 1) {
|
if (entity.totalTabs > 1) {
|
||||||
await TabTreeRoute(entity.rootId).push(context);
|
await TabTreeRoute(entity.rootId).push(context);
|
||||||
|
|||||||
+614
-281
@@ -25,6 +25,7 @@ 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:intl/intl.dart';
|
||||||
import 'package:weblibre/core/design/app_colors.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/providers/persisted_bool.dart';
|
import 'package:weblibre/core/providers/persisted_bool.dart';
|
||||||
@@ -34,6 +35,8 @@ import 'package:weblibre/features/geckoview/domain/providers.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/bookmarks/domain/repositories/bookmarks.dart';
|
import 'package:weblibre/features/geckoview/features/bookmarks/domain/repositories/bookmarks.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/browser/domain/entities/tab_view_filter_options.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/browser/domain/providers.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';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/tab_view_controllers.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/tab_view_controllers.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/dialogs/bookmark_all_dialog.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/dialogs/bookmark_all_dialog.dart';
|
||||||
@@ -220,6 +223,7 @@ class TabViewHeader extends HookConsumerWidget {
|
|||||||
|
|
||||||
final viewModeMenuController = useMenuController();
|
final viewModeMenuController = useMenuController();
|
||||||
final tabsActionMenuController = useMenuController();
|
final tabsActionMenuController = useMenuController();
|
||||||
|
final filterMenuController = useMenuController();
|
||||||
|
|
||||||
final hasSearchText = useListenableSelector(
|
final hasSearchText = useListenableSelector(
|
||||||
searchTextController,
|
searchTextController,
|
||||||
@@ -248,6 +252,39 @@ class TabViewHeader extends HookConsumerWidget {
|
|||||||
(scope) => scope == TabsTrayScope.synced,
|
(scope) => scope == TabsTrayScope.synced,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
final canManualTabReorder = ref.watch(canManualTabReorderProvider);
|
||||||
|
|
||||||
|
final tabsReorderable = ref.watch(tabsReorderableControllerProvider);
|
||||||
|
|
||||||
|
final canManualReorder =
|
||||||
|
!isSyncedScope &&
|
||||||
|
tabsViewMode != TabsViewMode.tree &&
|
||||||
|
canManualTabReorder;
|
||||||
|
|
||||||
|
final didShowReorderDisabledInfo = useRef(false);
|
||||||
|
|
||||||
|
useEffect(() {
|
||||||
|
if (tabsReorderable && !canManualReorder) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
ref.read(tabsReorderableControllerProvider.notifier).hide();
|
||||||
|
|
||||||
|
if (!didShowReorderDisabledInfo.value) {
|
||||||
|
didShowReorderDisabledInfo.value = true;
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
|
ui_helper.showInfoMessage(
|
||||||
|
context,
|
||||||
|
'Tab reordering is only available in default manual mode',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
didShowReorderDisabledInfo.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}, [tabsReorderable, canManualReorder]);
|
||||||
|
|
||||||
useOnListenableChange(searchTextController, () async {
|
useOnListenableChange(searchTextController, () async {
|
||||||
if (ref.exists(tabSearchRepositoryProvider(TabSearchPartition.preview))) {
|
if (ref.exists(tabSearchRepositoryProvider(TabSearchPartition.preview))) {
|
||||||
@@ -269,307 +306,600 @@ class TabViewHeader extends HookConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
if (!searchMode.value)
|
if (!searchMode.value)
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
children: [
|
||||||
Row(
|
if (tabsViewMode != TabsViewMode.tree)
|
||||||
mainAxisSize: MainAxisSize.min,
|
IconButton(
|
||||||
children: [
|
icon: const Icon(MdiIcons.tabSearch),
|
||||||
IconButton(
|
iconSize: 18,
|
||||||
icon: const Icon(MdiIcons.tabSearch),
|
padding: EdgeInsets.zero,
|
||||||
iconSize: 18,
|
tooltip: 'Search inside tabs',
|
||||||
padding: EdgeInsets.zero,
|
onPressed: () {
|
||||||
tooltip: 'Search inside tabs',
|
searchMode.value = true;
|
||||||
onPressed: () {
|
searchTextFocus.requestFocus();
|
||||||
switch (ref.read(tabsViewModeControllerProvider)) {
|
},
|
||||||
case TabsViewMode.tree:
|
),
|
||||||
case TabsViewMode.list:
|
if (!isSyncedScope && tabsViewMode != TabsViewMode.tree)
|
||||||
break;
|
Consumer(
|
||||||
case TabsViewMode.grid:
|
builder: (context, ref, child) {
|
||||||
|
final filterOptions = ref.watch(
|
||||||
|
tabViewFilterControllerProvider,
|
||||||
|
);
|
||||||
|
final hasActiveFilter = filterOptions.hasActiveFilter;
|
||||||
|
|
||||||
|
return MenuAnchor(
|
||||||
|
controller: filterMenuController,
|
||||||
|
consumeOutsideTap: true,
|
||||||
|
menuChildren: [
|
||||||
|
// Tab type filter
|
||||||
|
SubmenuButton(
|
||||||
|
leadingIcon: const Icon(MdiIcons.tabUnselected),
|
||||||
|
menuChildren: TabTypeFilter.values.map((type) {
|
||||||
|
final appColors = AppColors.of(context);
|
||||||
|
final (icon, color) = switch (type) {
|
||||||
|
TabTypeFilter.all => (
|
||||||
|
MdiIcons.tabUnselected,
|
||||||
|
null,
|
||||||
|
),
|
||||||
|
TabTypeFilter.regularOnly => (
|
||||||
|
MdiIcons.tab,
|
||||||
|
null,
|
||||||
|
),
|
||||||
|
TabTypeFilter.privateOnly => (
|
||||||
|
MdiIcons.dominoMask,
|
||||||
|
appColors.privateTabPurple,
|
||||||
|
),
|
||||||
|
TabTypeFilter.isolatedOnly => (
|
||||||
|
MdiIcons.snowflake,
|
||||||
|
appColors.isolatedTabTeal,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
return MenuItemButton(
|
||||||
|
leadingIcon: Icon(
|
||||||
|
filterOptions.tabTypeFilter == type
|
||||||
|
? Icons.radio_button_checked
|
||||||
|
: Icons.radio_button_unchecked,
|
||||||
|
),
|
||||||
|
trailingIcon: Icon(icon, color: color),
|
||||||
|
child: Text(type.label),
|
||||||
|
onPressed: () {
|
||||||
|
ref
|
||||||
|
.read(
|
||||||
|
tabViewFilterControllerProvider
|
||||||
|
.notifier,
|
||||||
|
)
|
||||||
|
.setTabTypeFilter(type);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
child: const Text('Tab Type'),
|
||||||
|
),
|
||||||
|
// Sort
|
||||||
|
SubmenuButton(
|
||||||
|
leadingIcon: const Icon(Icons.sort),
|
||||||
|
menuChildren: [
|
||||||
|
...TabSortType.values.map(
|
||||||
|
(sort) => MenuItemButton(
|
||||||
|
leadingIcon: Icon(
|
||||||
|
filterOptions.sortType == sort
|
||||||
|
? Icons.radio_button_checked
|
||||||
|
: Icons.radio_button_unchecked,
|
||||||
|
),
|
||||||
|
child: Text(sort.label),
|
||||||
|
onPressed: () {
|
||||||
|
ref
|
||||||
|
.read(
|
||||||
|
tabViewFilterControllerProvider
|
||||||
|
.notifier,
|
||||||
|
)
|
||||||
|
.setSortType(sort);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(),
|
||||||
|
MenuItemButton(
|
||||||
|
leadingIcon: Icon(
|
||||||
|
filterOptions.sortPinnedFirst ||
|
||||||
|
filterOptions
|
||||||
|
.sortType
|
||||||
|
.sortField ==
|
||||||
|
null
|
||||||
|
? Icons.check_box
|
||||||
|
: Icons.check_box_outline_blank,
|
||||||
|
),
|
||||||
|
onPressed:
|
||||||
|
filterOptions.sortType.sortField == null
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
ref
|
||||||
|
.read(
|
||||||
|
tabViewFilterControllerProvider
|
||||||
|
.notifier,
|
||||||
|
)
|
||||||
|
.setSortPinnedFirst(
|
||||||
|
!filterOptions
|
||||||
|
.sortPinnedFirst,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: const Text('Sort Pinned First'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
child: const Text('Sort'),
|
||||||
|
),
|
||||||
|
const Divider(),
|
||||||
|
// Date range picker
|
||||||
|
MenuItemButton(
|
||||||
|
closeOnActivate: false,
|
||||||
|
leadingIcon: const Icon(MdiIcons.calendarRange),
|
||||||
|
trailingIcon: filterOptions.dateRange != null
|
||||||
|
? IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
ref
|
||||||
|
.read(
|
||||||
|
tabViewFilterControllerProvider
|
||||||
|
.notifier,
|
||||||
|
)
|
||||||
|
.setDateRange(null);
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.clear),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
child: filterOptions.dateRange != null
|
||||||
|
? Text(
|
||||||
|
'${DateFormat.yMd().format(filterOptions.dateRange!.start)} - ${DateFormat.yMd().format(filterOptions.dateRange!.end)}',
|
||||||
|
)
|
||||||
|
: const Text('Filter Date'),
|
||||||
|
onPressed: () async {
|
||||||
|
final range = await showDateRangePicker(
|
||||||
|
context: context,
|
||||||
|
initialDateRange: filterOptions.dateRange,
|
||||||
|
firstDate: DateTime.now().subtract(
|
||||||
|
const Duration(days: 365),
|
||||||
|
),
|
||||||
|
lastDate: DateTime.now(),
|
||||||
|
);
|
||||||
|
if (range != null) {
|
||||||
|
ref
|
||||||
|
.read(
|
||||||
|
tabViewFilterControllerProvider
|
||||||
|
.notifier,
|
||||||
|
)
|
||||||
|
.setDateRange(
|
||||||
|
DateTimeRange(
|
||||||
|
start: range.start,
|
||||||
|
end: range.end.add(
|
||||||
|
const Duration(days: 1) -
|
||||||
|
const Duration(
|
||||||
|
milliseconds: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
// Quick intervals
|
||||||
|
SubmenuButton(
|
||||||
|
leadingIcon: const Icon(MdiIcons.clockOutline),
|
||||||
|
menuChildren: TabQuickInterval.values
|
||||||
|
.map(
|
||||||
|
(interval) => MenuItemButton(
|
||||||
|
leadingIcon: Icon(
|
||||||
|
filterOptions.quickInterval ==
|
||||||
|
interval
|
||||||
|
? Icons.radio_button_checked
|
||||||
|
: Icons.radio_button_unchecked,
|
||||||
|
),
|
||||||
|
child: Text(interval.label),
|
||||||
|
onPressed: () {
|
||||||
|
ref
|
||||||
|
.read(
|
||||||
|
tabViewFilterControllerProvider
|
||||||
|
.notifier,
|
||||||
|
)
|
||||||
|
.setQuickInterval(
|
||||||
|
filterOptions.quickInterval ==
|
||||||
|
interval
|
||||||
|
? null
|
||||||
|
: interval,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
child: const Text('Quick Interval'),
|
||||||
|
),
|
||||||
|
const Divider(),
|
||||||
|
// Reset
|
||||||
|
MenuItemButton(
|
||||||
|
leadingIcon: const Icon(MdiIcons.restore),
|
||||||
|
child: const Text('Reset Filter'),
|
||||||
|
onPressed: () {
|
||||||
|
ref
|
||||||
|
.read(
|
||||||
|
tabViewFilterControllerProvider
|
||||||
|
.notifier,
|
||||||
|
)
|
||||||
|
.reset();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
child: IconButton(
|
||||||
|
tooltip: 'Filter & Sort',
|
||||||
|
onPressed: () {
|
||||||
|
if (filterMenuController.isOpen) {
|
||||||
|
filterMenuController.close();
|
||||||
|
} else {
|
||||||
|
filterMenuController.open();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon: Badge(
|
||||||
|
isLabelVisible: hasActiveFilter,
|
||||||
|
child: const Icon(MdiIcons.filter, size: 18),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
MenuAnchor(
|
||||||
|
controller: viewModeMenuController,
|
||||||
|
menuChildren: TabsViewMode.values
|
||||||
|
.map(
|
||||||
|
(mode) => MenuItemButton(
|
||||||
|
leadingIcon: Icon(mode.icon),
|
||||||
|
child: Text(mode.label),
|
||||||
|
onPressed: () {
|
||||||
|
if (mode == TabsViewMode.tree) {
|
||||||
|
searchTextController.clear();
|
||||||
|
searchMode.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
ref
|
ref
|
||||||
.read(
|
.read(
|
||||||
tabsViewModeControllerProvider.notifier,
|
tabsViewModeControllerProvider.notifier,
|
||||||
)
|
)
|
||||||
.set(TabsViewMode.list);
|
.set(mode);
|
||||||
}
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
child: IconButton(
|
||||||
|
tooltip: 'Change view mode',
|
||||||
|
onPressed: isSyncedScope
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
if (viewModeMenuController.isOpen) {
|
||||||
|
viewModeMenuController.close();
|
||||||
|
} else {
|
||||||
|
viewModeMenuController.open();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(tabsViewMode.icon, size: 18),
|
||||||
|
const Icon(Icons.arrow_drop_down, size: 18),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (enableAiFeatures &&
|
||||||
|
switch (tabsViewMode) {
|
||||||
|
TabsViewMode.list || TabsViewMode.grid => true,
|
||||||
|
TabsViewMode.tree => false,
|
||||||
|
})
|
||||||
|
Consumer(
|
||||||
|
builder: (context, ref, child) {
|
||||||
|
final tabSuggestionsEnabled = ref.watch(
|
||||||
|
persistedBoolProvider(
|
||||||
|
PersistedBoolKey.tabSuggestions,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final downloadProgress = ref.watch(
|
||||||
|
mlDownloadStateProvider,
|
||||||
|
);
|
||||||
|
|
||||||
searchMode.value = true;
|
return Badge(
|
||||||
searchTextFocus.requestFocus();
|
isLabelVisible: downloadProgress != null,
|
||||||
},
|
offset: const Offset(-2, 2),
|
||||||
),
|
label: downloadProgress != null
|
||||||
const SizedBox(
|
? Text(
|
||||||
height: 32,
|
'${downloadProgress.progress.toInt()}%',
|
||||||
child: VerticalDivider(indent: 4, endIndent: 4),
|
style: const TextStyle(fontSize: 10),
|
||||||
),
|
)
|
||||||
MenuAnchor(
|
: null,
|
||||||
controller: viewModeMenuController,
|
child: IconButton.filledTonal(
|
||||||
menuChildren: TabsViewMode.values
|
icon: const Icon(MdiIcons.imageAutoAdjust),
|
||||||
.map(
|
isSelected: tabSuggestionsEnabled,
|
||||||
(mode) => MenuItemButton(
|
iconSize: 18,
|
||||||
leadingIcon: Icon(mode.icon),
|
padding: EdgeInsets.zero,
|
||||||
child: Text(mode.label),
|
tooltip: downloadProgress != null
|
||||||
onPressed: () {
|
? 'Downloading AI models (${downloadProgress.progress.toInt()}%)'
|
||||||
|
: tabSuggestionsEnabled
|
||||||
|
? 'Disable AI tab suggestions'
|
||||||
|
: 'Enable AI tab suggestions',
|
||||||
|
onPressed: () async {
|
||||||
|
if (!tabSuggestionsEnabled) {
|
||||||
|
final result =
|
||||||
|
await showEnableAiTabSuggestionsDialog(
|
||||||
|
context,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result == true) {
|
||||||
ref
|
ref
|
||||||
.read(
|
.read(
|
||||||
tabsViewModeControllerProvider
|
persistedBoolProvider(
|
||||||
.notifier,
|
PersistedBoolKey.tabSuggestions,
|
||||||
|
).notifier,
|
||||||
)
|
)
|
||||||
.set(mode);
|
.set(true);
|
||||||
},
|
}
|
||||||
),
|
} else {
|
||||||
)
|
ref
|
||||||
.toList(),
|
.read(
|
||||||
child: IconButton(
|
persistedBoolProvider(
|
||||||
tooltip: 'Change view mode',
|
PersistedBoolKey.tabSuggestions,
|
||||||
onPressed: isSyncedScope
|
).notifier,
|
||||||
? null
|
|
||||||
: () {
|
|
||||||
if (viewModeMenuController.isOpen) {
|
|
||||||
viewModeMenuController.close();
|
|
||||||
} else {
|
|
||||||
viewModeMenuController.open();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
icon: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Icon(tabsViewMode.icon, size: 18),
|
|
||||||
const Icon(Icons.arrow_drop_down, size: 18),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (enableAiFeatures &&
|
|
||||||
switch (tabsViewMode) {
|
|
||||||
TabsViewMode.list || TabsViewMode.grid => true,
|
|
||||||
TabsViewMode.tree => false,
|
|
||||||
})
|
|
||||||
Consumer(
|
|
||||||
builder: (context, ref, child) {
|
|
||||||
final tabSuggestionsEnabled = ref.watch(
|
|
||||||
persistedBoolProvider(PersistedBoolKey.tabSuggestions),
|
|
||||||
);
|
|
||||||
final downloadProgress = ref.watch(
|
|
||||||
mlDownloadStateProvider,
|
|
||||||
);
|
|
||||||
|
|
||||||
return Badge(
|
|
||||||
isLabelVisible: downloadProgress != null,
|
|
||||||
offset: const Offset(-2, 2),
|
|
||||||
label: downloadProgress != null
|
|
||||||
? Text(
|
|
||||||
'${downloadProgress.progress.toInt()}%',
|
|
||||||
style: const TextStyle(fontSize: 10),
|
|
||||||
)
|
)
|
||||||
: null,
|
.set(false);
|
||||||
child: IconButton.filledTonal(
|
}
|
||||||
icon: const Icon(MdiIcons.imageAutoAdjust),
|
},
|
||||||
isSelected: tabSuggestionsEnabled,
|
),
|
||||||
iconSize: 18,
|
);
|
||||||
padding: EdgeInsets.zero,
|
},
|
||||||
tooltip: downloadProgress != null
|
),
|
||||||
? 'Downloading AI models (${downloadProgress.progress.toInt()}%)'
|
if (switch (tabsViewMode) {
|
||||||
: tabSuggestionsEnabled
|
TabsViewMode.list || TabsViewMode.grid => true,
|
||||||
? 'Disable AI tab suggestions'
|
TabsViewMode.tree => false,
|
||||||
: 'Enable AI tab suggestions',
|
})
|
||||||
onPressed: () async {
|
IconButton.filledTonal(
|
||||||
if (!tabSuggestionsEnabled) {
|
icon: const Icon(Icons.swap_vert),
|
||||||
|
isSelected: tabsReorderable,
|
||||||
|
iconSize: 18,
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
tooltip: tabsReorderable
|
||||||
|
? 'Disable reordering mode'
|
||||||
|
: canManualReorder
|
||||||
|
? 'Enable reordering mode'
|
||||||
|
: 'Reordering requires default manual mode',
|
||||||
|
onPressed: canManualReorder
|
||||||
|
? () {
|
||||||
|
final wasEnabled = tabsReorderable;
|
||||||
|
|
||||||
|
ref
|
||||||
|
.read(
|
||||||
|
tabsReorderableControllerProvider
|
||||||
|
.notifier,
|
||||||
|
)
|
||||||
|
.toggle();
|
||||||
|
|
||||||
|
// Show info when enabling reordering
|
||||||
|
if (!wasEnabled && context.mounted) {
|
||||||
|
ui_helper.showInfoMessage(
|
||||||
|
context,
|
||||||
|
'Drag and drop tabs to reorder them',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
MenuAnchor(
|
||||||
|
controller: tabsActionMenuController,
|
||||||
|
menuChildren: [
|
||||||
|
SubmenuButton(
|
||||||
|
leadingIcon: const Icon(MdiIcons.closeCircle),
|
||||||
|
menuChildren: [
|
||||||
|
MenuItemButton(
|
||||||
|
leadingIcon: const Icon(MdiIcons.closeCircle),
|
||||||
|
onPressed: isSyncedScope
|
||||||
|
? null
|
||||||
|
: () async {
|
||||||
final result =
|
final result =
|
||||||
await showEnableAiTabSuggestionsDialog(
|
await showCloseAllTabsDialog(context);
|
||||||
|
|
||||||
|
if (result == true) {
|
||||||
|
final count = await ref
|
||||||
|
.read(
|
||||||
|
tabDataRepositoryProvider
|
||||||
|
.notifier,
|
||||||
|
)
|
||||||
|
.closeContainerTabs(
|
||||||
|
selectedContainerId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
|
ui_helper.showTabUndoClose(
|
||||||
|
context,
|
||||||
|
ref
|
||||||
|
.read(
|
||||||
|
tabRepositoryProvider
|
||||||
|
.notifier,
|
||||||
|
)
|
||||||
|
.undoClose,
|
||||||
|
count: count.length,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: const Text('All Tabs'),
|
||||||
|
),
|
||||||
|
MenuItemButton(
|
||||||
|
leadingIcon: Icon(
|
||||||
|
MdiIcons.dominoMask,
|
||||||
|
color: AppColors.of(context).privateTabPurple,
|
||||||
|
),
|
||||||
|
onPressed: isSyncedScope
|
||||||
|
? null
|
||||||
|
: () async {
|
||||||
|
final result =
|
||||||
|
await showCloseAllPrivateTabsDialog(
|
||||||
context,
|
context,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (result == true) {
|
if (result == true) {
|
||||||
ref
|
final count = await ref
|
||||||
.read(
|
.read(
|
||||||
persistedBoolProvider(PersistedBoolKey.tabSuggestions)
|
tabDataRepositoryProvider
|
||||||
.notifier,
|
.notifier,
|
||||||
)
|
)
|
||||||
.set(true);
|
.closeContainerTabs(
|
||||||
}
|
selectedContainerId,
|
||||||
} else {
|
includeRegular: false,
|
||||||
ref
|
includeIsolated: false,
|
||||||
.read(
|
);
|
||||||
persistedBoolProvider(PersistedBoolKey.tabSuggestions)
|
|
||||||
.notifier,
|
|
||||||
)
|
|
||||||
.set(false);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
if (switch (tabsViewMode) {
|
|
||||||
TabsViewMode.list || TabsViewMode.grid => true,
|
|
||||||
TabsViewMode.tree => false,
|
|
||||||
})
|
|
||||||
Consumer(
|
|
||||||
builder: (context, ref, child) {
|
|
||||||
final tabsReorderabe = ref.watch(
|
|
||||||
tabsReorderableControllerProvider,
|
|
||||||
);
|
|
||||||
|
|
||||||
return IconButton.filledTonal(
|
if (context.mounted) {
|
||||||
icon: const Icon(Icons.swap_vert),
|
ui_helper.showTabUndoClose(
|
||||||
isSelected: tabsReorderabe,
|
|
||||||
iconSize: 18,
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
tooltip: tabsReorderabe
|
|
||||||
? 'Disable reordering mode'
|
|
||||||
: 'Enable reordering mode',
|
|
||||||
onPressed: () {
|
|
||||||
final wasEnabled = tabsReorderabe;
|
|
||||||
|
|
||||||
ref
|
|
||||||
.read(
|
|
||||||
tabsReorderableControllerProvider
|
|
||||||
.notifier,
|
|
||||||
)
|
|
||||||
.toggle();
|
|
||||||
|
|
||||||
// Show info when enabling reordering
|
|
||||||
if (!wasEnabled && context.mounted) {
|
|
||||||
ui_helper.showInfoMessage(
|
|
||||||
context,
|
|
||||||
'Drag and drop tabs to reorder them',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
MenuAnchor(
|
|
||||||
controller: tabsActionMenuController,
|
|
||||||
menuChildren: [
|
|
||||||
MenuItemButton(
|
|
||||||
leadingIcon: const Icon(MdiIcons.closeCircle),
|
|
||||||
onPressed: isSyncedScope
|
|
||||||
? null
|
|
||||||
: () async {
|
|
||||||
final result = await showCloseAllTabsDialog(
|
|
||||||
context,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result == true) {
|
|
||||||
final count = await ref
|
|
||||||
.read(
|
|
||||||
tabDataRepositoryProvider.notifier,
|
|
||||||
)
|
|
||||||
.closeContainerTabs(
|
|
||||||
selectedContainerId,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (context.mounted) {
|
|
||||||
ui_helper.showTabUndoClose(
|
|
||||||
context,
|
|
||||||
ref
|
|
||||||
.read(
|
|
||||||
tabRepositoryProvider.notifier,
|
|
||||||
)
|
|
||||||
.undoClose,
|
|
||||||
count: count.length,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: const Text('Close All Tabs'),
|
|
||||||
),
|
|
||||||
MenuItemButton(
|
|
||||||
leadingIcon: const Icon(MdiIcons.incognitoCircleOff),
|
|
||||||
onPressed: isSyncedScope
|
|
||||||
? null
|
|
||||||
: () async {
|
|
||||||
final result =
|
|
||||||
await showCloseAllPrivateTabsDialog(
|
|
||||||
context,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result == true) {
|
|
||||||
final count = await ref
|
|
||||||
.read(
|
|
||||||
tabDataRepositoryProvider.notifier,
|
|
||||||
)
|
|
||||||
.closeContainerTabs(
|
|
||||||
selectedContainerId,
|
|
||||||
includeRegular: false,
|
|
||||||
includeIsolated: false,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (context.mounted) {
|
|
||||||
ui_helper.showTabUndoClose(
|
|
||||||
context,
|
|
||||||
ref
|
|
||||||
.read(
|
|
||||||
tabRepositoryProvider.notifier,
|
|
||||||
)
|
|
||||||
.undoClose,
|
|
||||||
count: count.length,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: const Text('Close Private Tabs'),
|
|
||||||
),
|
|
||||||
if (showIsolatedTabUi)
|
|
||||||
MenuItemButton(
|
|
||||||
leadingIcon: Icon(
|
|
||||||
MdiIcons.snowflake,
|
|
||||||
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,
|
context,
|
||||||
groupCount:
|
ref
|
||||||
isolatedContextIds.length,
|
.read(
|
||||||
|
tabRepositoryProvider
|
||||||
|
.notifier,
|
||||||
|
)
|
||||||
|
.undoClose,
|
||||||
|
count: count.length,
|
||||||
);
|
);
|
||||||
if (!confirmed) return;
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
final count = await ref
|
child: const Text('Private Tabs'),
|
||||||
.read(
|
),
|
||||||
tabDataRepositoryProvider.notifier,
|
if (showIsolatedTabUi)
|
||||||
)
|
MenuItemButton(
|
||||||
.closeContainerTabs(
|
leadingIcon: Icon(
|
||||||
selectedContainerId,
|
MdiIcons.snowflake,
|
||||||
includeRegular: false,
|
color: AppColors.of(context).isolatedTabTeal,
|
||||||
includePrivate: false,
|
),
|
||||||
|
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 (context.mounted) {
|
if (isolatedContextIds.isNotEmpty &&
|
||||||
ui_helper.showTabUndoClose(
|
context.mounted) {
|
||||||
context,
|
final confirmed = await ui_helper
|
||||||
ref
|
.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('Isolated Tabs'),
|
||||||
|
),
|
||||||
|
if (tabsViewMode != TabsViewMode.tree)
|
||||||
|
MenuItemButton(
|
||||||
|
leadingIcon: const Icon(MdiIcons.filterOutline),
|
||||||
|
onPressed:
|
||||||
|
isSyncedScope ||
|
||||||
|
!ref
|
||||||
|
.read(
|
||||||
|
tabViewFilterControllerProvider,
|
||||||
|
)
|
||||||
|
.hasActiveFilter
|
||||||
|
? null
|
||||||
|
: () async {
|
||||||
|
final filteredIds = await ref
|
||||||
|
.read(
|
||||||
|
tabDataRepositoryProvider
|
||||||
|
.notifier,
|
||||||
|
)
|
||||||
|
.getFilteredTabIds(
|
||||||
|
selectedContainerId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (filteredIds.isEmpty) return;
|
||||||
|
|
||||||
|
// Check for isolated tabs
|
||||||
|
final allStates = ref.read(
|
||||||
|
tabStatesProvider,
|
||||||
|
);
|
||||||
|
final isolatedContextIds = filteredIds
|
||||||
|
.map((id) => allStates[id])
|
||||||
|
.where(
|
||||||
|
(s) =>
|
||||||
|
s != null &&
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
await ref
|
||||||
.read(
|
.read(
|
||||||
tabRepositoryProvider.notifier,
|
tabRepositoryProvider.notifier,
|
||||||
)
|
)
|
||||||
.undoClose,
|
.closeTabs(filteredIds);
|
||||||
count: count.length,
|
|
||||||
);
|
if (context.mounted) {
|
||||||
}
|
ui_helper.showTabUndoClose(
|
||||||
},
|
context,
|
||||||
child: const Text('Close Isolated Tabs'),
|
ref
|
||||||
),
|
.read(
|
||||||
const Divider(),
|
tabRepositoryProvider
|
||||||
|
.notifier,
|
||||||
|
)
|
||||||
|
.undoClose,
|
||||||
|
count: filteredIds.length,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: const Text('Filtered Tabs'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
child: const Text('Close Tabs'),
|
||||||
|
),
|
||||||
MenuItemButton(
|
MenuItemButton(
|
||||||
leadingIcon: const Icon(MdiIcons.bookmarkPlusOutline),
|
leadingIcon: const Icon(MdiIcons.bookmarkPlusOutline),
|
||||||
onPressed: isSyncedScope
|
onPressed: isSyncedScope
|
||||||
@@ -799,11 +1129,10 @@ class TabViewHeader extends HookConsumerWidget {
|
|||||||
TextField(
|
TextField(
|
||||||
controller: searchTextController,
|
controller: searchTextController,
|
||||||
focusNode: searchTextFocus,
|
focusNode: searchTextFocus,
|
||||||
// enableIMEPersonalizedLearning: !incognitoEnabled,
|
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
// border: InputBorder.none,
|
border: InputBorder.none,
|
||||||
prefixIcon: const Icon(MdiIcons.tabSearch, size: 18),
|
prefixIcon: const Icon(MdiIcons.tabSearch, size: 18),
|
||||||
hintText: 'Search inside tabs...',
|
hintText: 'Search tabs',
|
||||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||||
suffixIcon: Row(
|
suffixIcon: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
@@ -816,9 +1145,12 @@ class TabViewHeader extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
searchTextController.clear();
|
if (searchTextController.text.isNotEmpty) {
|
||||||
searchTextFocus.requestFocus();
|
searchTextController.clear();
|
||||||
searchMode.value = false;
|
searchTextFocus.requestFocus();
|
||||||
|
} else {
|
||||||
|
searchMode.value = false;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.clear),
|
icon: const Icon(Icons.clear),
|
||||||
),
|
),
|
||||||
@@ -826,6 +1158,7 @@ class TabViewHeader extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
const Divider(),
|
||||||
if (showContainerUi)
|
if (showContainerUi)
|
||||||
Consumer(
|
Consumer(
|
||||||
builder: (context, ref, child) {
|
builder: (context, ref, child) {
|
||||||
|
|||||||
+8
-2
@@ -58,7 +58,9 @@ class FullSearchTermSuggestions extends HookConsumerWidget {
|
|||||||
|
|
||||||
final searchSuggestions = ref.watch(searchSuggestionsProvider());
|
final searchSuggestions = ref.watch(searchSuggestionsProvider());
|
||||||
final searchHistory = ref.watch(searchHistoryProvider);
|
final searchHistory = ref.watch(searchHistoryProvider);
|
||||||
final expanded = ref.watch(persistedBoolProvider(PersistedBoolKey.searchSuggestionsExpanded));
|
final expanded = ref.watch(
|
||||||
|
persistedBoolProvider(PersistedBoolKey.searchSuggestionsExpanded),
|
||||||
|
);
|
||||||
|
|
||||||
useOnListenableChange(searchTextController, () {
|
useOnListenableChange(searchTextController, () {
|
||||||
ref
|
ref
|
||||||
@@ -111,7 +113,11 @@ class FullSearchTermSuggestions extends HookConsumerWidget {
|
|||||||
padding: const EdgeInsets.only(top: 6.0),
|
padding: const EdgeInsets.only(top: 6.0),
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
onPressed: ref
|
onPressed: ref
|
||||||
.read(persistedBoolProvider(PersistedBoolKey.searchSuggestionsExpanded).notifier)
|
.read(
|
||||||
|
persistedBoolProvider(
|
||||||
|
PersistedBoolKey.searchSuggestionsExpanded,
|
||||||
|
).notifier,
|
||||||
|
)
|
||||||
.toggle,
|
.toggle,
|
||||||
icon: Icon(expanded ? Icons.unfold_less : Icons.unfold_more),
|
icon: Icon(expanded ? Icons.unfold_less : Icons.unfold_more),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -128,6 +128,14 @@ class TabDao extends DatabaseAccessor<TabDatabase> with $TabDaoMixin {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Selectable<MapEntry<String, String>> getTabOrderKeys() {
|
||||||
|
final query = selectOnly(db.tab)..addColumns([db.tab.id, db.tab.orderKey]);
|
||||||
|
|
||||||
|
return query.map(
|
||||||
|
(row) => MapEntry(row.read(db.tab.id)!, row.read(db.tab.orderKey)!),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Future<String> _generateOrderKey({
|
Future<String> _generateOrderKey({
|
||||||
required Value<String?> parentId,
|
required Value<String?> parentId,
|
||||||
required Value<String?> containerId,
|
required Value<String?> containerId,
|
||||||
@@ -463,6 +471,27 @@ class TabDao extends DatabaseAccessor<TabDatabase> with $TabDaoMixin {
|
|||||||
return db.definitionsDrift.isolatedContextContainerPairs();
|
return db.definitionsDrift.isolatedContextContainerPairs();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> setPinned(String id, {required bool pinned}) {
|
||||||
|
final statement = _updateByIdStatement(id);
|
||||||
|
return statement.write(TabCompanion(isPinned: Value(pinned)));
|
||||||
|
}
|
||||||
|
|
||||||
|
Selectable<String> getPinnedTabIds() {
|
||||||
|
final query = selectOnly(db.tab)
|
||||||
|
..addColumns([db.tab.id])
|
||||||
|
..where(db.tab.isPinned.equals(true));
|
||||||
|
|
||||||
|
return query.map((row) => row.read(db.tab.id)!);
|
||||||
|
}
|
||||||
|
|
||||||
|
Selectable<MapEntry<String, DateTime>> getTabTimestamps() {
|
||||||
|
final query = selectOnly(db.tab)..addColumns([db.tab.id, db.tab.timestamp]);
|
||||||
|
|
||||||
|
return query.map(
|
||||||
|
(row) => MapEntry(row.read(db.tab.id)!, row.read(db.tab.timestamp)!),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
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 = 6;
|
final int schemaVersion = 7;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
final int ftsTokenLimit = 10;
|
final int ftsTokenLimit = 10;
|
||||||
@@ -115,5 +115,8 @@ class TabDatabase extends $TabDatabase with TrigramQueryBuilderMixin {
|
|||||||
|
|
||||||
await m.alterTable(TableMigration(schema.tab));
|
await m.alterTable(TableMigration(schema.tab));
|
||||||
},
|
},
|
||||||
|
from6To7: (m, schema) async {
|
||||||
|
await m.addColumn(schema.tab, schema.tab.isPinned);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -594,11 +594,140 @@ i1.GeneratedColumn<String> _column_18(String aliasedName) =>
|
|||||||
type: i1.DriftSqlType.string,
|
type: i1.DriftSqlType.string,
|
||||||
$customConstraints: '',
|
$customConstraints: '',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final class Schema7 extends i0.VersionedSchema {
|
||||||
|
Schema7({required super.database}) : super(version: 7);
|
||||||
|
@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 Shape5 tab = Shape5(
|
||||||
|
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_19,
|
||||||
|
_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 Shape5 extends i0.VersionedTable {
|
||||||
|
Shape5({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 isPinned =>
|
||||||
|
columnsByName['is_pinned']! as i1.GeneratedColumn<int>;
|
||||||
|
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_19(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<int>(
|
||||||
|
'is_pinned',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i1.DriftSqlType.int,
|
||||||
|
$customConstraints: 'NOT NULL DEFAULT 0',
|
||||||
|
defaultValue: const i1.CustomExpression('0'),
|
||||||
|
);
|
||||||
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,
|
required Future<void> Function(i1.Migrator m, Schema6 schema) from5To6,
|
||||||
|
required Future<void> Function(i1.Migrator m, Schema7 schema) from6To7,
|
||||||
}) {
|
}) {
|
||||||
return (currentVersion, database) async {
|
return (currentVersion, database) async {
|
||||||
switch (currentVersion) {
|
switch (currentVersion) {
|
||||||
@@ -622,6 +751,11 @@ i0.MigrationStepWithVersion migrationSteps({
|
|||||||
final migrator = i1.Migrator(database, schema);
|
final migrator = i1.Migrator(database, schema);
|
||||||
await from5To6(migrator, schema);
|
await from5To6(migrator, schema);
|
||||||
return 6;
|
return 6;
|
||||||
|
case 6:
|
||||||
|
final schema = Schema7(database: database);
|
||||||
|
final migrator = i1.Migrator(database, schema);
|
||||||
|
await from6To7(migrator, schema);
|
||||||
|
return 7;
|
||||||
default:
|
default:
|
||||||
throw ArgumentError.value('Unknown migration from $currentVersion');
|
throw ArgumentError.value('Unknown migration from $currentVersion');
|
||||||
}
|
}
|
||||||
@@ -633,11 +767,13 @@ i1.OnUpgrade stepByStep({
|
|||||||
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,
|
required Future<void> Function(i1.Migrator m, Schema6 schema) from5To6,
|
||||||
|
required Future<void> Function(i1.Migrator m, Schema7 schema) from6To7,
|
||||||
}) => i0.VersionedSchema.stepByStepHelper(
|
}) => i0.VersionedSchema.stepByStepHelper(
|
||||||
step: migrationSteps(
|
step: migrationSteps(
|
||||||
from2To3: from2To3,
|
from2To3: from2To3,
|
||||||
from3To4: from3To4,
|
from3To4: from3To4,
|
||||||
from4To5: from4To5,
|
from4To5: from4To5,
|
||||||
from5To6: from5To6,
|
from5To6: from5To6,
|
||||||
|
from6To7: from6To7,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ CREATE TABLE tab(
|
|||||||
title TEXT,
|
title TEXT,
|
||||||
tab_mode ENUM(TabModeDbValue) NOT NULL DEFAULT 0,
|
tab_mode ENUM(TabModeDbValue) NOT NULL DEFAULT 0,
|
||||||
isolation_context_id TEXT,
|
isolation_context_id TEXT,
|
||||||
|
is_pinned BOOL NOT NULL DEFAULT 0,
|
||||||
is_probably_readerable BOOL,
|
is_probably_readerable BOOL,
|
||||||
extracted_content_markdown TEXT,
|
extracted_content_markdown TEXT,
|
||||||
extracted_content_plain TEXT,
|
extracted_content_plain TEXT,
|
||||||
|
|||||||
@@ -333,6 +333,7 @@ typedef $TabCreateCompanionBuilder =
|
|||||||
i0.Value<String?> title,
|
i0.Value<String?> title,
|
||||||
i0.Value<i7.TabModeDbValue> tabMode,
|
i0.Value<i7.TabModeDbValue> tabMode,
|
||||||
i0.Value<String?> isolationContextId,
|
i0.Value<String?> isolationContextId,
|
||||||
|
i0.Value<bool> isPinned,
|
||||||
i0.Value<bool?> isProbablyReaderable,
|
i0.Value<bool?> isProbablyReaderable,
|
||||||
i0.Value<String?> extractedContentMarkdown,
|
i0.Value<String?> extractedContentMarkdown,
|
||||||
i0.Value<String?> extractedContentPlain,
|
i0.Value<String?> extractedContentPlain,
|
||||||
@@ -352,6 +353,7 @@ typedef $TabUpdateCompanionBuilder =
|
|||||||
i0.Value<String?> title,
|
i0.Value<String?> title,
|
||||||
i0.Value<i7.TabModeDbValue> tabMode,
|
i0.Value<i7.TabModeDbValue> tabMode,
|
||||||
i0.Value<String?> isolationContextId,
|
i0.Value<String?> isolationContextId,
|
||||||
|
i0.Value<bool> isPinned,
|
||||||
i0.Value<bool?> isProbablyReaderable,
|
i0.Value<bool?> isProbablyReaderable,
|
||||||
i0.Value<String?> extractedContentMarkdown,
|
i0.Value<String?> extractedContentMarkdown,
|
||||||
i0.Value<String?> extractedContentPlain,
|
i0.Value<String?> extractedContentPlain,
|
||||||
@@ -445,6 +447,11 @@ class $TabFilterComposer extends i0.Composer<i0.GeneratedDatabase, i3.Tab> {
|
|||||||
builder: (column) => i0.ColumnFilters(column),
|
builder: (column) => i0.ColumnFilters(column),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
i0.ColumnFilters<bool> get isPinned => $composableBuilder(
|
||||||
|
column: $table.isPinned,
|
||||||
|
builder: (column) => i0.ColumnFilters(column),
|
||||||
|
);
|
||||||
|
|
||||||
i0.ColumnFilters<bool> get isProbablyReaderable => $composableBuilder(
|
i0.ColumnFilters<bool> get isProbablyReaderable => $composableBuilder(
|
||||||
column: $table.isProbablyReaderable,
|
column: $table.isProbablyReaderable,
|
||||||
builder: (column) => i0.ColumnFilters(column),
|
builder: (column) => i0.ColumnFilters(column),
|
||||||
@@ -551,6 +558,11 @@ class $TabOrderingComposer extends i0.Composer<i0.GeneratedDatabase, i3.Tab> {
|
|||||||
builder: (column) => i0.ColumnOrderings(column),
|
builder: (column) => i0.ColumnOrderings(column),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
i0.ColumnOrderings<bool> get isPinned => $composableBuilder(
|
||||||
|
column: $table.isPinned,
|
||||||
|
builder: (column) => i0.ColumnOrderings(column),
|
||||||
|
);
|
||||||
|
|
||||||
i0.ColumnOrderings<bool> get isProbablyReaderable => $composableBuilder(
|
i0.ColumnOrderings<bool> get isProbablyReaderable => $composableBuilder(
|
||||||
column: $table.isProbablyReaderable,
|
column: $table.isProbablyReaderable,
|
||||||
builder: (column) => i0.ColumnOrderings(column),
|
builder: (column) => i0.ColumnOrderings(column),
|
||||||
@@ -643,6 +655,9 @@ class $TabAnnotationComposer extends i0.Composer<i0.GeneratedDatabase, i3.Tab> {
|
|||||||
builder: (column) => column,
|
builder: (column) => column,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
i0.GeneratedColumn<bool> get isPinned =>
|
||||||
|
$composableBuilder(column: $table.isPinned, builder: (column) => column);
|
||||||
|
|
||||||
i0.GeneratedColumn<bool> get isProbablyReaderable => $composableBuilder(
|
i0.GeneratedColumn<bool> get isProbablyReaderable => $composableBuilder(
|
||||||
column: $table.isProbablyReaderable,
|
column: $table.isProbablyReaderable,
|
||||||
builder: (column) => column,
|
builder: (column) => column,
|
||||||
@@ -736,6 +751,7 @@ class $TabTableManager
|
|||||||
i0.Value<String?> title = const i0.Value.absent(),
|
i0.Value<String?> title = const i0.Value.absent(),
|
||||||
i0.Value<i7.TabModeDbValue> tabMode = const i0.Value.absent(),
|
i0.Value<i7.TabModeDbValue> tabMode = const i0.Value.absent(),
|
||||||
i0.Value<String?> isolationContextId = const i0.Value.absent(),
|
i0.Value<String?> isolationContextId = const i0.Value.absent(),
|
||||||
|
i0.Value<bool> isPinned = 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(),
|
||||||
@@ -755,6 +771,7 @@ class $TabTableManager
|
|||||||
title: title,
|
title: title,
|
||||||
tabMode: tabMode,
|
tabMode: tabMode,
|
||||||
isolationContextId: isolationContextId,
|
isolationContextId: isolationContextId,
|
||||||
|
isPinned: isPinned,
|
||||||
isProbablyReaderable: isProbablyReaderable,
|
isProbablyReaderable: isProbablyReaderable,
|
||||||
extractedContentMarkdown: extractedContentMarkdown,
|
extractedContentMarkdown: extractedContentMarkdown,
|
||||||
extractedContentPlain: extractedContentPlain,
|
extractedContentPlain: extractedContentPlain,
|
||||||
@@ -774,6 +791,7 @@ class $TabTableManager
|
|||||||
i0.Value<String?> title = const i0.Value.absent(),
|
i0.Value<String?> title = const i0.Value.absent(),
|
||||||
i0.Value<i7.TabModeDbValue> tabMode = const i0.Value.absent(),
|
i0.Value<i7.TabModeDbValue> tabMode = const i0.Value.absent(),
|
||||||
i0.Value<String?> isolationContextId = const i0.Value.absent(),
|
i0.Value<String?> isolationContextId = const i0.Value.absent(),
|
||||||
|
i0.Value<bool> isPinned = 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(),
|
||||||
@@ -793,6 +811,7 @@ class $TabTableManager
|
|||||||
title: title,
|
title: title,
|
||||||
tabMode: tabMode,
|
tabMode: tabMode,
|
||||||
isolationContextId: isolationContextId,
|
isolationContextId: isolationContextId,
|
||||||
|
isPinned: isPinned,
|
||||||
isProbablyReaderable: isProbablyReaderable,
|
isProbablyReaderable: isProbablyReaderable,
|
||||||
extractedContentMarkdown: extractedContentMarkdown,
|
extractedContentMarkdown: extractedContentMarkdown,
|
||||||
extractedContentPlain: extractedContentPlain,
|
extractedContentPlain: extractedContentPlain,
|
||||||
@@ -1312,6 +1331,15 @@ class Tab extends i0.Table with i0.TableInfo<Tab, i3.TabData> {
|
|||||||
requiredDuringInsert: false,
|
requiredDuringInsert: false,
|
||||||
$customConstraints: '',
|
$customConstraints: '',
|
||||||
);
|
);
|
||||||
|
late final i0.GeneratedColumn<bool> isPinned = i0.GeneratedColumn<bool>(
|
||||||
|
'is_pinned',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i0.DriftSqlType.bool,
|
||||||
|
requiredDuringInsert: false,
|
||||||
|
$customConstraints: 'NOT NULL DEFAULT 0',
|
||||||
|
defaultValue: const i0.CustomExpression('0'),
|
||||||
|
);
|
||||||
late final i0.GeneratedColumn<bool> isProbablyReaderable =
|
late final i0.GeneratedColumn<bool> isProbablyReaderable =
|
||||||
i0.GeneratedColumn<bool>(
|
i0.GeneratedColumn<bool>(
|
||||||
'is_probably_readerable',
|
'is_probably_readerable',
|
||||||
@@ -1377,6 +1405,7 @@ class Tab extends i0.Table with i0.TableInfo<Tab, i3.TabData> {
|
|||||||
title,
|
title,
|
||||||
tabMode,
|
tabMode,
|
||||||
isolationContextId,
|
isolationContextId,
|
||||||
|
isPinned,
|
||||||
isProbablyReaderable,
|
isProbablyReaderable,
|
||||||
extractedContentMarkdown,
|
extractedContentMarkdown,
|
||||||
extractedContentPlain,
|
extractedContentPlain,
|
||||||
@@ -1437,6 +1466,10 @@ class Tab extends i0.Table with i0.TableInfo<Tab, i3.TabData> {
|
|||||||
i0.DriftSqlType.string,
|
i0.DriftSqlType.string,
|
||||||
data['${effectivePrefix}isolation_context_id'],
|
data['${effectivePrefix}isolation_context_id'],
|
||||||
),
|
),
|
||||||
|
isPinned: attachedDatabase.typeMapping.read(
|
||||||
|
i0.DriftSqlType.bool,
|
||||||
|
data['${effectivePrefix}is_pinned'],
|
||||||
|
)!,
|
||||||
isProbablyReaderable: attachedDatabase.typeMapping.read(
|
isProbablyReaderable: attachedDatabase.typeMapping.read(
|
||||||
i0.DriftSqlType.bool,
|
i0.DriftSqlType.bool,
|
||||||
data['${effectivePrefix}is_probably_readerable'],
|
data['${effectivePrefix}is_probably_readerable'],
|
||||||
@@ -1493,6 +1526,7 @@ class TabData extends i0.DataClass implements i0.Insertable<i3.TabData> {
|
|||||||
final String? title;
|
final String? title;
|
||||||
final i7.TabModeDbValue tabMode;
|
final i7.TabModeDbValue tabMode;
|
||||||
final String? isolationContextId;
|
final String? isolationContextId;
|
||||||
|
final bool isPinned;
|
||||||
final bool? isProbablyReaderable;
|
final bool? isProbablyReaderable;
|
||||||
final String? extractedContentMarkdown;
|
final String? extractedContentMarkdown;
|
||||||
final String? extractedContentPlain;
|
final String? extractedContentPlain;
|
||||||
@@ -1509,6 +1543,7 @@ class TabData extends i0.DataClass implements i0.Insertable<i3.TabData> {
|
|||||||
this.title,
|
this.title,
|
||||||
required this.tabMode,
|
required this.tabMode,
|
||||||
this.isolationContextId,
|
this.isolationContextId,
|
||||||
|
required this.isPinned,
|
||||||
this.isProbablyReaderable,
|
this.isProbablyReaderable,
|
||||||
this.extractedContentMarkdown,
|
this.extractedContentMarkdown,
|
||||||
this.extractedContentPlain,
|
this.extractedContentPlain,
|
||||||
@@ -1544,6 +1579,7 @@ class TabData extends i0.DataClass implements i0.Insertable<i3.TabData> {
|
|||||||
if (!nullToAbsent || isolationContextId != null) {
|
if (!nullToAbsent || isolationContextId != null) {
|
||||||
map['isolation_context_id'] = i0.Variable<String>(isolationContextId);
|
map['isolation_context_id'] = i0.Variable<String>(isolationContextId);
|
||||||
}
|
}
|
||||||
|
map['is_pinned'] = i0.Variable<bool>(isPinned);
|
||||||
if (!nullToAbsent || isProbablyReaderable != null) {
|
if (!nullToAbsent || isProbablyReaderable != null) {
|
||||||
map['is_probably_readerable'] = i0.Variable<bool>(isProbablyReaderable);
|
map['is_probably_readerable'] = i0.Variable<bool>(isProbablyReaderable);
|
||||||
}
|
}
|
||||||
@@ -1588,6 +1624,7 @@ class TabData extends i0.DataClass implements i0.Insertable<i3.TabData> {
|
|||||||
isolationContextId: serializer.fromJson<String?>(
|
isolationContextId: serializer.fromJson<String?>(
|
||||||
json['isolation_context_id'],
|
json['isolation_context_id'],
|
||||||
),
|
),
|
||||||
|
isPinned: serializer.fromJson<bool>(json['is_pinned']),
|
||||||
isProbablyReaderable: serializer.fromJson<bool?>(
|
isProbablyReaderable: serializer.fromJson<bool?>(
|
||||||
json['is_probably_readerable'],
|
json['is_probably_readerable'],
|
||||||
),
|
),
|
||||||
@@ -1621,6 +1658,7 @@ class TabData extends i0.DataClass implements i0.Insertable<i3.TabData> {
|
|||||||
i3.Tab.$convertertabMode.toJson(tabMode),
|
i3.Tab.$convertertabMode.toJson(tabMode),
|
||||||
),
|
),
|
||||||
'isolation_context_id': serializer.toJson<String?>(isolationContextId),
|
'isolation_context_id': serializer.toJson<String?>(isolationContextId),
|
||||||
|
'is_pinned': serializer.toJson<bool>(isPinned),
|
||||||
'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,
|
||||||
@@ -1644,6 +1682,7 @@ class TabData extends i0.DataClass implements i0.Insertable<i3.TabData> {
|
|||||||
i0.Value<String?> title = const i0.Value.absent(),
|
i0.Value<String?> title = const i0.Value.absent(),
|
||||||
i7.TabModeDbValue? tabMode,
|
i7.TabModeDbValue? tabMode,
|
||||||
i0.Value<String?> isolationContextId = const i0.Value.absent(),
|
i0.Value<String?> isolationContextId = const i0.Value.absent(),
|
||||||
|
bool? isPinned,
|
||||||
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(),
|
||||||
@@ -1662,6 +1701,7 @@ class TabData extends i0.DataClass implements i0.Insertable<i3.TabData> {
|
|||||||
isolationContextId: isolationContextId.present
|
isolationContextId: isolationContextId.present
|
||||||
? isolationContextId.value
|
? isolationContextId.value
|
||||||
: this.isolationContextId,
|
: this.isolationContextId,
|
||||||
|
isPinned: isPinned ?? this.isPinned,
|
||||||
isProbablyReaderable: isProbablyReaderable.present
|
isProbablyReaderable: isProbablyReaderable.present
|
||||||
? isProbablyReaderable.value
|
? isProbablyReaderable.value
|
||||||
: this.isProbablyReaderable,
|
: this.isProbablyReaderable,
|
||||||
@@ -1694,6 +1734,7 @@ class TabData extends i0.DataClass implements i0.Insertable<i3.TabData> {
|
|||||||
isolationContextId: data.isolationContextId.present
|
isolationContextId: data.isolationContextId.present
|
||||||
? data.isolationContextId.value
|
? data.isolationContextId.value
|
||||||
: this.isolationContextId,
|
: this.isolationContextId,
|
||||||
|
isPinned: data.isPinned.present ? data.isPinned.value : this.isPinned,
|
||||||
isProbablyReaderable: data.isProbablyReaderable.present
|
isProbablyReaderable: data.isProbablyReaderable.present
|
||||||
? data.isProbablyReaderable.value
|
? data.isProbablyReaderable.value
|
||||||
: this.isProbablyReaderable,
|
: this.isProbablyReaderable,
|
||||||
@@ -1725,6 +1766,7 @@ class TabData extends i0.DataClass implements i0.Insertable<i3.TabData> {
|
|||||||
..write('title: $title, ')
|
..write('title: $title, ')
|
||||||
..write('tabMode: $tabMode, ')
|
..write('tabMode: $tabMode, ')
|
||||||
..write('isolationContextId: $isolationContextId, ')
|
..write('isolationContextId: $isolationContextId, ')
|
||||||
|
..write('isPinned: $isPinned, ')
|
||||||
..write('isProbablyReaderable: $isProbablyReaderable, ')
|
..write('isProbablyReaderable: $isProbablyReaderable, ')
|
||||||
..write('extractedContentMarkdown: $extractedContentMarkdown, ')
|
..write('extractedContentMarkdown: $extractedContentMarkdown, ')
|
||||||
..write('extractedContentPlain: $extractedContentPlain, ')
|
..write('extractedContentPlain: $extractedContentPlain, ')
|
||||||
@@ -1746,6 +1788,7 @@ class TabData extends i0.DataClass implements i0.Insertable<i3.TabData> {
|
|||||||
title,
|
title,
|
||||||
tabMode,
|
tabMode,
|
||||||
isolationContextId,
|
isolationContextId,
|
||||||
|
isPinned,
|
||||||
isProbablyReaderable,
|
isProbablyReaderable,
|
||||||
extractedContentMarkdown,
|
extractedContentMarkdown,
|
||||||
extractedContentPlain,
|
extractedContentPlain,
|
||||||
@@ -1766,6 +1809,7 @@ class TabData extends i0.DataClass implements i0.Insertable<i3.TabData> {
|
|||||||
other.title == this.title &&
|
other.title == this.title &&
|
||||||
other.tabMode == this.tabMode &&
|
other.tabMode == this.tabMode &&
|
||||||
other.isolationContextId == this.isolationContextId &&
|
other.isolationContextId == this.isolationContextId &&
|
||||||
|
other.isPinned == this.isPinned &&
|
||||||
other.isProbablyReaderable == this.isProbablyReaderable &&
|
other.isProbablyReaderable == this.isProbablyReaderable &&
|
||||||
other.extractedContentMarkdown == this.extractedContentMarkdown &&
|
other.extractedContentMarkdown == this.extractedContentMarkdown &&
|
||||||
other.extractedContentPlain == this.extractedContentPlain &&
|
other.extractedContentPlain == this.extractedContentPlain &&
|
||||||
@@ -1784,6 +1828,7 @@ class TabCompanion extends i0.UpdateCompanion<i3.TabData> {
|
|||||||
final i0.Value<String?> title;
|
final i0.Value<String?> title;
|
||||||
final i0.Value<i7.TabModeDbValue> tabMode;
|
final i0.Value<i7.TabModeDbValue> tabMode;
|
||||||
final i0.Value<String?> isolationContextId;
|
final i0.Value<String?> isolationContextId;
|
||||||
|
final i0.Value<bool> isPinned;
|
||||||
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;
|
||||||
@@ -1801,6 +1846,7 @@ class TabCompanion extends i0.UpdateCompanion<i3.TabData> {
|
|||||||
this.title = const i0.Value.absent(),
|
this.title = const i0.Value.absent(),
|
||||||
this.tabMode = const i0.Value.absent(),
|
this.tabMode = const i0.Value.absent(),
|
||||||
this.isolationContextId = const i0.Value.absent(),
|
this.isolationContextId = const i0.Value.absent(),
|
||||||
|
this.isPinned = 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(),
|
||||||
@@ -1819,6 +1865,7 @@ class TabCompanion extends i0.UpdateCompanion<i3.TabData> {
|
|||||||
this.title = const i0.Value.absent(),
|
this.title = const i0.Value.absent(),
|
||||||
this.tabMode = const i0.Value.absent(),
|
this.tabMode = const i0.Value.absent(),
|
||||||
this.isolationContextId = const i0.Value.absent(),
|
this.isolationContextId = const i0.Value.absent(),
|
||||||
|
this.isPinned = 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(),
|
||||||
@@ -1840,6 +1887,7 @@ class TabCompanion extends i0.UpdateCompanion<i3.TabData> {
|
|||||||
i0.Expression<String>? title,
|
i0.Expression<String>? title,
|
||||||
i0.Expression<int>? tabMode,
|
i0.Expression<int>? tabMode,
|
||||||
i0.Expression<String>? isolationContextId,
|
i0.Expression<String>? isolationContextId,
|
||||||
|
i0.Expression<bool>? isPinned,
|
||||||
i0.Expression<bool>? isProbablyReaderable,
|
i0.Expression<bool>? isProbablyReaderable,
|
||||||
i0.Expression<String>? extractedContentMarkdown,
|
i0.Expression<String>? extractedContentMarkdown,
|
||||||
i0.Expression<String>? extractedContentPlain,
|
i0.Expression<String>? extractedContentPlain,
|
||||||
@@ -1859,6 +1907,7 @@ class TabCompanion extends i0.UpdateCompanion<i3.TabData> {
|
|||||||
if (tabMode != null) 'tab_mode': tabMode,
|
if (tabMode != null) 'tab_mode': tabMode,
|
||||||
if (isolationContextId != null)
|
if (isolationContextId != null)
|
||||||
'isolation_context_id': isolationContextId,
|
'isolation_context_id': isolationContextId,
|
||||||
|
if (isPinned != null) 'is_pinned': isPinned,
|
||||||
if (isProbablyReaderable != null)
|
if (isProbablyReaderable != null)
|
||||||
'is_probably_readerable': isProbablyReaderable,
|
'is_probably_readerable': isProbablyReaderable,
|
||||||
if (extractedContentMarkdown != null)
|
if (extractedContentMarkdown != null)
|
||||||
@@ -1883,6 +1932,7 @@ class TabCompanion extends i0.UpdateCompanion<i3.TabData> {
|
|||||||
i0.Value<String?>? title,
|
i0.Value<String?>? title,
|
||||||
i0.Value<i7.TabModeDbValue>? tabMode,
|
i0.Value<i7.TabModeDbValue>? tabMode,
|
||||||
i0.Value<String?>? isolationContextId,
|
i0.Value<String?>? isolationContextId,
|
||||||
|
i0.Value<bool>? isPinned,
|
||||||
i0.Value<bool?>? isProbablyReaderable,
|
i0.Value<bool?>? isProbablyReaderable,
|
||||||
i0.Value<String?>? extractedContentMarkdown,
|
i0.Value<String?>? extractedContentMarkdown,
|
||||||
i0.Value<String?>? extractedContentPlain,
|
i0.Value<String?>? extractedContentPlain,
|
||||||
@@ -1901,6 +1951,7 @@ class TabCompanion extends i0.UpdateCompanion<i3.TabData> {
|
|||||||
title: title ?? this.title,
|
title: title ?? this.title,
|
||||||
tabMode: tabMode ?? this.tabMode,
|
tabMode: tabMode ?? this.tabMode,
|
||||||
isolationContextId: isolationContextId ?? this.isolationContextId,
|
isolationContextId: isolationContextId ?? this.isolationContextId,
|
||||||
|
isPinned: isPinned ?? this.isPinned,
|
||||||
isProbablyReaderable: isProbablyReaderable ?? this.isProbablyReaderable,
|
isProbablyReaderable: isProbablyReaderable ?? this.isProbablyReaderable,
|
||||||
extractedContentMarkdown:
|
extractedContentMarkdown:
|
||||||
extractedContentMarkdown ?? this.extractedContentMarkdown,
|
extractedContentMarkdown ?? this.extractedContentMarkdown,
|
||||||
@@ -1949,6 +2000,9 @@ class TabCompanion extends i0.UpdateCompanion<i3.TabData> {
|
|||||||
isolationContextId.value,
|
isolationContextId.value,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (isPinned.present) {
|
||||||
|
map['is_pinned'] = i0.Variable<bool>(isPinned.value);
|
||||||
|
}
|
||||||
if (isProbablyReaderable.present) {
|
if (isProbablyReaderable.present) {
|
||||||
map['is_probably_readerable'] = i0.Variable<bool>(
|
map['is_probably_readerable'] = i0.Variable<bool>(
|
||||||
isProbablyReaderable.value,
|
isProbablyReaderable.value,
|
||||||
@@ -1993,6 +2047,7 @@ class TabCompanion extends i0.UpdateCompanion<i3.TabData> {
|
|||||||
..write('title: $title, ')
|
..write('title: $title, ')
|
||||||
..write('tabMode: $tabMode, ')
|
..write('tabMode: $tabMode, ')
|
||||||
..write('isolationContextId: $isolationContextId, ')
|
..write('isolationContextId: $isolationContextId, ')
|
||||||
|
..write('isPinned: $isPinned, ')
|
||||||
..write('isProbablyReaderable: $isProbablyReaderable, ')
|
..write('isProbablyReaderable: $isProbablyReaderable, ')
|
||||||
..write('extractedContentMarkdown: $extractedContentMarkdown, ')
|
..write('extractedContentMarkdown: $extractedContentMarkdown, ')
|
||||||
..write('extractedContentPlain: $extractedContentPlain, ')
|
..write('extractedContentPlain: $extractedContentPlain, ')
|
||||||
|
|||||||
@@ -21,39 +21,56 @@ import 'package:fast_equatable/fast_equatable.dart';
|
|||||||
|
|
||||||
sealed class TabEntity with FastEquatable {
|
sealed class TabEntity with FastEquatable {
|
||||||
String get tabId;
|
String get tabId;
|
||||||
|
String get orderKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
class DefaultTabEntity extends TabEntity {
|
class DefaultTabEntity extends TabEntity {
|
||||||
@override
|
@override
|
||||||
final String tabId;
|
final String tabId;
|
||||||
|
@override
|
||||||
|
final String orderKey;
|
||||||
final String? containerId;
|
final String? containerId;
|
||||||
|
|
||||||
DefaultTabEntity({required this.tabId, required this.containerId});
|
DefaultTabEntity({
|
||||||
|
required this.tabId,
|
||||||
|
required this.orderKey,
|
||||||
|
required this.containerId,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get hashParameters => [tabId, containerId];
|
List<Object?> get hashParameters => [tabId, orderKey, containerId];
|
||||||
}
|
}
|
||||||
|
|
||||||
class SearchResultTabEntity extends TabEntity {
|
class SearchResultTabEntity extends TabEntity {
|
||||||
@override
|
@override
|
||||||
final String tabId;
|
final String tabId;
|
||||||
|
@override
|
||||||
|
final String orderKey;
|
||||||
final String? containerId;
|
final String? containerId;
|
||||||
|
|
||||||
final String searchQuery;
|
final String searchQuery;
|
||||||
|
|
||||||
SearchResultTabEntity({
|
SearchResultTabEntity({
|
||||||
required this.tabId,
|
required this.tabId,
|
||||||
|
required this.orderKey,
|
||||||
required this.searchQuery,
|
required this.searchQuery,
|
||||||
required this.containerId,
|
required this.containerId,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get hashParameters => [tabId, searchQuery, containerId];
|
List<Object?> get hashParameters => [
|
||||||
|
tabId,
|
||||||
|
orderKey,
|
||||||
|
searchQuery,
|
||||||
|
containerId,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
class TabTreeEntity extends TabEntity {
|
class TabTreeEntity extends TabEntity {
|
||||||
@override
|
@override
|
||||||
final String tabId;
|
final String tabId;
|
||||||
|
@override
|
||||||
|
final String orderKey;
|
||||||
|
|
||||||
final String? containerId;
|
final String? containerId;
|
||||||
|
|
||||||
@@ -63,11 +80,18 @@ class TabTreeEntity extends TabEntity {
|
|||||||
|
|
||||||
TabTreeEntity({
|
TabTreeEntity({
|
||||||
required this.tabId,
|
required this.tabId,
|
||||||
|
required this.orderKey,
|
||||||
required this.containerId,
|
required this.containerId,
|
||||||
required this.rootId,
|
required this.rootId,
|
||||||
required this.totalTabs,
|
required this.totalTabs,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get hashParameters => [tabId, containerId, rootId, totalTabs];
|
List<Object?> get hashParameters => [
|
||||||
|
tabId,
|
||||||
|
orderKey,
|
||||||
|
containerId,
|
||||||
|
rootId,
|
||||||
|
totalTabs,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,6 +115,24 @@ Stream<List<TabData>> watchContainerTabsData(Ref ref, String? containerId) {
|
|||||||
return db.containerDao.getContainerTabsData(containerId).watch();
|
return db.containerDao.getContainerTabsData(containerId).watch();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Riverpod(keepAlive: true)
|
||||||
|
Stream<Set<String>> watchPinnedTabIds(Ref ref) {
|
||||||
|
final db = ref.watch(tabDatabaseProvider);
|
||||||
|
return db.tabDao.getPinnedTabIds().watch().map((ids) => ids.toSet());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Riverpod()
|
||||||
|
Stream<Map<String, DateTime>> watchTabTimestamps(Ref ref) {
|
||||||
|
final db = ref.watch(tabDatabaseProvider);
|
||||||
|
return db.tabDao.getTabTimestamps().watch().map(Map.fromEntries);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Riverpod(keepAlive: true)
|
||||||
|
Stream<Map<String, String>> watchTabOrderKeys(Ref ref) {
|
||||||
|
final db = ref.watch(tabDatabaseProvider);
|
||||||
|
return db.tabDao.getTabOrderKeys().watch().map(Map.fromEntries);
|
||||||
|
}
|
||||||
|
|
||||||
@Riverpod()
|
@Riverpod()
|
||||||
Stream<ContainerData?> watchContainerData(Ref ref, String containerId) {
|
Stream<ContainerData?> watchContainerData(Ref ref, String containerId) {
|
||||||
final db = ref.watch(tabDatabaseProvider);
|
final db = ref.watch(tabDatabaseProvider);
|
||||||
|
|||||||
@@ -525,6 +525,128 @@ final class WatchContainerTabsDataFamily extends $Family
|
|||||||
String toString() => r'watchContainerTabsDataProvider';
|
String toString() => r'watchContainerTabsDataProvider';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ProviderFor(watchPinnedTabIds)
|
||||||
|
final watchPinnedTabIdsProvider = WatchPinnedTabIdsProvider._();
|
||||||
|
|
||||||
|
final class WatchPinnedTabIdsProvider
|
||||||
|
extends
|
||||||
|
$FunctionalProvider<
|
||||||
|
AsyncValue<Set<String>>,
|
||||||
|
Set<String>,
|
||||||
|
Stream<Set<String>>
|
||||||
|
>
|
||||||
|
with $FutureModifier<Set<String>>, $StreamProvider<Set<String>> {
|
||||||
|
WatchPinnedTabIdsProvider._()
|
||||||
|
: super(
|
||||||
|
from: null,
|
||||||
|
argument: null,
|
||||||
|
retry: null,
|
||||||
|
name: r'watchPinnedTabIdsProvider',
|
||||||
|
isAutoDispose: false,
|
||||||
|
dependencies: null,
|
||||||
|
$allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String debugGetCreateSourceHash() => _$watchPinnedTabIdsHash();
|
||||||
|
|
||||||
|
@$internal
|
||||||
|
@override
|
||||||
|
$StreamProviderElement<Set<String>> $createElement(
|
||||||
|
$ProviderPointer pointer,
|
||||||
|
) => $StreamProviderElement(pointer);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Stream<Set<String>> create(Ref ref) {
|
||||||
|
return watchPinnedTabIds(ref);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$watchPinnedTabIdsHash() => r'5623faf1a4d90185c718654f4172092e28dd1e54';
|
||||||
|
|
||||||
|
@ProviderFor(watchTabTimestamps)
|
||||||
|
final watchTabTimestampsProvider = WatchTabTimestampsProvider._();
|
||||||
|
|
||||||
|
final class WatchTabTimestampsProvider
|
||||||
|
extends
|
||||||
|
$FunctionalProvider<
|
||||||
|
AsyncValue<Map<String, DateTime>>,
|
||||||
|
Map<String, DateTime>,
|
||||||
|
Stream<Map<String, DateTime>>
|
||||||
|
>
|
||||||
|
with
|
||||||
|
$FutureModifier<Map<String, DateTime>>,
|
||||||
|
$StreamProvider<Map<String, DateTime>> {
|
||||||
|
WatchTabTimestampsProvider._()
|
||||||
|
: super(
|
||||||
|
from: null,
|
||||||
|
argument: null,
|
||||||
|
retry: null,
|
||||||
|
name: r'watchTabTimestampsProvider',
|
||||||
|
isAutoDispose: true,
|
||||||
|
dependencies: null,
|
||||||
|
$allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String debugGetCreateSourceHash() => _$watchTabTimestampsHash();
|
||||||
|
|
||||||
|
@$internal
|
||||||
|
@override
|
||||||
|
$StreamProviderElement<Map<String, DateTime>> $createElement(
|
||||||
|
$ProviderPointer pointer,
|
||||||
|
) => $StreamProviderElement(pointer);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Stream<Map<String, DateTime>> create(Ref ref) {
|
||||||
|
return watchTabTimestamps(ref);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$watchTabTimestampsHash() =>
|
||||||
|
r'8b3eea3dded71795f80c607117978bbec1de7cff';
|
||||||
|
|
||||||
|
@ProviderFor(watchTabOrderKeys)
|
||||||
|
final watchTabOrderKeysProvider = WatchTabOrderKeysProvider._();
|
||||||
|
|
||||||
|
final class WatchTabOrderKeysProvider
|
||||||
|
extends
|
||||||
|
$FunctionalProvider<
|
||||||
|
AsyncValue<Map<String, String>>,
|
||||||
|
Map<String, String>,
|
||||||
|
Stream<Map<String, String>>
|
||||||
|
>
|
||||||
|
with
|
||||||
|
$FutureModifier<Map<String, String>>,
|
||||||
|
$StreamProvider<Map<String, String>> {
|
||||||
|
WatchTabOrderKeysProvider._()
|
||||||
|
: super(
|
||||||
|
from: null,
|
||||||
|
argument: null,
|
||||||
|
retry: null,
|
||||||
|
name: r'watchTabOrderKeysProvider',
|
||||||
|
isAutoDispose: false,
|
||||||
|
dependencies: null,
|
||||||
|
$allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String debugGetCreateSourceHash() => _$watchTabOrderKeysHash();
|
||||||
|
|
||||||
|
@$internal
|
||||||
|
@override
|
||||||
|
$StreamProviderElement<Map<String, String>> $createElement(
|
||||||
|
$ProviderPointer pointer,
|
||||||
|
) => $StreamProviderElement(pointer);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Stream<Map<String, String>> create(Ref ref) {
|
||||||
|
return watchTabOrderKeys(ref);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$watchTabOrderKeysHash() => r'675ef0d1b1c5445face8d6ab727507ca1a5e2c44';
|
||||||
|
|
||||||
@ProviderFor(watchContainerData)
|
@ProviderFor(watchContainerData)
|
||||||
final watchContainerDataProvider = WatchContainerDataFamily._();
|
final watchContainerDataProvider = WatchContainerDataFamily._();
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import 'package:weblibre/features/geckoview/domain/entities/tab_container_select
|
|||||||
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_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/browser/presentation/controllers/tab_view_controllers.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_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';
|
||||||
@@ -114,6 +115,13 @@ class TabDataRepository extends _$TabDataRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> setPinned(String tabId, {required bool pinned}) {
|
||||||
|
return ref
|
||||||
|
.read(tabDatabaseProvider)
|
||||||
|
.tabDao
|
||||||
|
.setPinned(tabId, pinned: pinned);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> assignOrderKey(String tabId, String orderKey) {
|
Future<void> assignOrderKey(String tabId, String orderKey) {
|
||||||
return ref
|
return ref
|
||||||
.read(tabDatabaseProvider)
|
.read(tabDatabaseProvider)
|
||||||
@@ -235,6 +243,36 @@ class TabDataRepository extends _$TabDataRepository {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<List<String>> getFilteredTabIds(String? containerId) async {
|
||||||
|
final db = ref.read(tabDatabaseProvider);
|
||||||
|
final filterOptions = ref.read(tabViewFilterControllerProvider);
|
||||||
|
final tabStates = ref.read(tabStatesProvider);
|
||||||
|
|
||||||
|
// Get all tab IDs in this container from DB
|
||||||
|
final containerTabIds = await db.containerDao
|
||||||
|
.getContainerTabIds(containerId)
|
||||||
|
.get();
|
||||||
|
|
||||||
|
// Only keep tabs that exist in the engine
|
||||||
|
final candidateIds = containerTabIds
|
||||||
|
.where((id) => tabStates.containsKey(id))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
if (candidateIds.isEmpty) return const [];
|
||||||
|
|
||||||
|
if (!filterOptions.hasActiveFilter) return candidateIds;
|
||||||
|
|
||||||
|
// Only fetch timestamps when date filtering is active
|
||||||
|
final timestamps = filterOptions.effectiveDateRange != null
|
||||||
|
? Map.fromEntries(await db.tabDao.getTabTimestamps().get())
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return candidateIds.where((id) {
|
||||||
|
final state = tabStates[id];
|
||||||
|
return filterOptions.matchesTab(state?.tabMode, timestamps?[id]);
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
Future<int> deleteUnassignedTabsOlderThan(DateTime threshold) async {
|
Future<int> deleteUnassignedTabsOlderThan(DateTime threshold) async {
|
||||||
final tabIds = await ref
|
final tabIds = await ref
|
||||||
.read(tabDatabaseProvider)
|
.read(tabDatabaseProvider)
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ final class TabDataRepositoryProvider
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$tabDataRepositoryHash() => r'41ca88333684db71f809e35874b0e5bdddde0f60';
|
String _$tabDataRepositoryHash() => r'c64a44c7d9de566617798a0a3bdcad51bf436658';
|
||||||
|
|
||||||
abstract class _$TabDataRepository extends $Notifier<void> {
|
abstract class _$TabDataRepository extends $Notifier<void> {
|
||||||
void build();
|
void build();
|
||||||
|
|||||||
@@ -109,7 +109,9 @@ class _ContainerSuggestionsChip extends ConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final tabSuggestionsEnabled = ref.watch(persistedBoolProvider(PersistedBoolKey.tabSuggestions));
|
final tabSuggestionsEnabled = ref.watch(
|
||||||
|
persistedBoolProvider(PersistedBoolKey.tabSuggestions),
|
||||||
|
);
|
||||||
final enableAiFeatures = ref.watch(
|
final enableAiFeatures = ref.watch(
|
||||||
generalSettingsWithDefaultsProvider.select(
|
generalSettingsWithDefaultsProvider.select(
|
||||||
(settings) => settings.enableLocalAiFeatures,
|
(settings) => settings.enableLocalAiFeatures,
|
||||||
@@ -306,7 +308,7 @@ class ContainerChips extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
if (displayMenu)
|
if (displayMenu)
|
||||||
IconButton(
|
IconButton(
|
||||||
visualDensity: VisualDensity.compact,
|
// visualDensity: VisualDensity.compact,
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await const ContainerListRoute().push(context);
|
await const ContainerListRoute().push(context);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -508,7 +508,6 @@ class _PullToRefreshTile extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class _DoubleBackCloseTabTile extends HookConsumerWidget {
|
class _DoubleBackCloseTabTile extends HookConsumerWidget {
|
||||||
const _DoubleBackCloseTabTile();
|
const _DoubleBackCloseTabTile();
|
||||||
|
|
||||||
|
|||||||
@@ -67,9 +67,11 @@ void showInfoMessage(
|
|||||||
String message, {
|
String message, {
|
||||||
Duration duration = const Duration(seconds: 4),
|
Duration duration = const Duration(seconds: 4),
|
||||||
bool persist = false,
|
bool persist = false,
|
||||||
|
SnackBarAction? action,
|
||||||
}) {
|
}) {
|
||||||
final snackBar = _createFloatingSnackBar(
|
final snackBar = _createFloatingSnackBar(
|
||||||
content: Text(message),
|
content: Text(message),
|
||||||
|
action: action,
|
||||||
duration: duration,
|
duration: duration,
|
||||||
persist: persist,
|
persist: persist,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ 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;
|
import 'schema_v6.dart' as v6;
|
||||||
|
import 'schema_v7.dart' as v7;
|
||||||
|
|
||||||
class GeneratedHelper implements SchemaInstantiationHelper {
|
class GeneratedHelper implements SchemaInstantiationHelper {
|
||||||
@override
|
@override
|
||||||
@@ -24,10 +25,12 @@ class GeneratedHelper implements SchemaInstantiationHelper {
|
|||||||
return v5.DatabaseAtV5(db);
|
return v5.DatabaseAtV5(db);
|
||||||
case 6:
|
case 6:
|
||||||
return v6.DatabaseAtV6(db);
|
return v6.DatabaseAtV6(db);
|
||||||
|
case 7:
|
||||||
|
return v7.DatabaseAtV7(db);
|
||||||
default:
|
default:
|
||||||
throw MissingSchemaException(version, versions);
|
throw MissingSchemaException(version, versions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static const versions = const [2, 3, 4, 5, 6];
|
static const versions = const [2, 3, 4, 5, 6, 7];
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user