added feature to configure contextual tab bar buttons
This commit is contained in:
@@ -0,0 +1,320 @@
|
|||||||
|
{
|
||||||
|
"_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": "setting",
|
||||||
|
"was_declared_in_moor": true,
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "key",
|
||||||
|
"getter_name": "key",
|
||||||
|
"moor_type": "string",
|
||||||
|
"nullable": false,
|
||||||
|
"customConstraints": "PRIMARY KEY NOT NULL",
|
||||||
|
"default_dart": null,
|
||||||
|
"default_client_dart": null,
|
||||||
|
"dsl_features": [
|
||||||
|
"primary-key"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "partition_key",
|
||||||
|
"getter_name": "partitionKey",
|
||||||
|
"moor_type": "string",
|
||||||
|
"nullable": true,
|
||||||
|
"customConstraints": "",
|
||||||
|
"default_dart": null,
|
||||||
|
"default_client_dart": null,
|
||||||
|
"dsl_features": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "value",
|
||||||
|
"getter_name": "value",
|
||||||
|
"moor_type": "any",
|
||||||
|
"nullable": true,
|
||||||
|
"customConstraints": "",
|
||||||
|
"default_dart": null,
|
||||||
|
"default_client_dart": null,
|
||||||
|
"dsl_features": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_virtual": false,
|
||||||
|
"without_rowid": false,
|
||||||
|
"constraints": [],
|
||||||
|
"strict": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"references": [],
|
||||||
|
"type": "table",
|
||||||
|
"data": {
|
||||||
|
"name": "icon_cache",
|
||||||
|
"was_declared_in_moor": true,
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "origin",
|
||||||
|
"getter_name": "origin",
|
||||||
|
"moor_type": "string",
|
||||||
|
"nullable": false,
|
||||||
|
"customConstraints": "PRIMARY KEY NOT NULL",
|
||||||
|
"default_dart": null,
|
||||||
|
"default_client_dart": null,
|
||||||
|
"dsl_features": [
|
||||||
|
"primary-key"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "icon_data",
|
||||||
|
"getter_name": "iconData",
|
||||||
|
"moor_type": "blob",
|
||||||
|
"nullable": false,
|
||||||
|
"customConstraints": "NOT NULL",
|
||||||
|
"default_dart": null,
|
||||||
|
"default_client_dart": null,
|
||||||
|
"dsl_features": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "fetch_date",
|
||||||
|
"getter_name": "fetchDate",
|
||||||
|
"moor_type": "dateTime",
|
||||||
|
"nullable": false,
|
||||||
|
"customConstraints": "NOT NULL",
|
||||||
|
"default_dart": null,
|
||||||
|
"default_client_dart": null,
|
||||||
|
"dsl_features": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_virtual": false,
|
||||||
|
"without_rowid": false,
|
||||||
|
"constraints": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"references": [],
|
||||||
|
"type": "table",
|
||||||
|
"data": {
|
||||||
|
"name": "onboarding",
|
||||||
|
"was_declared_in_moor": true,
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "revision",
|
||||||
|
"getter_name": "revision",
|
||||||
|
"moor_type": "int",
|
||||||
|
"nullable": false,
|
||||||
|
"customConstraints": "NOT NULL",
|
||||||
|
"default_dart": null,
|
||||||
|
"default_client_dart": null,
|
||||||
|
"dsl_features": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "completion_date",
|
||||||
|
"getter_name": "completionDate",
|
||||||
|
"moor_type": "dateTime",
|
||||||
|
"nullable": false,
|
||||||
|
"customConstraints": "NOT NULL",
|
||||||
|
"default_dart": null,
|
||||||
|
"default_client_dart": null,
|
||||||
|
"dsl_features": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_virtual": false,
|
||||||
|
"without_rowid": false,
|
||||||
|
"constraints": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"references": [],
|
||||||
|
"type": "table",
|
||||||
|
"data": {
|
||||||
|
"name": "riverpod",
|
||||||
|
"was_declared_in_moor": true,
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "key",
|
||||||
|
"getter_name": "key",
|
||||||
|
"moor_type": "string",
|
||||||
|
"nullable": false,
|
||||||
|
"customConstraints": "PRIMARY KEY NOT NULL",
|
||||||
|
"default_dart": null,
|
||||||
|
"default_client_dart": null,
|
||||||
|
"dsl_features": [
|
||||||
|
"primary-key"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "json",
|
||||||
|
"getter_name": "json",
|
||||||
|
"moor_type": "string",
|
||||||
|
"nullable": false,
|
||||||
|
"customConstraints": "NOT NULL",
|
||||||
|
"default_dart": null,
|
||||||
|
"default_client_dart": null,
|
||||||
|
"dsl_features": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "expireAt",
|
||||||
|
"getter_name": "expireAt",
|
||||||
|
"moor_type": "dateTime",
|
||||||
|
"nullable": true,
|
||||||
|
"customConstraints": "",
|
||||||
|
"default_dart": null,
|
||||||
|
"default_client_dart": null,
|
||||||
|
"dsl_features": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "destroyKey",
|
||||||
|
"getter_name": "destroyKey",
|
||||||
|
"moor_type": "string",
|
||||||
|
"nullable": true,
|
||||||
|
"customConstraints": "",
|
||||||
|
"default_dart": null,
|
||||||
|
"default_client_dart": null,
|
||||||
|
"dsl_features": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_virtual": false,
|
||||||
|
"without_rowid": true,
|
||||||
|
"constraints": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"references": [],
|
||||||
|
"type": "table",
|
||||||
|
"data": {
|
||||||
|
"name": "toolbar_button_configs",
|
||||||
|
"was_declared_in_moor": true,
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "button_id",
|
||||||
|
"getter_name": "buttonId",
|
||||||
|
"moor_type": "string",
|
||||||
|
"nullable": false,
|
||||||
|
"customConstraints": "NOT NULL PRIMARY KEY",
|
||||||
|
"default_dart": null,
|
||||||
|
"default_client_dart": null,
|
||||||
|
"dsl_features": [
|
||||||
|
"primary-key"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "order_key",
|
||||||
|
"getter_name": "orderKey",
|
||||||
|
"moor_type": "string",
|
||||||
|
"nullable": false,
|
||||||
|
"customConstraints": "NOT NULL",
|
||||||
|
"default_dart": null,
|
||||||
|
"default_client_dart": null,
|
||||||
|
"dsl_features": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "is_visible",
|
||||||
|
"getter_name": "isVisible",
|
||||||
|
"moor_type": "bool",
|
||||||
|
"nullable": false,
|
||||||
|
"customConstraints": "NOT NULL DEFAULT TRUE",
|
||||||
|
"default_dart": "const CustomExpression('TRUE')",
|
||||||
|
"default_client_dart": null,
|
||||||
|
"dsl_features": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "fallback_id",
|
||||||
|
"getter_name": "fallbackId",
|
||||||
|
"moor_type": "string",
|
||||||
|
"nullable": true,
|
||||||
|
"customConstraints": "REFERENCES toolbar_button_configs(button_id)ON DELETE SET NULL",
|
||||||
|
"default_dart": null,
|
||||||
|
"default_client_dart": null,
|
||||||
|
"dsl_features": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_virtual": false,
|
||||||
|
"without_rowid": false,
|
||||||
|
"constraints": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"references": [
|
||||||
|
4
|
||||||
|
],
|
||||||
|
"type": "index",
|
||||||
|
"data": {
|
||||||
|
"on": 4,
|
||||||
|
"name": "idx_toolbar_order_key",
|
||||||
|
"sql": "CREATE INDEX idx_toolbar_order_key ON toolbar_button_configs(order_key);",
|
||||||
|
"unique": false,
|
||||||
|
"columns": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fixed_sql": [
|
||||||
|
{
|
||||||
|
"name": "setting",
|
||||||
|
"sql": [
|
||||||
|
{
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"sql": "CREATE TABLE IF NOT EXISTS \"setting\" (\"key\" TEXT PRIMARY KEY NOT NULL, \"partition_key\" TEXT, \"value\" ANY) STRICT;"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "icon_cache",
|
||||||
|
"sql": [
|
||||||
|
{
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"sql": "CREATE TABLE IF NOT EXISTS \"icon_cache\" (\"origin\" TEXT PRIMARY KEY NOT NULL, \"icon_data\" BLOB NOT NULL, \"fetch_date\" INTEGER NOT NULL);"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "onboarding",
|
||||||
|
"sql": [
|
||||||
|
{
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"sql": "CREATE TABLE IF NOT EXISTS \"onboarding\" (\"revision\" INTEGER NOT NULL, \"completion_date\" INTEGER NOT NULL);"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "riverpod",
|
||||||
|
"sql": [
|
||||||
|
{
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"sql": "CREATE TABLE IF NOT EXISTS \"riverpod\" (\"key\" TEXT PRIMARY KEY NOT NULL, \"json\" TEXT NOT NULL, \"expireAt\" INTEGER, \"destroyKey\" TEXT) WITHOUT ROWID;"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "toolbar_button_configs",
|
||||||
|
"sql": [
|
||||||
|
{
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"sql": "CREATE TABLE IF NOT EXISTS \"toolbar_button_configs\" (\"button_id\" TEXT NOT NULL PRIMARY KEY, \"order_key\" TEXT NOT NULL, \"is_visible\" INTEGER NOT NULL DEFAULT TRUE, \"fallback_id\" TEXT REFERENCES toolbar_button_configs(button_id)ON DELETE SET NULL);"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "idx_toolbar_order_key",
|
||||||
|
"sql": [
|
||||||
|
{
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"sql": "CREATE INDEX idx_toolbar_order_key ON toolbar_button_configs (order_key)"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -60,6 +60,7 @@ import 'package:weblibre/features/settings/presentation/screens/addon_collection
|
|||||||
import 'package:weblibre/features/settings/presentation/screens/advanced_settings.dart';
|
import 'package:weblibre/features/settings/presentation/screens/advanced_settings.dart';
|
||||||
import 'package:weblibre/features/settings/presentation/screens/appearance_display_settings.dart';
|
import 'package:weblibre/features/settings/presentation/screens/appearance_display_settings.dart';
|
||||||
import 'package:weblibre/features/settings/presentation/screens/bang_settings.dart';
|
import 'package:weblibre/features/settings/presentation/screens/bang_settings.dart';
|
||||||
|
import 'package:weblibre/features/settings/presentation/screens/contextual_toolbar_settings.dart';
|
||||||
import 'package:weblibre/features/settings/presentation/screens/custom_tracking_protection.dart';
|
import 'package:weblibre/features/settings/presentation/screens/custom_tracking_protection.dart';
|
||||||
import 'package:weblibre/features/settings/presentation/screens/doh_settings.dart';
|
import 'package:weblibre/features/settings/presentation/screens/doh_settings.dart';
|
||||||
import 'package:weblibre/features/settings/presentation/screens/error_logs_screen.dart';
|
import 'package:weblibre/features/settings/presentation/screens/error_logs_screen.dart';
|
||||||
|
|||||||
@@ -1476,6 +1476,11 @@ RouteBase get $settingsRoute => GoRouteData.$route(
|
|||||||
name: 'UnshortenerSettingsRoute',
|
name: 'UnshortenerSettingsRoute',
|
||||||
factory: $UnshortenerSettingsRoute._fromState,
|
factory: $UnshortenerSettingsRoute._fromState,
|
||||||
),
|
),
|
||||||
|
GoRouteData.$route(
|
||||||
|
path: 'contextual_toolbar',
|
||||||
|
name: 'ContextualToolbarSettingsRoute',
|
||||||
|
factory: $ContextualToolbarSettingsRoute._fromState,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1924,6 +1929,27 @@ mixin $UnshortenerSettingsRoute on GoRouteData {
|
|||||||
void replace(BuildContext context) => context.replace(location);
|
void replace(BuildContext context) => context.replace(location);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mixin $ContextualToolbarSettingsRoute on GoRouteData {
|
||||||
|
static ContextualToolbarSettingsRoute _fromState(GoRouterState state) =>
|
||||||
|
const ContextualToolbarSettingsRoute();
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get location => GoRouteData.$location('/settings/contextual_toolbar');
|
||||||
|
|
||||||
|
@override
|
||||||
|
void go(BuildContext context) => context.go(location);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<T?> push<T>(BuildContext context) => context.push<T>(location);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void pushReplacement(BuildContext context) =>
|
||||||
|
context.pushReplacement(location);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void replace(BuildContext context) => context.replace(location);
|
||||||
|
}
|
||||||
|
|
||||||
RouteBase get $torProxyRoute => GoRouteData.$route(
|
RouteBase get $torProxyRoute => GoRouteData.$route(
|
||||||
path: '/tor',
|
path: '/tor',
|
||||||
name: 'TorProxyRoute',
|
name: 'TorProxyRoute',
|
||||||
|
|||||||
@@ -93,6 +93,10 @@ part of 'routes.dart';
|
|||||||
name: 'UnshortenerSettingsRoute',
|
name: 'UnshortenerSettingsRoute',
|
||||||
path: 'unshortener',
|
path: 'unshortener',
|
||||||
),
|
),
|
||||||
|
TypedGoRoute<ContextualToolbarSettingsRoute>(
|
||||||
|
name: 'ContextualToolbarSettingsRoute',
|
||||||
|
path: 'contextual_toolbar',
|
||||||
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
class SettingsRoute extends GoRouteData with $SettingsRoute {
|
class SettingsRoute extends GoRouteData with $SettingsRoute {
|
||||||
@@ -257,3 +261,13 @@ class UnshortenerSettingsRoute extends GoRouteData
|
|||||||
return const UnshortenerSettingsScreen();
|
return const UnshortenerSettingsScreen();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ContextualToolbarSettingsRoute extends GoRouteData
|
||||||
|
with $ContextualToolbarSettingsRoute {
|
||||||
|
const ContextualToolbarSettingsRoute();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, GoRouterState state) {
|
||||||
|
return const ContextualToolbarSettingsScreen();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+82
@@ -0,0 +1,82 @@
|
|||||||
|
/*
|
||||||
|
* 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:fast_equatable/fast_equatable.dart';
|
||||||
|
import 'package:lexo_rank/lexo_rank.dart';
|
||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/browser/features/contextual_toolbar/data/repositories/contextual_toolbar_config_repository.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/browser/features/contextual_toolbar/domain/entities/toolbar_button_spec.dart';
|
||||||
|
import 'package:weblibre/features/user/data/database/definitions.drift.dart'
|
||||||
|
show ToolbarButtonConfig;
|
||||||
|
|
||||||
|
part 'toolbar_button_configs.g.dart';
|
||||||
|
|
||||||
|
List<ToolbarButtonConfig> _buildDefaultToolbarButtonConfigs() {
|
||||||
|
String? lastKey;
|
||||||
|
|
||||||
|
return toolbarButtonSpecs.map((spec) {
|
||||||
|
final key = lastKey == null
|
||||||
|
? LexoRank.middle().value
|
||||||
|
: LexoRank.parse(lastKey!).genNext().value;
|
||||||
|
|
||||||
|
lastKey = key;
|
||||||
|
|
||||||
|
return ToolbarButtonConfig(
|
||||||
|
buttonId: spec.id.name,
|
||||||
|
orderKey: key,
|
||||||
|
isVisible: spec.defaultVisible,
|
||||||
|
fallbackId: spec.defaultFallback?.name,
|
||||||
|
);
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
final defaultToolbarButtonConfigs = EquatableValue(
|
||||||
|
_buildDefaultToolbarButtonConfigs(),
|
||||||
|
);
|
||||||
|
|
||||||
|
@Riverpod(keepAlive: true)
|
||||||
|
Stream<List<ToolbarButtonConfig>> toolbarButtonConfigs(Ref ref) async* {
|
||||||
|
final repository = ref.watch(contextualToolbarConfigRepositoryProvider);
|
||||||
|
await repository.seedMissingDefaults();
|
||||||
|
yield* repository.watchAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Riverpod(keepAlive: true)
|
||||||
|
EquatableValue<List<ToolbarButtonConfig>> effectiveToolbarButtonConfigs(
|
||||||
|
Ref ref,
|
||||||
|
) {
|
||||||
|
final configsAsync = ref.watch(toolbarButtonConfigsProvider);
|
||||||
|
|
||||||
|
return configsAsync.when(
|
||||||
|
data: (configs) {
|
||||||
|
final filtered = configs
|
||||||
|
.where((config) => knownToolbarButtonIds.contains(config.buttonId))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
if (filtered.isEmpty) {
|
||||||
|
return defaultToolbarButtonConfigs;
|
||||||
|
}
|
||||||
|
|
||||||
|
return EquatableValue(filtered);
|
||||||
|
},
|
||||||
|
loading: () => defaultToolbarButtonConfigs,
|
||||||
|
error: (_, _) => defaultToolbarButtonConfigs,
|
||||||
|
);
|
||||||
|
}
|
||||||
+102
@@ -0,0 +1,102 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'toolbar_button_configs.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// RiverpodGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
// ignore_for_file: type=lint, type=warning
|
||||||
|
|
||||||
|
@ProviderFor(toolbarButtonConfigs)
|
||||||
|
final toolbarButtonConfigsProvider = ToolbarButtonConfigsProvider._();
|
||||||
|
|
||||||
|
final class ToolbarButtonConfigsProvider
|
||||||
|
extends
|
||||||
|
$FunctionalProvider<
|
||||||
|
AsyncValue<List<ToolbarButtonConfig>>,
|
||||||
|
List<ToolbarButtonConfig>,
|
||||||
|
Stream<List<ToolbarButtonConfig>>
|
||||||
|
>
|
||||||
|
with
|
||||||
|
$FutureModifier<List<ToolbarButtonConfig>>,
|
||||||
|
$StreamProvider<List<ToolbarButtonConfig>> {
|
||||||
|
ToolbarButtonConfigsProvider._()
|
||||||
|
: super(
|
||||||
|
from: null,
|
||||||
|
argument: null,
|
||||||
|
retry: null,
|
||||||
|
name: r'toolbarButtonConfigsProvider',
|
||||||
|
isAutoDispose: false,
|
||||||
|
dependencies: null,
|
||||||
|
$allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String debugGetCreateSourceHash() => _$toolbarButtonConfigsHash();
|
||||||
|
|
||||||
|
@$internal
|
||||||
|
@override
|
||||||
|
$StreamProviderElement<List<ToolbarButtonConfig>> $createElement(
|
||||||
|
$ProviderPointer pointer,
|
||||||
|
) => $StreamProviderElement(pointer);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Stream<List<ToolbarButtonConfig>> create(Ref ref) {
|
||||||
|
return toolbarButtonConfigs(ref);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$toolbarButtonConfigsHash() =>
|
||||||
|
r'02f79c2087a3a5413fe879405c05f4a4d1eaffeb';
|
||||||
|
|
||||||
|
@ProviderFor(effectiveToolbarButtonConfigs)
|
||||||
|
final effectiveToolbarButtonConfigsProvider =
|
||||||
|
EffectiveToolbarButtonConfigsProvider._();
|
||||||
|
|
||||||
|
final class EffectiveToolbarButtonConfigsProvider
|
||||||
|
extends
|
||||||
|
$FunctionalProvider<
|
||||||
|
EquatableValue<List<ToolbarButtonConfig>>,
|
||||||
|
EquatableValue<List<ToolbarButtonConfig>>,
|
||||||
|
EquatableValue<List<ToolbarButtonConfig>>
|
||||||
|
>
|
||||||
|
with $Provider<EquatableValue<List<ToolbarButtonConfig>>> {
|
||||||
|
EffectiveToolbarButtonConfigsProvider._()
|
||||||
|
: super(
|
||||||
|
from: null,
|
||||||
|
argument: null,
|
||||||
|
retry: null,
|
||||||
|
name: r'effectiveToolbarButtonConfigsProvider',
|
||||||
|
isAutoDispose: false,
|
||||||
|
dependencies: null,
|
||||||
|
$allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String debugGetCreateSourceHash() => _$effectiveToolbarButtonConfigsHash();
|
||||||
|
|
||||||
|
@$internal
|
||||||
|
@override
|
||||||
|
$ProviderElement<EquatableValue<List<ToolbarButtonConfig>>> $createElement(
|
||||||
|
$ProviderPointer pointer,
|
||||||
|
) => $ProviderElement(pointer);
|
||||||
|
|
||||||
|
@override
|
||||||
|
EquatableValue<List<ToolbarButtonConfig>> create(Ref ref) {
|
||||||
|
return effectiveToolbarButtonConfigs(ref);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// {@macro riverpod.override_with_value}
|
||||||
|
Override overrideWithValue(EquatableValue<List<ToolbarButtonConfig>> value) {
|
||||||
|
return $ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
providerOverride:
|
||||||
|
$SyncValueProvider<EquatableValue<List<ToolbarButtonConfig>>>(value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$effectiveToolbarButtonConfigsHash() =>
|
||||||
|
r'dd876160f586c64237a42a29eb63cb119f962b02';
|
||||||
+89
@@ -0,0 +1,89 @@
|
|||||||
|
/*
|
||||||
|
* 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:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/browser/features/contextual_toolbar/domain/entities/toolbar_button_spec.dart';
|
||||||
|
import 'package:weblibre/features/user/data/database/daos/toolbar_button_config.dart';
|
||||||
|
import 'package:weblibre/features/user/data/database/definitions.drift.dart'
|
||||||
|
show ToolbarButtonConfig;
|
||||||
|
import 'package:weblibre/features/user/data/providers.dart';
|
||||||
|
|
||||||
|
part 'contextual_toolbar_config_repository.g.dart';
|
||||||
|
|
||||||
|
class ContextualToolbarConfigRepository {
|
||||||
|
ContextualToolbarConfigRepository(this._dao);
|
||||||
|
|
||||||
|
final ToolbarButtonConfigDao _dao;
|
||||||
|
|
||||||
|
Stream<List<ToolbarButtonConfig>> watchAll() => _dao.watchAll();
|
||||||
|
|
||||||
|
Future<void> replaceAll(List<ToolbarButtonConfig> configs) {
|
||||||
|
return _dao.replaceAll(configs);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> assignOrderKey(String buttonId, {required String orderKey}) {
|
||||||
|
return _dao.assignOrderKey(buttonId, orderKey: orderKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> assignVisibility(String buttonId, {required bool visible}) {
|
||||||
|
return _dao.assignVisibility(buttonId, visible: visible);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> assignFallback(String buttonId, String? fallbackId) {
|
||||||
|
return _dao.assignFallback(buttonId, fallbackId);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> generateLeadingOrderKey() {
|
||||||
|
return _dao.generateLeadingOrderKey().getSingle();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> generateTrailingOrderKey() {
|
||||||
|
return _dao.generateTrailingOrderKey().getSingle();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String?> generateOrderKeyAfterButtonId(String buttonId) {
|
||||||
|
return _dao.generateOrderKeyAfterButtonId(buttonId).getSingleOrNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> generateOrderKeyBeforeButtonId(String buttonId) {
|
||||||
|
return _dao.generateOrderKeyBeforeButtonId(buttonId).getSingle();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> seedMissingDefaults() {
|
||||||
|
return _dao.seedMissing(
|
||||||
|
toolbarButtonSpecs
|
||||||
|
.map(
|
||||||
|
(spec) => (
|
||||||
|
buttonId: spec.id.name,
|
||||||
|
defaultVisible: spec.defaultVisible,
|
||||||
|
defaultFallback: spec.defaultFallback?.name,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Riverpod(keepAlive: true)
|
||||||
|
ContextualToolbarConfigRepository contextualToolbarConfigRepository(Ref ref) {
|
||||||
|
return ContextualToolbarConfigRepository(
|
||||||
|
ref.watch(userDatabaseProvider).toolbarButtonConfigDao,
|
||||||
|
);
|
||||||
|
}
|
||||||
+62
@@ -0,0 +1,62 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'contextual_toolbar_config_repository.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// RiverpodGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
// ignore_for_file: type=lint, type=warning
|
||||||
|
|
||||||
|
@ProviderFor(contextualToolbarConfigRepository)
|
||||||
|
final contextualToolbarConfigRepositoryProvider =
|
||||||
|
ContextualToolbarConfigRepositoryProvider._();
|
||||||
|
|
||||||
|
final class ContextualToolbarConfigRepositoryProvider
|
||||||
|
extends
|
||||||
|
$FunctionalProvider<
|
||||||
|
ContextualToolbarConfigRepository,
|
||||||
|
ContextualToolbarConfigRepository,
|
||||||
|
ContextualToolbarConfigRepository
|
||||||
|
>
|
||||||
|
with $Provider<ContextualToolbarConfigRepository> {
|
||||||
|
ContextualToolbarConfigRepositoryProvider._()
|
||||||
|
: super(
|
||||||
|
from: null,
|
||||||
|
argument: null,
|
||||||
|
retry: null,
|
||||||
|
name: r'contextualToolbarConfigRepositoryProvider',
|
||||||
|
isAutoDispose: false,
|
||||||
|
dependencies: null,
|
||||||
|
$allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String debugGetCreateSourceHash() =>
|
||||||
|
_$contextualToolbarConfigRepositoryHash();
|
||||||
|
|
||||||
|
@$internal
|
||||||
|
@override
|
||||||
|
$ProviderElement<ContextualToolbarConfigRepository> $createElement(
|
||||||
|
$ProviderPointer pointer,
|
||||||
|
) => $ProviderElement(pointer);
|
||||||
|
|
||||||
|
@override
|
||||||
|
ContextualToolbarConfigRepository create(Ref ref) {
|
||||||
|
return contextualToolbarConfigRepository(ref);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// {@macro riverpod.override_with_value}
|
||||||
|
Override overrideWithValue(ContextualToolbarConfigRepository value) {
|
||||||
|
return $ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
providerOverride: $SyncValueProvider<ContextualToolbarConfigRepository>(
|
||||||
|
value,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$contextualToolbarConfigRepositoryHash() =>
|
||||||
|
r'96bb0c00019e076ef65d3e901ba850de0fccbb1b';
|
||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
/*
|
||||||
|
* 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
enum ToolbarButtonId {
|
||||||
|
back,
|
||||||
|
forward,
|
||||||
|
bookmarks,
|
||||||
|
share,
|
||||||
|
addTab,
|
||||||
|
tabsCount,
|
||||||
|
navigationMenu,
|
||||||
|
}
|
||||||
+88
@@ -0,0 +1,88 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2024-2026 Fabian Freund.
|
||||||
|
*
|
||||||
|
* This file is part of WebLibre
|
||||||
|
* (see https://weblibre.eu).
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import 'package:weblibre/features/geckoview/features/browser/features/contextual_toolbar/domain/entities/toolbar_button_id.dart';
|
||||||
|
|
||||||
|
class ToolbarButtonSpec {
|
||||||
|
final ToolbarButtonId id;
|
||||||
|
final bool defaultVisible;
|
||||||
|
final ToolbarButtonId? defaultFallback;
|
||||||
|
final bool canBeFallbackTarget;
|
||||||
|
|
||||||
|
const ToolbarButtonSpec({
|
||||||
|
required this.id,
|
||||||
|
required this.defaultVisible,
|
||||||
|
this.defaultFallback,
|
||||||
|
this.canBeFallbackTarget = true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const backToolbarButtonSpec = ToolbarButtonSpec(
|
||||||
|
id: ToolbarButtonId.back,
|
||||||
|
defaultVisible: true,
|
||||||
|
defaultFallback: ToolbarButtonId.bookmarks,
|
||||||
|
);
|
||||||
|
|
||||||
|
const forwardToolbarButtonSpec = ToolbarButtonSpec(
|
||||||
|
id: ToolbarButtonId.forward,
|
||||||
|
defaultVisible: true,
|
||||||
|
defaultFallback: ToolbarButtonId.share,
|
||||||
|
);
|
||||||
|
|
||||||
|
const bookmarksToolbarButtonSpec = ToolbarButtonSpec(
|
||||||
|
id: ToolbarButtonId.bookmarks,
|
||||||
|
defaultVisible: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
const shareToolbarButtonSpec = ToolbarButtonSpec(
|
||||||
|
id: ToolbarButtonId.share,
|
||||||
|
defaultVisible: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
const addTabToolbarButtonSpec = ToolbarButtonSpec(
|
||||||
|
id: ToolbarButtonId.addTab,
|
||||||
|
defaultVisible: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
const tabsCountToolbarButtonSpec = ToolbarButtonSpec(
|
||||||
|
id: ToolbarButtonId.tabsCount,
|
||||||
|
defaultVisible: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
const navigationMenuToolbarButtonSpec = ToolbarButtonSpec(
|
||||||
|
id: ToolbarButtonId.navigationMenu,
|
||||||
|
defaultVisible: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
const toolbarButtonSpecs = [
|
||||||
|
backToolbarButtonSpec,
|
||||||
|
forwardToolbarButtonSpec,
|
||||||
|
bookmarksToolbarButtonSpec,
|
||||||
|
shareToolbarButtonSpec,
|
||||||
|
addTabToolbarButtonSpec,
|
||||||
|
tabsCountToolbarButtonSpec,
|
||||||
|
navigationMenuToolbarButtonSpec,
|
||||||
|
];
|
||||||
|
|
||||||
|
final Map<String, ToolbarButtonSpec> toolbarButtonSpecsById = {
|
||||||
|
for (final spec in toolbarButtonSpecs) spec.id.name: spec,
|
||||||
|
};
|
||||||
|
|
||||||
|
final Set<String> knownToolbarButtonIds = toolbarButtonSpecsById.keys.toSet();
|
||||||
+57
@@ -0,0 +1,57 @@
|
|||||||
|
/*
|
||||||
|
* 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:fast_equatable/fast_equatable.dart';
|
||||||
|
|
||||||
|
sealed class ToolbarFallbackChoice with FastEquatable {
|
||||||
|
ToolbarFallbackChoice();
|
||||||
|
|
||||||
|
factory ToolbarFallbackChoice.fromStored(String? storedFallbackId) {
|
||||||
|
if (storedFallbackId != null) {
|
||||||
|
return ToolbarFallbackButton(buttonId: storedFallbackId);
|
||||||
|
}
|
||||||
|
return ToolbarFallbackNone();
|
||||||
|
}
|
||||||
|
|
||||||
|
String? resolveRuntimeFallbackId();
|
||||||
|
|
||||||
|
String? toStoredFallbackId() => resolveRuntimeFallbackId();
|
||||||
|
}
|
||||||
|
|
||||||
|
class ToolbarFallbackButton extends ToolbarFallbackChoice {
|
||||||
|
final String buttonId;
|
||||||
|
|
||||||
|
ToolbarFallbackButton({required this.buttonId});
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get hashParameters => [buttonId];
|
||||||
|
|
||||||
|
@override
|
||||||
|
String resolveRuntimeFallbackId() => buttonId;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ToolbarFallbackNone extends ToolbarFallbackChoice {
|
||||||
|
ToolbarFallbackNone();
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get hashParameters => [null];
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? resolveRuntimeFallbackId() => null;
|
||||||
|
}
|
||||||
+116
@@ -0,0 +1,116 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2024-2026 Fabian Freund.
|
||||||
|
*
|
||||||
|
* This file is part of WebLibre
|
||||||
|
* (see https://weblibre.eu).
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import 'package:weblibre/features/geckoview/features/browser/features/contextual_toolbar/domain/entities/toolbar_fallback_choice.dart';
|
||||||
|
import 'package:weblibre/features/user/data/database/definitions.drift.dart';
|
||||||
|
|
||||||
|
class ContextualToolbarButtonResolution {
|
||||||
|
const ContextualToolbarButtonResolution({
|
||||||
|
required this.buttonId,
|
||||||
|
required this.isEnabled,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String buttonId;
|
||||||
|
final bool isEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ContextualToolbarButtonResolution> resolveVisibleContextualToolbarButtons({
|
||||||
|
required List<ToolbarButtonConfig> configs,
|
||||||
|
required Set<String> knownButtonIds,
|
||||||
|
required bool Function(String buttonId) isPrimaryAvailable,
|
||||||
|
}) {
|
||||||
|
final configById = {for (final config in configs) config.buttonId: config};
|
||||||
|
final resolvedButtons = <ContextualToolbarButtonResolution>[];
|
||||||
|
final seenIds = <String>{};
|
||||||
|
|
||||||
|
for (final config in configs.where((config) => config.isVisible)) {
|
||||||
|
final resolvedButton = resolveContextualToolbarButton(
|
||||||
|
config: config,
|
||||||
|
configById: configById,
|
||||||
|
knownButtonIds: knownButtonIds,
|
||||||
|
isPrimaryAvailable: isPrimaryAvailable,
|
||||||
|
);
|
||||||
|
if (resolvedButton == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (seenIds.add(resolvedButton.buttonId)) {
|
||||||
|
resolvedButtons.add(resolvedButton);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolvedButtons;
|
||||||
|
}
|
||||||
|
|
||||||
|
ContextualToolbarButtonResolution? resolveContextualToolbarButton({
|
||||||
|
required ToolbarButtonConfig config,
|
||||||
|
required Map<String, ToolbarButtonConfig> configById,
|
||||||
|
required Set<String> knownButtonIds,
|
||||||
|
required bool Function(String buttonId) isPrimaryAvailable,
|
||||||
|
Set<String> visited = const {},
|
||||||
|
}) {
|
||||||
|
if (visited.contains(config.buttonId)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!knownButtonIds.contains(config.buttonId)) {
|
||||||
|
return ContextualToolbarButtonResolution(
|
||||||
|
buttonId: config.buttonId,
|
||||||
|
isEnabled: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPrimaryAvailable(config.buttonId)) {
|
||||||
|
return ContextualToolbarButtonResolution(
|
||||||
|
buttonId: config.buttonId,
|
||||||
|
isEnabled: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final fallbackId = ToolbarFallbackChoice.fromStored(
|
||||||
|
config.fallbackId,
|
||||||
|
).resolveRuntimeFallbackId();
|
||||||
|
if (fallbackId == null) {
|
||||||
|
return ContextualToolbarButtonResolution(
|
||||||
|
buttonId: config.buttonId,
|
||||||
|
isEnabled: false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final fallbackConfig = configById[fallbackId];
|
||||||
|
if (fallbackConfig == null) {
|
||||||
|
return knownButtonIds.contains(fallbackId)
|
||||||
|
? ContextualToolbarButtonResolution(
|
||||||
|
buttonId: fallbackId,
|
||||||
|
isEnabled: true,
|
||||||
|
)
|
||||||
|
: ContextualToolbarButtonResolution(
|
||||||
|
buttonId: config.buttonId,
|
||||||
|
isEnabled: false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolveContextualToolbarButton(
|
||||||
|
config: fallbackConfig,
|
||||||
|
configById: configById,
|
||||||
|
knownButtonIds: knownButtonIds,
|
||||||
|
isPrimaryAvailable: isPrimaryAvailable,
|
||||||
|
visited: {...visited, config.buttonId},
|
||||||
|
);
|
||||||
|
}
|
||||||
+44
@@ -0,0 +1,44 @@
|
|||||||
|
/*
|
||||||
|
* 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:fast_equatable/fast_equatable.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/domain/entities/states/tab.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/browser/domain/entities/sheet.dart';
|
||||||
|
|
||||||
|
class ContextualToolbarScope with FastEquatable {
|
||||||
|
final String? selectedTabId;
|
||||||
|
final Sheet? displayedSheet;
|
||||||
|
final TabState? tabState;
|
||||||
|
final bool isPreview;
|
||||||
|
|
||||||
|
ContextualToolbarScope({
|
||||||
|
required this.selectedTabId,
|
||||||
|
required this.displayedSheet,
|
||||||
|
required this.tabState,
|
||||||
|
required this.isPreview,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get hashParameters => [
|
||||||
|
selectedTabId,
|
||||||
|
displayedSheet,
|
||||||
|
tabState,
|
||||||
|
isPreview,
|
||||||
|
];
|
||||||
|
}
|
||||||
+162
@@ -0,0 +1,162 @@
|
|||||||
|
/*
|
||||||
|
* 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:flutter/material.dart';
|
||||||
|
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
|
||||||
|
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:weblibre/core/routing/routes.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/browser/features/contextual_toolbar/domain/entities/toolbar_button_spec.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/browser/features/contextual_toolbar/presentation/models/contextual_toolbar_scope.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/browser/features/contextual_toolbar/presentation/widgets/contextual_bar_buttons.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/navigation_buttons.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tabs_action_button.dart';
|
||||||
|
|
||||||
|
class ToolbarButtonDefinition {
|
||||||
|
final ToolbarButtonSpec spec;
|
||||||
|
final String label;
|
||||||
|
final IconData icon;
|
||||||
|
final bool Function(ContextualToolbarScope scope, WidgetRef ref)?
|
||||||
|
isPrimaryAvailable;
|
||||||
|
final Widget Function(
|
||||||
|
ContextualToolbarScope scope,
|
||||||
|
BuildContext context,
|
||||||
|
WidgetRef ref,
|
||||||
|
)
|
||||||
|
builder;
|
||||||
|
|
||||||
|
const ToolbarButtonDefinition({
|
||||||
|
required this.spec,
|
||||||
|
required this.label,
|
||||||
|
required this.icon,
|
||||||
|
this.isPrimaryAvailable,
|
||||||
|
required this.builder,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<ToolbarButtonDefinition> toolbarButtonRegistry = [
|
||||||
|
ToolbarButtonDefinition(
|
||||||
|
spec: backToolbarButtonSpec,
|
||||||
|
label: 'Back',
|
||||||
|
icon: Icons.arrow_back,
|
||||||
|
isPrimaryAvailable: (scope, ref) =>
|
||||||
|
scope.tabState?.historyState.canGoBack == true ||
|
||||||
|
scope.tabState?.isLoading == true,
|
||||||
|
builder: (scope, context, ref) {
|
||||||
|
if (scope.isPreview) {
|
||||||
|
return NavigateBackButtonView(
|
||||||
|
canGoBack: true,
|
||||||
|
isLoading: false,
|
||||||
|
onPressed: () {},
|
||||||
|
onLongPress: () {},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return NavigateBackButton(
|
||||||
|
selectedTabId: scope.selectedTabId,
|
||||||
|
isLoading: scope.tabState?.isLoading ?? false,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ToolbarButtonDefinition(
|
||||||
|
spec: forwardToolbarButtonSpec,
|
||||||
|
label: 'Forward',
|
||||||
|
icon: Icons.arrow_forward,
|
||||||
|
isPrimaryAvailable: (scope, ref) =>
|
||||||
|
scope.tabState?.historyState.canGoForward == true,
|
||||||
|
builder: (scope, context, ref) {
|
||||||
|
if (scope.isPreview) {
|
||||||
|
return NavigateForwardButtonView(
|
||||||
|
canGoForward: true,
|
||||||
|
onPressed: () {},
|
||||||
|
onLongPress: () {},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return NavigateForwardButton(selectedTabId: scope.selectedTabId);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ToolbarButtonDefinition(
|
||||||
|
spec: bookmarksToolbarButtonSpec,
|
||||||
|
label: 'Bookmarks',
|
||||||
|
icon: MdiIcons.bookmarkMultiple,
|
||||||
|
builder: (scope, context, ref) {
|
||||||
|
return IconButton(
|
||||||
|
onPressed: scope.isPreview
|
||||||
|
? () {}
|
||||||
|
: () async {
|
||||||
|
await BookmarkListRoute(
|
||||||
|
entryGuid: BookmarkRoot.root.id,
|
||||||
|
).push(context);
|
||||||
|
},
|
||||||
|
icon: const Icon(MdiIcons.bookmarkMultiple),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ToolbarButtonDefinition(
|
||||||
|
spec: shareToolbarButtonSpec,
|
||||||
|
label: 'Share',
|
||||||
|
icon: Icons.share,
|
||||||
|
builder: (scope, context, ref) => scope.isPreview
|
||||||
|
? ShareMenuButtonView(onPressed: () {})
|
||||||
|
: ShareMenuButton(selectedTabId: scope.selectedTabId),
|
||||||
|
),
|
||||||
|
ToolbarButtonDefinition(
|
||||||
|
spec: addTabToolbarButtonSpec,
|
||||||
|
label: 'New Tab',
|
||||||
|
icon: MdiIcons.tabPlus,
|
||||||
|
builder: (scope, context, ref) => scope.isPreview
|
||||||
|
? AddTabButtonView(onPressed: () {}, onLongPress: () {})
|
||||||
|
: const AddTabButton(),
|
||||||
|
),
|
||||||
|
ToolbarButtonDefinition(
|
||||||
|
spec: tabsCountToolbarButtonSpec,
|
||||||
|
label: 'Tabs',
|
||||||
|
icon: MdiIcons.tab,
|
||||||
|
builder: (scope, context, ref) => scope.isPreview
|
||||||
|
? TabsCountButtonView(
|
||||||
|
isActive: false,
|
||||||
|
onTap: () {},
|
||||||
|
onLongPress: () {},
|
||||||
|
buttonBuilder: (isActive, onTap, onLongPress) {
|
||||||
|
return TabsActionButtonView(
|
||||||
|
isActive: isActive,
|
||||||
|
tabCountText: '5',
|
||||||
|
onTap: onTap,
|
||||||
|
onLongPress: onLongPress,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: TabsCountButton(
|
||||||
|
selectedTabId: scope.selectedTabId,
|
||||||
|
displayedSheet: scope.displayedSheet,
|
||||||
|
showLongPressMenu: false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ToolbarButtonDefinition(
|
||||||
|
spec: navigationMenuToolbarButtonSpec,
|
||||||
|
label: 'Menu',
|
||||||
|
icon: Icons.more_vert,
|
||||||
|
builder: (scope, context, ref) => scope.isPreview
|
||||||
|
? NavigationMenuButtonView(onTap: () {})
|
||||||
|
: NavigationMenuButton(selectedTabId: scope.selectedTabId),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
final Map<String, ToolbarButtonDefinition> toolbarButtonRegistryById = {
|
||||||
|
for (final def in toolbarButtonRegistry) def.spec.id.name: def,
|
||||||
|
};
|
||||||
+221
@@ -0,0 +1,221 @@
|
|||||||
|
/*
|
||||||
|
* 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:flutter/material.dart';
|
||||||
|
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:weblibre/core/routing/routes.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/domain/controllers/bottom_sheet.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/browser/domain/entities/sheet.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/browser_menu_sheet.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/share_bottom_sheet.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_creation_menu.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tabs_action_button.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/toolbar_button.dart';
|
||||||
|
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
||||||
|
import 'package:weblibre/presentation/hooks/menu_controller.dart';
|
||||||
|
|
||||||
|
class ShareMenuButton extends StatelessWidget {
|
||||||
|
final String? selectedTabId;
|
||||||
|
|
||||||
|
const ShareMenuButton({super.key, required this.selectedTabId});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ShareMenuButtonView(
|
||||||
|
onPressed: () async {
|
||||||
|
final tabId = selectedTabId;
|
||||||
|
if (tabId != null) {
|
||||||
|
await showShareBottomSheet(context, selectedTabId: tabId);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ShareMenuButtonView extends StatelessWidget {
|
||||||
|
const ShareMenuButtonView({super.key, this.onPressed});
|
||||||
|
|
||||||
|
final VoidCallback? onPressed;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return IconButton(onPressed: onPressed, icon: const Icon(Icons.share));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NavigationMenuButton extends StatelessWidget {
|
||||||
|
final String? selectedTabId;
|
||||||
|
|
||||||
|
const NavigationMenuButton({super.key, required this.selectedTabId});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return NavigationMenuButtonView(
|
||||||
|
onTap: () async {
|
||||||
|
await showBrowserMenuSheet(context);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NavigationMenuButtonView extends StatelessWidget {
|
||||||
|
const NavigationMenuButtonView({super.key, this.onTap});
|
||||||
|
|
||||||
|
final VoidCallback? onTap;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ToolbarButton(onTap: onTap, child: const Icon(Icons.more_vert));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AddTabButton extends HookConsumerWidget {
|
||||||
|
const AddTabButton({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final tabMenuController = useMenuController();
|
||||||
|
|
||||||
|
return TabCreationMenu(
|
||||||
|
controller: tabMenuController,
|
||||||
|
child: AddTabButtonView(
|
||||||
|
onPressed: () async {
|
||||||
|
final settings = ref.read(generalSettingsWithDefaultsProvider);
|
||||||
|
|
||||||
|
await SearchRoute(
|
||||||
|
tabType:
|
||||||
|
ref.read(selectedTabTypeProvider) ??
|
||||||
|
settings.effectiveDefaultCreateTabType,
|
||||||
|
).push(context);
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
|
const BrowserRoute().go(context);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLongPress: () {
|
||||||
|
if (tabMenuController.isOpen) {
|
||||||
|
tabMenuController.close();
|
||||||
|
} else {
|
||||||
|
tabMenuController.open();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AddTabButtonView extends StatelessWidget {
|
||||||
|
const AddTabButtonView({super.key, this.onPressed, this.onLongPress});
|
||||||
|
|
||||||
|
final VoidCallback? onPressed;
|
||||||
|
final VoidCallback? onLongPress;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return IconButton(
|
||||||
|
onPressed: onPressed,
|
||||||
|
icon: const Icon(MdiIcons.tabPlus),
|
||||||
|
onLongPress: onLongPress,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TabsCountButtonView extends StatelessWidget {
|
||||||
|
const TabsCountButtonView({
|
||||||
|
super.key,
|
||||||
|
required this.isActive,
|
||||||
|
required this.onTap,
|
||||||
|
this.onLongPress,
|
||||||
|
this.buttonBuilder,
|
||||||
|
});
|
||||||
|
|
||||||
|
final bool isActive;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
final VoidCallback? onLongPress;
|
||||||
|
final Widget Function(
|
||||||
|
bool isActive,
|
||||||
|
VoidCallback onTap,
|
||||||
|
VoidCallback? onLongPress,
|
||||||
|
)?
|
||||||
|
buttonBuilder;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return (buttonBuilder != null)
|
||||||
|
? buttonBuilder!(isActive, onTap, onLongPress)
|
||||||
|
: TabsActionButton(
|
||||||
|
isActive: isActive,
|
||||||
|
onTap: onTap,
|
||||||
|
onLongPress: onLongPress,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TabsCountButton extends HookConsumerWidget {
|
||||||
|
const TabsCountButton({
|
||||||
|
super.key,
|
||||||
|
required this.selectedTabId,
|
||||||
|
required this.displayedSheet,
|
||||||
|
required this.showLongPressMenu,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String? selectedTabId;
|
||||||
|
final Sheet? displayedSheet;
|
||||||
|
final bool showLongPressMenu;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final tabMenuController = useMenuController();
|
||||||
|
|
||||||
|
return TabCreationMenu(
|
||||||
|
controller: tabMenuController,
|
||||||
|
child: TabsCountButtonView(
|
||||||
|
isActive: displayedSheet is ViewTabsSheet,
|
||||||
|
onTap: () async {
|
||||||
|
final tabViewBottomSheet = ref
|
||||||
|
.read(generalSettingsWithDefaultsProvider)
|
||||||
|
.tabViewBottomSheet;
|
||||||
|
|
||||||
|
if (tabViewBottomSheet) {
|
||||||
|
if (displayedSheet case ViewTabsSheet()) {
|
||||||
|
ref.read(bottomSheetControllerProvider.notifier).requestDismiss();
|
||||||
|
} else {
|
||||||
|
ref
|
||||||
|
.read(bottomSheetControllerProvider.notifier)
|
||||||
|
.show(ViewTabsSheet());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await const TabViewRoute().push(context);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLongPress: showLongPressMenu
|
||||||
|
? () {
|
||||||
|
if (tabMenuController.isOpen) {
|
||||||
|
tabMenuController.close();
|
||||||
|
} else {
|
||||||
|
tabMenuController.open();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+128
@@ -0,0 +1,128 @@
|
|||||||
|
/*
|
||||||
|
* 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:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/browser/domain/entities/sheet.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/browser/features/contextual_toolbar/data/providers/toolbar_button_configs.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/browser/features/contextual_toolbar/domain/entities/toolbar_button_spec.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/browser/features/contextual_toolbar/domain/services/toolbar_button_resolution.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/browser/features/contextual_toolbar/presentation/models/contextual_toolbar_scope.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/browser/features/contextual_toolbar/presentation/toolbar_button_registry.dart';
|
||||||
|
|
||||||
|
class ContextualToolbar extends HookConsumerWidget {
|
||||||
|
const ContextualToolbar({
|
||||||
|
super.key,
|
||||||
|
required this.selectedTabId,
|
||||||
|
required this.displayedSheet,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String? selectedTabId;
|
||||||
|
final Sheet? displayedSheet;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final tabState = ref.watch(tabStateProvider(selectedTabId));
|
||||||
|
final configs = ref.watch(effectiveToolbarButtonConfigsProvider);
|
||||||
|
|
||||||
|
final scope = ContextualToolbarScope(
|
||||||
|
selectedTabId: selectedTabId,
|
||||||
|
displayedSheet: displayedSheet,
|
||||||
|
tabState: tabState,
|
||||||
|
isPreview: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
final resolvedButtons = useMemoized(
|
||||||
|
() => resolveVisibleContextualToolbarButtons(
|
||||||
|
configs: configs.value,
|
||||||
|
knownButtonIds: knownToolbarButtonIds,
|
||||||
|
isPrimaryAvailable: (buttonId) {
|
||||||
|
final def = toolbarButtonRegistryById[buttonId];
|
||||||
|
return def?.isPrimaryAvailable?.call(scope, ref) ?? true;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
[configs, scope],
|
||||||
|
);
|
||||||
|
|
||||||
|
final buttons = resolvedButtons
|
||||||
|
.map((button) => _buildButton(scope, context, ref, button))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
return ContextualToolbarView(buttons: buttons);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildButton(
|
||||||
|
ContextualToolbarScope scope,
|
||||||
|
BuildContext context,
|
||||||
|
WidgetRef ref,
|
||||||
|
ContextualToolbarButtonResolution button,
|
||||||
|
) {
|
||||||
|
final def = toolbarButtonRegistryById[button.buttonId];
|
||||||
|
if (def == null) return const SizedBox.shrink();
|
||||||
|
|
||||||
|
final child = def.builder(scope, context, ref);
|
||||||
|
|
||||||
|
if (button.isEnabled) {
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Opacity(opacity: 0.38, child: IgnorePointer(child: child));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ContextualToolbarView extends StatelessWidget {
|
||||||
|
const ContextualToolbarView({super.key, required this.buttons});
|
||||||
|
|
||||||
|
final List<Widget> buttons;
|
||||||
|
|
||||||
|
static const _minButtonWidth = 48.0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (buttons.isEmpty) return const SizedBox.shrink();
|
||||||
|
|
||||||
|
return LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
final fitsEvenly =
|
||||||
|
constraints.maxWidth >= _minButtonWidth * buttons.length;
|
||||||
|
|
||||||
|
if (fitsEvenly) {
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
|
children: buttons,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return SingleChildScrollView(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
child: Row(
|
||||||
|
children: buttons
|
||||||
|
.map(
|
||||||
|
(button) => SizedBox(width: _minButtonWidth, child: button),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-279
@@ -25,10 +25,8 @@ 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';
|
||||||
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:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:weblibre/core/design/app_colors.dart';
|
import 'package:weblibre/core/design/app_colors.dart';
|
||||||
import 'package:weblibre/core/routing/routes.dart';
|
|
||||||
import 'package:weblibre/features/geckoview/domain/controllers/bottom_sheet.dart';
|
import 'package:weblibre/features/geckoview/domain/controllers/bottom_sheet.dart';
|
||||||
import 'package:weblibre/features/geckoview/domain/entities/states/readerable.dart';
|
import 'package:weblibre/features/geckoview/domain/entities/states/readerable.dart';
|
||||||
import 'package:weblibre/features/geckoview/domain/providers/selected_tab.dart';
|
import 'package:weblibre/features/geckoview/domain/providers/selected_tab.dart';
|
||||||
@@ -36,16 +34,13 @@ 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/domain/entities/sheet.dart';
|
import 'package:weblibre/features/geckoview/features/browser/domain/entities/sheet.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/features/contextual_toolbar/presentation/widgets/contextual_bar_buttons.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/browser/features/contextual_toolbar/presentation/widgets/contextual_toolbar.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/toolbar_visibility.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/toolbar_visibility.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/browser_menu_sheet.dart';
|
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/browser_modules/app_bar_title.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/browser_modules/app_bar_title.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/extension_shortcut_menu.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/extension_shortcut_menu.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/navigation_buttons.dart';
|
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/share_bottom_sheet.dart';
|
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_creation_menu.dart';
|
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_icon.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_icon.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_menu.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_menu.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tabs_action_button.dart';
|
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/toolbar_button.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/toolbar_button.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/readerview/presentation/controllers/readerable.dart';
|
import 'package:weblibre/features/geckoview/features/readerview/presentation/controllers/readerable.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/readerview/presentation/widgets/reader_button.dart';
|
import 'package:weblibre/features/geckoview/features/readerview/presentation/widgets/reader_button.dart';
|
||||||
@@ -460,90 +455,6 @@ class QuickTabSwitcherItem with FastEquatable {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
class ContextualToolbar extends HookConsumerWidget {
|
|
||||||
const ContextualToolbar({
|
|
||||||
super.key,
|
|
||||||
required this.selectedTabId,
|
|
||||||
required this.displayedSheet,
|
|
||||||
});
|
|
||||||
|
|
||||||
final String? selectedTabId;
|
|
||||||
final Sheet? displayedSheet;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final tabState = ref.watch(tabStateProvider(selectedTabId));
|
|
||||||
|
|
||||||
return ContextualToolbarView(
|
|
||||||
canGoBack:
|
|
||||||
tabState?.historyState.canGoBack == true ||
|
|
||||||
tabState?.isLoading == true,
|
|
||||||
canGoForward: tabState?.historyState.canGoForward == true,
|
|
||||||
onBookmarksTap: () async {
|
|
||||||
await BookmarkListRoute(entryGuid: BookmarkRoot.root.id).push(context);
|
|
||||||
},
|
|
||||||
backButton: NavigateBackButton(
|
|
||||||
selectedTabId: selectedTabId,
|
|
||||||
isLoading: tabState?.isLoading ?? false,
|
|
||||||
),
|
|
||||||
forwardButton: NavigateForwardButton(selectedTabId: selectedTabId),
|
|
||||||
shareButton: ShareMenuButton(selectedTabId: selectedTabId),
|
|
||||||
addTabButton: const AddTabButton(),
|
|
||||||
tabsCountButton: TabsCountButton(
|
|
||||||
selectedTabId: selectedTabId,
|
|
||||||
displayedSheet: displayedSheet,
|
|
||||||
showLongPressMenu: false,
|
|
||||||
),
|
|
||||||
navigationButton: NavigationMenuButton(selectedTabId: selectedTabId),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ContextualToolbarView extends StatelessWidget {
|
|
||||||
const ContextualToolbarView({
|
|
||||||
super.key,
|
|
||||||
required this.canGoBack,
|
|
||||||
required this.canGoForward,
|
|
||||||
required this.onBookmarksTap,
|
|
||||||
required this.backButton,
|
|
||||||
required this.forwardButton,
|
|
||||||
required this.shareButton,
|
|
||||||
required this.addTabButton,
|
|
||||||
required this.tabsCountButton,
|
|
||||||
required this.navigationButton,
|
|
||||||
});
|
|
||||||
|
|
||||||
final bool canGoBack;
|
|
||||||
final bool canGoForward;
|
|
||||||
final VoidCallback onBookmarksTap;
|
|
||||||
final Widget backButton;
|
|
||||||
final Widget forwardButton;
|
|
||||||
final Widget shareButton;
|
|
||||||
final Widget addTabButton;
|
|
||||||
final Widget tabsCountButton;
|
|
||||||
final Widget navigationButton;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
||||||
children: [
|
|
||||||
if (canGoBack)
|
|
||||||
backButton
|
|
||||||
else
|
|
||||||
IconButton(
|
|
||||||
onPressed: onBookmarksTap,
|
|
||||||
icon: const Icon(MdiIcons.bookmarkMultiple),
|
|
||||||
),
|
|
||||||
if (canGoForward) forwardButton else shareButton,
|
|
||||||
addTabButton,
|
|
||||||
tabsCountButton,
|
|
||||||
navigationButton,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class QuickTabSwitcher extends HookConsumerWidget {
|
class QuickTabSwitcher extends HookConsumerWidget {
|
||||||
final QuickTabSwitcherMode quickTabSwitcherMode;
|
final QuickTabSwitcherMode quickTabSwitcherMode;
|
||||||
|
|
||||||
@@ -760,191 +671,3 @@ class QuickTabSwitcherView extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ShareMenuButton extends StatelessWidget {
|
|
||||||
final String? selectedTabId;
|
|
||||||
|
|
||||||
const ShareMenuButton({super.key, required this.selectedTabId});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return ShareMenuButtonView(
|
|
||||||
onPressed: () async {
|
|
||||||
final tabId = selectedTabId;
|
|
||||||
if (tabId != null) {
|
|
||||||
await showShareBottomSheet(context, selectedTabId: tabId);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ShareMenuButtonView extends StatelessWidget {
|
|
||||||
const ShareMenuButtonView({super.key, this.onPressed});
|
|
||||||
|
|
||||||
final VoidCallback? onPressed;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return IconButton(onPressed: onPressed, icon: const Icon(Icons.share));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class NavigationMenuButton extends StatelessWidget {
|
|
||||||
final String? selectedTabId;
|
|
||||||
|
|
||||||
const NavigationMenuButton({super.key, required this.selectedTabId});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return NavigationMenuButtonView(
|
|
||||||
onTap: () async {
|
|
||||||
await showBrowserMenuSheet(context);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class NavigationMenuButtonView extends StatelessWidget {
|
|
||||||
const NavigationMenuButtonView({super.key, this.onTap});
|
|
||||||
|
|
||||||
final VoidCallback? onTap;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return ToolbarButton(onTap: onTap, child: const Icon(Icons.more_vert));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class AddTabButton extends HookConsumerWidget {
|
|
||||||
const AddTabButton({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final tabMenuController = useMenuController();
|
|
||||||
|
|
||||||
return TabCreationMenu(
|
|
||||||
controller: tabMenuController,
|
|
||||||
child: AddTabButtonView(
|
|
||||||
onPressed: () async {
|
|
||||||
final settings = ref.read(generalSettingsWithDefaultsProvider);
|
|
||||||
|
|
||||||
await SearchRoute(
|
|
||||||
tabType:
|
|
||||||
ref.read(selectedTabTypeProvider) ??
|
|
||||||
settings.effectiveDefaultCreateTabType,
|
|
||||||
).push(context);
|
|
||||||
|
|
||||||
if (context.mounted) {
|
|
||||||
const BrowserRoute().go(context);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onLongPress: () {
|
|
||||||
if (tabMenuController.isOpen) {
|
|
||||||
tabMenuController.close();
|
|
||||||
} else {
|
|
||||||
tabMenuController.open();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class AddTabButtonView extends StatelessWidget {
|
|
||||||
const AddTabButtonView({super.key, this.onPressed, this.onLongPress});
|
|
||||||
|
|
||||||
final VoidCallback? onPressed;
|
|
||||||
final VoidCallback? onLongPress;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return IconButton(
|
|
||||||
onPressed: onPressed,
|
|
||||||
icon: const Icon(MdiIcons.tabPlus),
|
|
||||||
onLongPress: onLongPress,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class TabsCountButtonView extends StatelessWidget {
|
|
||||||
const TabsCountButtonView({
|
|
||||||
super.key,
|
|
||||||
required this.isActive,
|
|
||||||
required this.onTap,
|
|
||||||
this.onLongPress,
|
|
||||||
this.buttonBuilder,
|
|
||||||
});
|
|
||||||
|
|
||||||
final bool isActive;
|
|
||||||
final VoidCallback onTap;
|
|
||||||
final VoidCallback? onLongPress;
|
|
||||||
final Widget Function(
|
|
||||||
bool isActive,
|
|
||||||
VoidCallback onTap,
|
|
||||||
VoidCallback? onLongPress,
|
|
||||||
)?
|
|
||||||
buttonBuilder;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return (buttonBuilder != null)
|
|
||||||
? buttonBuilder!(isActive, onTap, onLongPress)
|
|
||||||
: TabsActionButton(
|
|
||||||
isActive: isActive,
|
|
||||||
onTap: onTap,
|
|
||||||
onLongPress: onLongPress,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class TabsCountButton extends HookConsumerWidget {
|
|
||||||
const TabsCountButton({
|
|
||||||
super.key,
|
|
||||||
required this.selectedTabId,
|
|
||||||
required this.displayedSheet,
|
|
||||||
required this.showLongPressMenu,
|
|
||||||
});
|
|
||||||
|
|
||||||
final String? selectedTabId;
|
|
||||||
final Sheet? displayedSheet;
|
|
||||||
final bool showLongPressMenu;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final tabMenuController = useMenuController();
|
|
||||||
|
|
||||||
return TabCreationMenu(
|
|
||||||
controller: tabMenuController,
|
|
||||||
child: TabsCountButtonView(
|
|
||||||
isActive: displayedSheet is ViewTabsSheet,
|
|
||||||
onTap: () async {
|
|
||||||
final tabViewBottomSheet = ref
|
|
||||||
.read(generalSettingsWithDefaultsProvider)
|
|
||||||
.tabViewBottomSheet;
|
|
||||||
|
|
||||||
if (tabViewBottomSheet) {
|
|
||||||
if (displayedSheet case ViewTabsSheet()) {
|
|
||||||
ref.read(bottomSheetControllerProvider.notifier).requestDismiss();
|
|
||||||
} else {
|
|
||||||
ref
|
|
||||||
.read(bottomSheetControllerProvider.notifier)
|
|
||||||
.show(ViewTabsSheet());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
await const TabViewRoute().push(context);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onLongPress: showLongPressMenu
|
|
||||||
? () {
|
|
||||||
if (tabMenuController.isOpen) {
|
|
||||||
tabMenuController.close();
|
|
||||||
} else {
|
|
||||||
tabMenuController.open();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
+1
-1
@@ -45,7 +45,7 @@ final class StartupPreferenceEnforcementServiceProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _$startupPreferenceEnforcementServiceHash() =>
|
String _$startupPreferenceEnforcementServiceHash() =>
|
||||||
r'107cdcf7550b7977b53a3cf7419861cdcac0fe3a';
|
r'5cfe5aec33a9e9e077704ff519c10b0398c5085d';
|
||||||
|
|
||||||
abstract class _$StartupPreferenceEnforcementService extends $Notifier<void> {
|
abstract class _$StartupPreferenceEnforcementService extends $Notifier<void> {
|
||||||
void build();
|
void build();
|
||||||
|
|||||||
@@ -0,0 +1,321 @@
|
|||||||
|
/*
|
||||||
|
* 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/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/browser/features/contextual_toolbar/data/providers/toolbar_button_configs.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/browser/features/contextual_toolbar/data/repositories/contextual_toolbar_config_repository.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/browser/features/contextual_toolbar/domain/entities/toolbar_fallback_choice.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/browser/features/contextual_toolbar/presentation/models/contextual_toolbar_scope.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/browser/features/contextual_toolbar/presentation/toolbar_button_registry.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/browser/features/contextual_toolbar/presentation/widgets/contextual_toolbar.dart';
|
||||||
|
import 'package:weblibre/features/user/data/database/definitions.drift.dart';
|
||||||
|
|
||||||
|
class ContextualToolbarSettingsScreen extends HookConsumerWidget {
|
||||||
|
const ContextualToolbarSettingsScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final configs = ref.watch(effectiveToolbarButtonConfigsProvider);
|
||||||
|
final repository = ref.watch(contextualToolbarConfigRepositoryProvider);
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Customize Toolbar'),
|
||||||
|
actions: [
|
||||||
|
MenuAnchor(
|
||||||
|
builder: (context, controller, child) => IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
if (controller.isOpen) {
|
||||||
|
controller.close();
|
||||||
|
} else {
|
||||||
|
controller.open();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.more_vert),
|
||||||
|
),
|
||||||
|
menuChildren: [
|
||||||
|
MenuItemButton(
|
||||||
|
leadingIcon: const Icon(Icons.restore),
|
||||||
|
onPressed: () => _resetToDefaults(ref),
|
||||||
|
child: const Text('Reset to Defaults'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: SafeArea(
|
||||||
|
child: CustomScrollView(
|
||||||
|
slivers: [
|
||||||
|
SliverPersistentHeader(
|
||||||
|
pinned: true,
|
||||||
|
delegate: _ToolbarPreviewDelegate(configs: configs.value),
|
||||||
|
),
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: ListTile(
|
||||||
|
title: Text(
|
||||||
|
'Button Order',
|
||||||
|
style: Theme.of(context).textTheme.labelLarge,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SliverReorderableList(
|
||||||
|
itemCount: configs.value.length,
|
||||||
|
onReorder: (oldIndex, newIndex) =>
|
||||||
|
_onReorder(configs.value, oldIndex, newIndex, repository),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final config = configs.value[index];
|
||||||
|
return _ToolbarButtonConfigTile(
|
||||||
|
key: ValueKey(config.buttonId),
|
||||||
|
index: index,
|
||||||
|
config: config,
|
||||||
|
repository: repository,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
({String movedId, int targetIndex}) _resolveToolbarReorder(
|
||||||
|
List<ToolbarButtonConfig> configs,
|
||||||
|
int oldIndex,
|
||||||
|
int newIndex,
|
||||||
|
) {
|
||||||
|
var targetIndex = newIndex;
|
||||||
|
if (targetIndex > oldIndex) {
|
||||||
|
targetIndex -= 1;
|
||||||
|
}
|
||||||
|
targetIndex = targetIndex.clamp(0, configs.length - 1);
|
||||||
|
return (movedId: configs[oldIndex].buttonId, targetIndex: targetIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onReorder(
|
||||||
|
List<ToolbarButtonConfig> configs,
|
||||||
|
int oldIndex,
|
||||||
|
int newIndex,
|
||||||
|
ContextualToolbarConfigRepository repository,
|
||||||
|
) {
|
||||||
|
if (oldIndex == newIndex) return;
|
||||||
|
final reorder = _resolveToolbarReorder(configs, oldIndex, newIndex);
|
||||||
|
if (reorder.targetIndex == oldIndex) return;
|
||||||
|
unawaited(
|
||||||
|
_reorderViaDb(
|
||||||
|
repository,
|
||||||
|
configs,
|
||||||
|
oldIndex,
|
||||||
|
reorder.targetIndex,
|
||||||
|
reorder.movedId,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _reorderViaDb(
|
||||||
|
ContextualToolbarConfigRepository repository,
|
||||||
|
List<ToolbarButtonConfig> configs,
|
||||||
|
int oldIndex,
|
||||||
|
int targetIndex,
|
||||||
|
String movedId,
|
||||||
|
) async {
|
||||||
|
final String orderKey;
|
||||||
|
if (targetIndex <= 0) {
|
||||||
|
orderKey = await repository.generateLeadingOrderKey();
|
||||||
|
} else if (targetIndex >= configs.length - 1) {
|
||||||
|
orderKey = await repository.generateTrailingOrderKey();
|
||||||
|
} else if (targetIndex < oldIndex) {
|
||||||
|
orderKey =
|
||||||
|
await repository.generateOrderKeyAfterButtonId(
|
||||||
|
configs[targetIndex - 1].buttonId,
|
||||||
|
) ??
|
||||||
|
await repository.generateLeadingOrderKey();
|
||||||
|
} else {
|
||||||
|
orderKey = await repository.generateOrderKeyBeforeButtonId(
|
||||||
|
configs[targetIndex + 1].buttonId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await repository.assignOrderKey(movedId, orderKey: orderKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _resetToDefaults(WidgetRef ref) async {
|
||||||
|
final repository = ref.read(contextualToolbarConfigRepositoryProvider);
|
||||||
|
await repository.replaceAll(defaultToolbarButtonConfigs.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ToolbarButtonConfigTile extends HookConsumerWidget {
|
||||||
|
const _ToolbarButtonConfigTile({
|
||||||
|
super.key,
|
||||||
|
required this.index,
|
||||||
|
required this.config,
|
||||||
|
required this.repository,
|
||||||
|
});
|
||||||
|
|
||||||
|
final int index;
|
||||||
|
final ToolbarButtonConfig config;
|
||||||
|
final ContextualToolbarConfigRepository repository;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final def = toolbarButtonRegistryById[config.buttonId];
|
||||||
|
if (def == null) return const SizedBox.shrink();
|
||||||
|
|
||||||
|
final isVisible = config.isVisible;
|
||||||
|
final hasStatefulFallback =
|
||||||
|
def.isPrimaryAvailable != null || def.spec.defaultFallback != null;
|
||||||
|
|
||||||
|
final fallbackOptions = toolbarButtonRegistry
|
||||||
|
.where(
|
||||||
|
(d) =>
|
||||||
|
d.spec.id.name != config.buttonId && d.spec.canBeFallbackTarget,
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
return Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: ListTile(
|
||||||
|
leading: Icon(def.icon),
|
||||||
|
title: Text(def.label),
|
||||||
|
subtitle: hasStatefulFallback
|
||||||
|
? _FallbackPicker(
|
||||||
|
current: ToolbarFallbackChoice.fromStored(config.fallbackId),
|
||||||
|
options: fallbackOptions,
|
||||||
|
onChanged: (newFallback) => repository.assignFallback(
|
||||||
|
config.buttonId,
|
||||||
|
(newFallback ?? ToolbarFallbackNone()).toStoredFallbackId(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
trailing: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Switch.adaptive(
|
||||||
|
value: isVisible,
|
||||||
|
onChanged: (v) =>
|
||||||
|
repository.assignVisibility(config.buttonId, visible: v),
|
||||||
|
),
|
||||||
|
ReorderableDragStartListener(
|
||||||
|
index: index,
|
||||||
|
child: const Padding(
|
||||||
|
padding: EdgeInsets.all(8),
|
||||||
|
child: Icon(Icons.drag_handle),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FallbackPicker extends StatelessWidget {
|
||||||
|
const _FallbackPicker({
|
||||||
|
required this.current,
|
||||||
|
required this.options,
|
||||||
|
required this.onChanged,
|
||||||
|
});
|
||||||
|
|
||||||
|
final ToolbarFallbackChoice current;
|
||||||
|
final List<ToolbarButtonDefinition> options;
|
||||||
|
final ValueChanged<ToolbarFallbackChoice?> onChanged;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return DropdownButton<ToolbarFallbackChoice>(
|
||||||
|
value: current,
|
||||||
|
hint: const Text('No fallback'),
|
||||||
|
isExpanded: true,
|
||||||
|
underline: const SizedBox.shrink(),
|
||||||
|
items: [
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: ToolbarFallbackNone(),
|
||||||
|
child: const Text('No fallback'),
|
||||||
|
),
|
||||||
|
for (final opt in options)
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: ToolbarFallbackButton(buttonId: opt.spec.id.name),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(opt.icon, size: 16),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(opt.label),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
onChanged: onChanged,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ToolbarPreviewDelegate extends SliverPersistentHeaderDelegate {
|
||||||
|
const _ToolbarPreviewDelegate({required this.configs});
|
||||||
|
|
||||||
|
final List<ToolbarButtonConfig> configs;
|
||||||
|
|
||||||
|
@override
|
||||||
|
double get minExtent => 56.0;
|
||||||
|
@override
|
||||||
|
double get maxExtent => 56.0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(
|
||||||
|
BuildContext context,
|
||||||
|
double shrinkOffset,
|
||||||
|
bool overlapsContent,
|
||||||
|
) {
|
||||||
|
return ColoredBox(
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
|
child: IgnorePointer(child: _ToolbarPreview(configs: configs)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRebuild(_ToolbarPreviewDelegate old) => old.configs != configs;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ToolbarPreview extends ConsumerWidget {
|
||||||
|
const _ToolbarPreview({required this.configs});
|
||||||
|
|
||||||
|
final List<ToolbarButtonConfig> configs;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final visibleConfigs = configs.where((c) => c.isVisible).toList();
|
||||||
|
|
||||||
|
final scope = ContextualToolbarScope(
|
||||||
|
selectedTabId: null,
|
||||||
|
displayedSheet: null,
|
||||||
|
tabState: null,
|
||||||
|
isPreview: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
final buttons = visibleConfigs.map((config) {
|
||||||
|
final def = toolbarButtonRegistryById[config.buttonId];
|
||||||
|
if (def == null) return const SizedBox.shrink();
|
||||||
|
return def.builder(scope, context, ref);
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
return ContextualToolbarView(buttons: buttons);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,8 +22,11 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
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:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:weblibre/core/routing/routes.dart';
|
||||||
import 'package:weblibre/features/geckoview/domain/entities/states/security.dart';
|
import 'package:weblibre/features/geckoview/domain/entities/states/security.dart';
|
||||||
import 'package:weblibre/features/geckoview/domain/entities/states/tab.dart';
|
import 'package:weblibre/features/geckoview/domain/entities/states/tab.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/browser/features/contextual_toolbar/presentation/widgets/contextual_bar_buttons.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/browser/features/contextual_toolbar/presentation/widgets/contextual_toolbar.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/browser_modules/app_bar_title.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/browser_modules/app_bar_title.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/browser_modules/bottom_app_bar.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/browser_modules/bottom_app_bar.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/navigation_buttons.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/navigation_buttons.dart';
|
||||||
@@ -77,6 +80,7 @@ class _TabBarLayoutSection extends StatelessWidget {
|
|||||||
_TabBarLayoutModeSection(),
|
_TabBarLayoutModeSection(),
|
||||||
_ShowExtensionShortcutTile(),
|
_ShowExtensionShortcutTile(),
|
||||||
_ShowContextualTabBarTile(),
|
_ShowContextualTabBarTile(),
|
||||||
|
_CustomizeToolbarButtonsTile(),
|
||||||
_AutoHideTabBarTile(),
|
_AutoHideTabBarTile(),
|
||||||
_ShowQuickTabSwitcherBarTile(),
|
_ShowQuickTabSwitcherBarTile(),
|
||||||
_QuickTabSwitcherModeSection(),
|
_QuickTabSwitcherModeSection(),
|
||||||
@@ -232,24 +236,22 @@ class _TabBarPreviewCard extends HookWidget {
|
|||||||
|
|
||||||
Widget buildContextualToolbar() {
|
Widget buildContextualToolbar() {
|
||||||
return ContextualToolbarView(
|
return ContextualToolbarView(
|
||||||
canGoBack: true,
|
buttons: [
|
||||||
canGoForward: false,
|
NavigateBackButtonView(
|
||||||
onBookmarksTap: () {},
|
|
||||||
backButton: NavigateBackButtonView(
|
|
||||||
canGoBack: true,
|
canGoBack: true,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
onPressed: () {},
|
onPressed: () {},
|
||||||
onLongPress: () {},
|
onLongPress: () {},
|
||||||
),
|
),
|
||||||
forwardButton: NavigateForwardButtonView(
|
NavigateForwardButtonView(
|
||||||
canGoForward: true,
|
canGoForward: true,
|
||||||
onPressed: () {},
|
onPressed: () {},
|
||||||
onLongPress: () {},
|
onLongPress: () {},
|
||||||
),
|
),
|
||||||
shareButton: ShareMenuButtonView(onPressed: () {}),
|
AddTabButtonView(onPressed: () {}, onLongPress: () {}),
|
||||||
addTabButton: AddTabButtonView(onPressed: () {}, onLongPress: () {}),
|
tabCountButton,
|
||||||
tabsCountButton: tabCountButton,
|
NavigationMenuButtonView(onTap: () {}),
|
||||||
navigationButton: NavigationMenuButtonView(onTap: () {}),
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -614,6 +616,29 @@ class _ShowContextualTabBarTile extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _CustomizeToolbarButtonsTile extends HookConsumerWidget {
|
||||||
|
const _CustomizeToolbarButtonsTile();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final tabBarShowContextualBar = ref.watch(
|
||||||
|
generalSettingsWithDefaultsProvider.select(
|
||||||
|
(s) => s.tabBarShowContextualBar,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return ListTile(
|
||||||
|
leading: const Icon(Icons.tune),
|
||||||
|
title: const Text('Customize Toolbar Buttons'),
|
||||||
|
trailing: const Icon(Icons.chevron_right),
|
||||||
|
enabled: tabBarShowContextualBar,
|
||||||
|
onTap: () async {
|
||||||
|
await const ContextualToolbarSettingsRoute().push(context);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _ShowExtensionShortcutTile extends HookConsumerWidget {
|
class _ShowExtensionShortcutTile extends HookConsumerWidget {
|
||||||
const _ShowExtensionShortcutTile();
|
const _ShowExtensionShortcutTile();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
/*
|
||||||
|
* 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:drift/drift.dart';
|
||||||
|
import 'package:weblibre/features/user/data/database/daos/toolbar_button_config.drift.dart';
|
||||||
|
import 'package:weblibre/features/user/data/database/database.dart';
|
||||||
|
import 'package:weblibre/features/user/data/database/definitions.drift.dart';
|
||||||
|
|
||||||
|
@DriftAccessor()
|
||||||
|
class ToolbarButtonConfigDao extends DatabaseAccessor<UserDatabase>
|
||||||
|
with $ToolbarButtonConfigDaoMixin {
|
||||||
|
ToolbarButtonConfigDao(super.attachedDatabase);
|
||||||
|
|
||||||
|
Selectable<ToolbarButtonConfig> selectAll() =>
|
||||||
|
db.toolbarButtonConfigs.select()
|
||||||
|
..orderBy([(t) => OrderingTerm.asc(t.orderKey)]);
|
||||||
|
|
||||||
|
Stream<List<ToolbarButtonConfig>> watchAll() => selectAll().watch();
|
||||||
|
|
||||||
|
Future<List<ToolbarButtonConfig>> getAll() => selectAll().get();
|
||||||
|
|
||||||
|
Future<void> upsert(ToolbarButtonConfig config) =>
|
||||||
|
into(db.toolbarButtonConfigs).insertOnConflictUpdate(config);
|
||||||
|
|
||||||
|
Future<void> assignOrderKey(String buttonId, {required String orderKey}) =>
|
||||||
|
(update(db.toolbarButtonConfigs)
|
||||||
|
..where((t) => t.buttonId.equals(buttonId)))
|
||||||
|
.write(ToolbarButtonConfigsCompanion(orderKey: Value(orderKey)));
|
||||||
|
|
||||||
|
Future<void> assignVisibility(String buttonId, {required bool visible}) =>
|
||||||
|
(update(db.toolbarButtonConfigs)
|
||||||
|
..where((t) => t.buttonId.equals(buttonId)))
|
||||||
|
.write(ToolbarButtonConfigsCompanion(isVisible: Value(visible)));
|
||||||
|
|
||||||
|
Future<void> assignFallback(String buttonId, String? fallbackId) =>
|
||||||
|
(update(db.toolbarButtonConfigs)
|
||||||
|
..where((t) => t.buttonId.equals(buttonId)))
|
||||||
|
.write(ToolbarButtonConfigsCompanion(fallbackId: Value(fallbackId)));
|
||||||
|
|
||||||
|
Future<void> replaceAll(List<ToolbarButtonConfig> configs) =>
|
||||||
|
transaction(() async {
|
||||||
|
await delete(db.toolbarButtonConfigs).go();
|
||||||
|
await _insertWithDeferredFallbacks(configs);
|
||||||
|
});
|
||||||
|
|
||||||
|
Future<void> seedMissing(
|
||||||
|
List<({String buttonId, bool defaultVisible, String? defaultFallback})>
|
||||||
|
defaults,
|
||||||
|
) async {
|
||||||
|
final existing = await getAll();
|
||||||
|
final existingIds = {for (final r in existing) r.buttonId};
|
||||||
|
|
||||||
|
final missing = defaults
|
||||||
|
.where((d) => !existingIds.contains(d.buttonId))
|
||||||
|
.toList();
|
||||||
|
if (missing.isEmpty) return;
|
||||||
|
|
||||||
|
await transaction(() async {
|
||||||
|
final inserted = <ToolbarButtonConfig>[];
|
||||||
|
for (final def in missing) {
|
||||||
|
final orderKey = await generateTrailingOrderKey().getSingle();
|
||||||
|
inserted.add(
|
||||||
|
ToolbarButtonConfig(
|
||||||
|
buttonId: def.buttonId,
|
||||||
|
orderKey: orderKey,
|
||||||
|
isVisible: def.defaultVisible,
|
||||||
|
fallbackId: def.defaultFallback,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await into(db.toolbarButtonConfigs).insert(
|
||||||
|
ToolbarButtonConfig(
|
||||||
|
buttonId: def.buttonId,
|
||||||
|
orderKey: orderKey,
|
||||||
|
isVisible: def.defaultVisible,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await _assignFallbacks(inserted);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
SingleSelectable<String> generateLeadingOrderKey({int bucket = 0}) =>
|
||||||
|
db.definitionsDrift.toolbarLeadingOrderKey(bucket: bucket);
|
||||||
|
|
||||||
|
SingleSelectable<String> generateTrailingOrderKey({int bucket = 0}) =>
|
||||||
|
db.definitionsDrift.toolbarTrailingOrderKey(bucket: bucket);
|
||||||
|
|
||||||
|
SingleOrNullSelectable<String> generateOrderKeyAfterButtonId(
|
||||||
|
String buttonId,
|
||||||
|
) => db.definitionsDrift.toolbarOrderKeyAfterButton(buttonId: buttonId);
|
||||||
|
|
||||||
|
SingleSelectable<String> generateOrderKeyBeforeButtonId(String buttonId) =>
|
||||||
|
db.definitionsDrift.toolbarOrderKeyBeforeButton(buttonId: buttonId);
|
||||||
|
|
||||||
|
Future<void> _insertWithDeferredFallbacks(
|
||||||
|
List<ToolbarButtonConfig> configs,
|
||||||
|
) async {
|
||||||
|
for (final config in configs) {
|
||||||
|
await into(db.toolbarButtonConfigs).insert(
|
||||||
|
ToolbarButtonConfig(
|
||||||
|
buttonId: config.buttonId,
|
||||||
|
orderKey: config.orderKey,
|
||||||
|
isVisible: config.isVisible,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await _assignFallbacks(configs);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _assignFallbacks(List<ToolbarButtonConfig> configs) async {
|
||||||
|
for (final config in configs) {
|
||||||
|
if (config.fallbackId == null) continue;
|
||||||
|
await assignFallback(config.buttonId, config.fallbackId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
// dart format width=80
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
import 'package:drift/drift.dart' as i0;
|
||||||
|
import 'package:weblibre/features/user/data/database/database.dart' as i1;
|
||||||
|
|
||||||
|
mixin $ToolbarButtonConfigDaoMixin on i0.DatabaseAccessor<i1.UserDatabase> {
|
||||||
|
ToolbarButtonConfigDaoManager get managers =>
|
||||||
|
ToolbarButtonConfigDaoManager(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
class ToolbarButtonConfigDaoManager {
|
||||||
|
final $ToolbarButtonConfigDaoMixin _db;
|
||||||
|
ToolbarButtonConfigDaoManager(this._db);
|
||||||
|
}
|
||||||
@@ -24,16 +24,17 @@ import 'package:flutter/foundation.dart';
|
|||||||
import 'package:weblibre/features/user/data/database/daos/cache.dart';
|
import 'package:weblibre/features/user/data/database/daos/cache.dart';
|
||||||
import 'package:weblibre/features/user/data/database/daos/onboarding.dart';
|
import 'package:weblibre/features/user/data/database/daos/onboarding.dart';
|
||||||
import 'package:weblibre/features/user/data/database/daos/setting.dart';
|
import 'package:weblibre/features/user/data/database/daos/setting.dart';
|
||||||
|
import 'package:weblibre/features/user/data/database/daos/toolbar_button_config.dart';
|
||||||
import 'package:weblibre/features/user/data/database/database.drift.dart';
|
import 'package:weblibre/features/user/data/database/database.drift.dart';
|
||||||
import 'package:weblibre/features/user/data/database/database.steps.dart';
|
import 'package:weblibre/features/user/data/database/database.steps.dart';
|
||||||
|
|
||||||
@DriftDatabase(
|
@DriftDatabase(
|
||||||
include: {'definitions.drift'},
|
include: {'definitions.drift'},
|
||||||
daos: [SettingDao, CacheDao, OnboardingDao],
|
daos: [SettingDao, CacheDao, OnboardingDao, ToolbarButtonConfigDao],
|
||||||
)
|
)
|
||||||
class UserDatabase extends $UserDatabase {
|
class UserDatabase extends $UserDatabase {
|
||||||
@override
|
@override
|
||||||
final int schemaVersion = 2;
|
final int schemaVersion = 3;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
MigrationStrategy get migration => MigrationStrategy(
|
MigrationStrategy get migration => MigrationStrategy(
|
||||||
@@ -45,6 +46,8 @@ class UserDatabase extends $UserDatabase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await customStatement('PRAGMA foreign_keys = ON;');
|
await customStatement('PRAGMA foreign_keys = ON;');
|
||||||
|
|
||||||
|
await onAfterOpen?.call(this);
|
||||||
},
|
},
|
||||||
onUpgrade: (m, from, to) async {
|
onUpgrade: (m, from, to) async {
|
||||||
// Following the advice from https://drift.simonbinder.eu/Migrations/api/#general-tips
|
// Following the advice from https://drift.simonbinder.eu/Migrations/api/#general-tips
|
||||||
@@ -73,11 +76,17 @@ class UserDatabase extends $UserDatabase {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
UserDatabase(super.e);
|
UserDatabase(super.e, {this.onAfterOpen});
|
||||||
|
|
||||||
|
final Future<void> Function(UserDatabase db)? onAfterOpen;
|
||||||
|
|
||||||
static final _upgrade = migrationSteps(
|
static final _upgrade = migrationSteps(
|
||||||
from1To2: (m, schema) async {
|
from1To2: (m, schema) async {
|
||||||
await m.createTable(schema.riverpod);
|
await m.createTable(schema.riverpod);
|
||||||
},
|
},
|
||||||
|
from2To3: (m, schema) async {
|
||||||
|
await m.createTable(schema.toolbarButtonConfigs);
|
||||||
|
await m.createIndex(schema.idxToolbarOrderKey);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,8 +8,10 @@ import 'package:weblibre/features/user/data/database/database.dart' as i3;
|
|||||||
import 'package:weblibre/features/user/data/database/daos/cache.dart' as i4;
|
import 'package:weblibre/features/user/data/database/daos/cache.dart' as i4;
|
||||||
import 'package:weblibre/features/user/data/database/daos/onboarding.dart'
|
import 'package:weblibre/features/user/data/database/daos/onboarding.dart'
|
||||||
as i5;
|
as i5;
|
||||||
import 'package:drift/internal/modular.dart' as i6;
|
import 'package:weblibre/features/user/data/database/daos/toolbar_button_config.dart'
|
||||||
import 'package:sqlite3/common.dart' as i7;
|
as i6;
|
||||||
|
import 'package:drift/internal/modular.dart' as i7;
|
||||||
|
import 'package:sqlite3/common.dart' as i8;
|
||||||
|
|
||||||
abstract class $UserDatabase extends i0.GeneratedDatabase {
|
abstract class $UserDatabase extends i0.GeneratedDatabase {
|
||||||
$UserDatabase(i0.QueryExecutor e) : super(e);
|
$UserDatabase(i0.QueryExecutor e) : super(e);
|
||||||
@@ -18,12 +20,16 @@ abstract class $UserDatabase extends i0.GeneratedDatabase {
|
|||||||
late final i1.IconCache iconCache = i1.IconCache(this);
|
late final i1.IconCache iconCache = i1.IconCache(this);
|
||||||
late final i1.Onboarding onboarding = i1.Onboarding(this);
|
late final i1.Onboarding onboarding = i1.Onboarding(this);
|
||||||
late final i1.Riverpod riverpod = i1.Riverpod(this);
|
late final i1.Riverpod riverpod = i1.Riverpod(this);
|
||||||
|
late final i1.ToolbarButtonConfigs toolbarButtonConfigs =
|
||||||
|
i1.ToolbarButtonConfigs(this);
|
||||||
late final i2.SettingDao settingDao = i2.SettingDao(this as i3.UserDatabase);
|
late final i2.SettingDao settingDao = i2.SettingDao(this as i3.UserDatabase);
|
||||||
late final i4.CacheDao cacheDao = i4.CacheDao(this as i3.UserDatabase);
|
late final i4.CacheDao cacheDao = i4.CacheDao(this as i3.UserDatabase);
|
||||||
late final i5.OnboardingDao onboardingDao = i5.OnboardingDao(
|
late final i5.OnboardingDao onboardingDao = i5.OnboardingDao(
|
||||||
this as i3.UserDatabase,
|
this as i3.UserDatabase,
|
||||||
);
|
);
|
||||||
i1.DefinitionsDrift get definitionsDrift => i6.ReadDatabaseContainer(
|
late final i6.ToolbarButtonConfigDao toolbarButtonConfigDao =
|
||||||
|
i6.ToolbarButtonConfigDao(this as i3.UserDatabase);
|
||||||
|
i1.DefinitionsDrift get definitionsDrift => i7.ReadDatabaseContainer(
|
||||||
this,
|
this,
|
||||||
).accessor<i1.DefinitionsDrift>(i1.DefinitionsDrift.new);
|
).accessor<i1.DefinitionsDrift>(i1.DefinitionsDrift.new);
|
||||||
@override
|
@override
|
||||||
@@ -35,6 +41,8 @@ abstract class $UserDatabase extends i0.GeneratedDatabase {
|
|||||||
iconCache,
|
iconCache,
|
||||||
onboarding,
|
onboarding,
|
||||||
riverpod,
|
riverpod,
|
||||||
|
toolbarButtonConfigs,
|
||||||
|
i1.idxToolbarOrderKey,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,9 +57,11 @@ class $UserDatabaseManager {
|
|||||||
i1.$OnboardingTableManager(_db, _db.onboarding);
|
i1.$OnboardingTableManager(_db, _db.onboarding);
|
||||||
i1.$RiverpodTableManager get riverpod =>
|
i1.$RiverpodTableManager get riverpod =>
|
||||||
i1.$RiverpodTableManager(_db, _db.riverpod);
|
i1.$RiverpodTableManager(_db, _db.riverpod);
|
||||||
|
i1.$ToolbarButtonConfigsTableManager get toolbarButtonConfigs =>
|
||||||
|
i1.$ToolbarButtonConfigsTableManager(_db, _db.toolbarButtonConfigs);
|
||||||
}
|
}
|
||||||
|
|
||||||
extension DefineFunctions on i7.CommonDatabase {
|
extension DefineFunctions on i8.CommonDatabase {
|
||||||
void defineFunctions({
|
void defineFunctions({
|
||||||
required String Function(int, String?) lexoRankNext,
|
required String Function(int, String?) lexoRankNext,
|
||||||
required String Function(int, String?) lexoRankPrevious,
|
required String Function(int, String?) lexoRankPrevious,
|
||||||
@@ -60,7 +70,7 @@ extension DefineFunctions on i7.CommonDatabase {
|
|||||||
}) {
|
}) {
|
||||||
createFunction(
|
createFunction(
|
||||||
functionName: 'lexo_rank_next',
|
functionName: 'lexo_rank_next',
|
||||||
argumentCount: const i7.AllowedArgumentCount(2),
|
argumentCount: const i8.AllowedArgumentCount(2),
|
||||||
function: (args) {
|
function: (args) {
|
||||||
final arg0 = args[0] as int;
|
final arg0 = args[0] as int;
|
||||||
final arg1 = args[1] as String?;
|
final arg1 = args[1] as String?;
|
||||||
@@ -69,7 +79,7 @@ extension DefineFunctions on i7.CommonDatabase {
|
|||||||
);
|
);
|
||||||
createFunction(
|
createFunction(
|
||||||
functionName: 'lexo_rank_previous',
|
functionName: 'lexo_rank_previous',
|
||||||
argumentCount: const i7.AllowedArgumentCount(2),
|
argumentCount: const i8.AllowedArgumentCount(2),
|
||||||
function: (args) {
|
function: (args) {
|
||||||
final arg0 = args[0] as int;
|
final arg0 = args[0] as int;
|
||||||
final arg1 = args[1] as String?;
|
final arg1 = args[1] as String?;
|
||||||
@@ -78,7 +88,7 @@ extension DefineFunctions on i7.CommonDatabase {
|
|||||||
);
|
);
|
||||||
createFunction(
|
createFunction(
|
||||||
functionName: 'lexo_rank_reorder_after',
|
functionName: 'lexo_rank_reorder_after',
|
||||||
argumentCount: const i7.AllowedArgumentCount(2),
|
argumentCount: const i8.AllowedArgumentCount(2),
|
||||||
function: (args) {
|
function: (args) {
|
||||||
final arg0 = args[0] as String?;
|
final arg0 = args[0] as String?;
|
||||||
final arg1 = args[1] as String?;
|
final arg1 = args[1] as String?;
|
||||||
@@ -87,7 +97,7 @@ extension DefineFunctions on i7.CommonDatabase {
|
|||||||
);
|
);
|
||||||
createFunction(
|
createFunction(
|
||||||
functionName: 'lexo_rank_reorder_before',
|
functionName: 'lexo_rank_reorder_before',
|
||||||
argumentCount: const i7.AllowedArgumentCount(2),
|
argumentCount: const i8.AllowedArgumentCount(2),
|
||||||
function: (args) {
|
function: (args) {
|
||||||
final arg0 = args[0] as String?;
|
final arg0 = args[0] as String?;
|
||||||
final arg1 = args[1] as String?;
|
final arg1 = args[1] as String?;
|
||||||
|
|||||||
@@ -192,8 +192,128 @@ i1.GeneratedColumn<String> _column_10(String aliasedName) =>
|
|||||||
type: i1.DriftSqlType.string,
|
type: i1.DriftSqlType.string,
|
||||||
$customConstraints: '',
|
$customConstraints: '',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final class Schema3 extends i0.VersionedSchema {
|
||||||
|
Schema3({required super.database}) : super(version: 3);
|
||||||
|
@override
|
||||||
|
late final List<i1.DatabaseSchemaEntity> entities = [
|
||||||
|
setting,
|
||||||
|
iconCache,
|
||||||
|
onboarding,
|
||||||
|
riverpod,
|
||||||
|
toolbarButtonConfigs,
|
||||||
|
idxToolbarOrderKey,
|
||||||
|
];
|
||||||
|
late final Shape0 setting = Shape0(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'setting',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: [],
|
||||||
|
columns: [_column_0, _column_1, _column_2],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape1 iconCache = Shape1(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'icon_cache',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: [],
|
||||||
|
columns: [_column_3, _column_4, _column_5],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape2 onboarding = Shape2(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'onboarding',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: [],
|
||||||
|
columns: [_column_6, _column_7],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape3 riverpod = Shape3(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'riverpod',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: [],
|
||||||
|
columns: [_column_0, _column_8, _column_9, _column_10],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape4 toolbarButtonConfigs = Shape4(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'toolbar_button_configs',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: [],
|
||||||
|
columns: [_column_11, _column_12, _column_13, _column_14],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
final i1.Index idxToolbarOrderKey = i1.Index(
|
||||||
|
'idx_toolbar_order_key',
|
||||||
|
'CREATE INDEX idx_toolbar_order_key ON toolbar_button_configs (order_key)',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class Shape4 extends i0.VersionedTable {
|
||||||
|
Shape4({required super.source, required super.alias}) : super.aliased();
|
||||||
|
i1.GeneratedColumn<String> get buttonId =>
|
||||||
|
columnsByName['button_id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get orderKey =>
|
||||||
|
columnsByName['order_key']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<int> get isVisible =>
|
||||||
|
columnsByName['is_visible']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<String> get fallbackId =>
|
||||||
|
columnsByName['fallback_id']! as i1.GeneratedColumn<String>;
|
||||||
|
}
|
||||||
|
|
||||||
|
i1.GeneratedColumn<String> _column_11(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>(
|
||||||
|
'button_id',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
$customConstraints: 'NOT NULL PRIMARY KEY',
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<String> _column_12(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>(
|
||||||
|
'order_key',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
$customConstraints: 'NOT NULL',
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<int> _column_13(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<int>(
|
||||||
|
'is_visible',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i1.DriftSqlType.int,
|
||||||
|
$customConstraints: 'NOT NULL DEFAULT TRUE',
|
||||||
|
defaultValue: const i1.CustomExpression('TRUE'),
|
||||||
|
);
|
||||||
|
i1.GeneratedColumn<String> _column_14(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>(
|
||||||
|
'fallback_id',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
$customConstraints:
|
||||||
|
'REFERENCES toolbar_button_configs(button_id)ON DELETE SET NULL',
|
||||||
|
);
|
||||||
i0.MigrationStepWithVersion migrationSteps({
|
i0.MigrationStepWithVersion migrationSteps({
|
||||||
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
|
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
|
||||||
|
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
|
||||||
}) {
|
}) {
|
||||||
return (currentVersion, database) async {
|
return (currentVersion, database) async {
|
||||||
switch (currentVersion) {
|
switch (currentVersion) {
|
||||||
@@ -202,6 +322,11 @@ i0.MigrationStepWithVersion migrationSteps({
|
|||||||
final migrator = i1.Migrator(database, schema);
|
final migrator = i1.Migrator(database, schema);
|
||||||
await from1To2(migrator, schema);
|
await from1To2(migrator, schema);
|
||||||
return 2;
|
return 2;
|
||||||
|
case 2:
|
||||||
|
final schema = Schema3(database: database);
|
||||||
|
final migrator = i1.Migrator(database, schema);
|
||||||
|
await from2To3(migrator, schema);
|
||||||
|
return 3;
|
||||||
default:
|
default:
|
||||||
throw ArgumentError.value('Unknown migration from $currentVersion');
|
throw ArgumentError.value('Unknown migration from $currentVersion');
|
||||||
}
|
}
|
||||||
@@ -210,6 +335,7 @@ i0.MigrationStepWithVersion migrationSteps({
|
|||||||
|
|
||||||
i1.OnUpgrade stepByStep({
|
i1.OnUpgrade stepByStep({
|
||||||
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
|
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
|
||||||
|
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
|
||||||
}) => i0.VersionedSchema.stepByStepHelper(
|
}) => i0.VersionedSchema.stepByStepHelper(
|
||||||
step: migrationSteps(from1To2: from1To2),
|
step: migrationSteps(from1To2: from1To2, from2To3: from2To3),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -22,6 +22,61 @@ CREATE TABLE riverpod (
|
|||||||
destroyKey TEXT
|
destroyKey TEXT
|
||||||
) WITHOUT ROWID;
|
) WITHOUT ROWID;
|
||||||
|
|
||||||
|
CREATE TABLE toolbar_button_configs (
|
||||||
|
button_id TEXT NOT NULL PRIMARY KEY,
|
||||||
|
order_key TEXT NOT NULL,
|
||||||
|
is_visible BOOLEAN NOT NULL DEFAULT TRUE,
|
||||||
|
fallback_id TEXT REFERENCES toolbar_button_configs(button_id) ON DELETE SET NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_toolbar_order_key ON toolbar_button_configs(order_key);
|
||||||
|
|
||||||
|
toolbarLeadingOrderKey(:bucket AS INTEGER):
|
||||||
|
SELECT lexo_rank_previous(
|
||||||
|
:bucket,
|
||||||
|
(
|
||||||
|
SELECT order_key
|
||||||
|
FROM toolbar_button_configs
|
||||||
|
ORDER BY order_key
|
||||||
|
LIMIT 1
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
toolbarTrailingOrderKey(:bucket AS INTEGER):
|
||||||
|
SELECT lexo_rank_next(
|
||||||
|
:bucket,
|
||||||
|
(
|
||||||
|
SELECT order_key
|
||||||
|
FROM toolbar_button_configs
|
||||||
|
ORDER BY order_key DESC
|
||||||
|
LIMIT 1
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
toolbarOrderKeyAfterButton(:button_id AS TEXT):
|
||||||
|
WITH ordered_table AS (
|
||||||
|
SELECT
|
||||||
|
button_id,
|
||||||
|
order_key,
|
||||||
|
LEAD(order_key) OVER (ORDER BY order_key) AS next_order_key
|
||||||
|
FROM toolbar_button_configs
|
||||||
|
)
|
||||||
|
SELECT lexo_rank_reorder_after(order_key, next_order_key)
|
||||||
|
FROM ordered_table
|
||||||
|
WHERE button_id = :button_id;
|
||||||
|
|
||||||
|
toolbarOrderKeyBeforeButton(:button_id AS TEXT):
|
||||||
|
WITH ordered_table AS (
|
||||||
|
SELECT
|
||||||
|
button_id,
|
||||||
|
order_key,
|
||||||
|
LAG(order_key) OVER (ORDER BY order_key) AS prev_order_key
|
||||||
|
FROM toolbar_button_configs
|
||||||
|
)
|
||||||
|
SELECT lexo_rank_reorder_before(order_key, prev_order_key)
|
||||||
|
FROM ordered_table
|
||||||
|
WHERE button_id = :button_id;
|
||||||
|
|
||||||
evictCacheEntries:
|
evictCacheEntries:
|
||||||
DELETE FROM icon_cache
|
DELETE FROM icon_cache
|
||||||
WHERE rowid IN (
|
WHERE rowid IN (
|
||||||
|
|||||||
@@ -670,6 +670,199 @@ typedef $RiverpodProcessedTableManager =
|
|||||||
i1.RiverpodData,
|
i1.RiverpodData,
|
||||||
i0.PrefetchHooks Function()
|
i0.PrefetchHooks Function()
|
||||||
>;
|
>;
|
||||||
|
typedef $ToolbarButtonConfigsCreateCompanionBuilder =
|
||||||
|
i1.ToolbarButtonConfigsCompanion Function({
|
||||||
|
required String buttonId,
|
||||||
|
required String orderKey,
|
||||||
|
i0.Value<bool> isVisible,
|
||||||
|
i0.Value<String?> fallbackId,
|
||||||
|
i0.Value<int> rowid,
|
||||||
|
});
|
||||||
|
typedef $ToolbarButtonConfigsUpdateCompanionBuilder =
|
||||||
|
i1.ToolbarButtonConfigsCompanion Function({
|
||||||
|
i0.Value<String> buttonId,
|
||||||
|
i0.Value<String> orderKey,
|
||||||
|
i0.Value<bool> isVisible,
|
||||||
|
i0.Value<String?> fallbackId,
|
||||||
|
i0.Value<int> rowid,
|
||||||
|
});
|
||||||
|
|
||||||
|
class $ToolbarButtonConfigsFilterComposer
|
||||||
|
extends i0.Composer<i0.GeneratedDatabase, i1.ToolbarButtonConfigs> {
|
||||||
|
$ToolbarButtonConfigsFilterComposer({
|
||||||
|
required super.$db,
|
||||||
|
required super.$table,
|
||||||
|
super.joinBuilder,
|
||||||
|
super.$addJoinBuilderToRootComposer,
|
||||||
|
super.$removeJoinBuilderFromRootComposer,
|
||||||
|
});
|
||||||
|
i0.ColumnFilters<String> get buttonId => $composableBuilder(
|
||||||
|
column: $table.buttonId,
|
||||||
|
builder: (column) => i0.ColumnFilters(column),
|
||||||
|
);
|
||||||
|
|
||||||
|
i0.ColumnFilters<String> get orderKey => $composableBuilder(
|
||||||
|
column: $table.orderKey,
|
||||||
|
builder: (column) => i0.ColumnFilters(column),
|
||||||
|
);
|
||||||
|
|
||||||
|
i0.ColumnFilters<bool> get isVisible => $composableBuilder(
|
||||||
|
column: $table.isVisible,
|
||||||
|
builder: (column) => i0.ColumnFilters(column),
|
||||||
|
);
|
||||||
|
|
||||||
|
i0.ColumnFilters<String> get fallbackId => $composableBuilder(
|
||||||
|
column: $table.fallbackId,
|
||||||
|
builder: (column) => i0.ColumnFilters(column),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class $ToolbarButtonConfigsOrderingComposer
|
||||||
|
extends i0.Composer<i0.GeneratedDatabase, i1.ToolbarButtonConfigs> {
|
||||||
|
$ToolbarButtonConfigsOrderingComposer({
|
||||||
|
required super.$db,
|
||||||
|
required super.$table,
|
||||||
|
super.joinBuilder,
|
||||||
|
super.$addJoinBuilderToRootComposer,
|
||||||
|
super.$removeJoinBuilderFromRootComposer,
|
||||||
|
});
|
||||||
|
i0.ColumnOrderings<String> get buttonId => $composableBuilder(
|
||||||
|
column: $table.buttonId,
|
||||||
|
builder: (column) => i0.ColumnOrderings(column),
|
||||||
|
);
|
||||||
|
|
||||||
|
i0.ColumnOrderings<String> get orderKey => $composableBuilder(
|
||||||
|
column: $table.orderKey,
|
||||||
|
builder: (column) => i0.ColumnOrderings(column),
|
||||||
|
);
|
||||||
|
|
||||||
|
i0.ColumnOrderings<bool> get isVisible => $composableBuilder(
|
||||||
|
column: $table.isVisible,
|
||||||
|
builder: (column) => i0.ColumnOrderings(column),
|
||||||
|
);
|
||||||
|
|
||||||
|
i0.ColumnOrderings<String> get fallbackId => $composableBuilder(
|
||||||
|
column: $table.fallbackId,
|
||||||
|
builder: (column) => i0.ColumnOrderings(column),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class $ToolbarButtonConfigsAnnotationComposer
|
||||||
|
extends i0.Composer<i0.GeneratedDatabase, i1.ToolbarButtonConfigs> {
|
||||||
|
$ToolbarButtonConfigsAnnotationComposer({
|
||||||
|
required super.$db,
|
||||||
|
required super.$table,
|
||||||
|
super.joinBuilder,
|
||||||
|
super.$addJoinBuilderToRootComposer,
|
||||||
|
super.$removeJoinBuilderFromRootComposer,
|
||||||
|
});
|
||||||
|
i0.GeneratedColumn<String> get buttonId =>
|
||||||
|
$composableBuilder(column: $table.buttonId, builder: (column) => column);
|
||||||
|
|
||||||
|
i0.GeneratedColumn<String> get orderKey =>
|
||||||
|
$composableBuilder(column: $table.orderKey, builder: (column) => column);
|
||||||
|
|
||||||
|
i0.GeneratedColumn<bool> get isVisible =>
|
||||||
|
$composableBuilder(column: $table.isVisible, builder: (column) => column);
|
||||||
|
|
||||||
|
i0.GeneratedColumn<String> get fallbackId => $composableBuilder(
|
||||||
|
column: $table.fallbackId,
|
||||||
|
builder: (column) => column,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class $ToolbarButtonConfigsTableManager
|
||||||
|
extends
|
||||||
|
i0.RootTableManager<
|
||||||
|
i0.GeneratedDatabase,
|
||||||
|
i1.ToolbarButtonConfigs,
|
||||||
|
i1.ToolbarButtonConfig,
|
||||||
|
i1.$ToolbarButtonConfigsFilterComposer,
|
||||||
|
i1.$ToolbarButtonConfigsOrderingComposer,
|
||||||
|
i1.$ToolbarButtonConfigsAnnotationComposer,
|
||||||
|
$ToolbarButtonConfigsCreateCompanionBuilder,
|
||||||
|
$ToolbarButtonConfigsUpdateCompanionBuilder,
|
||||||
|
(
|
||||||
|
i1.ToolbarButtonConfig,
|
||||||
|
i0.BaseReferences<
|
||||||
|
i0.GeneratedDatabase,
|
||||||
|
i1.ToolbarButtonConfigs,
|
||||||
|
i1.ToolbarButtonConfig
|
||||||
|
>,
|
||||||
|
),
|
||||||
|
i1.ToolbarButtonConfig,
|
||||||
|
i0.PrefetchHooks Function()
|
||||||
|
> {
|
||||||
|
$ToolbarButtonConfigsTableManager(
|
||||||
|
i0.GeneratedDatabase db,
|
||||||
|
i1.ToolbarButtonConfigs table,
|
||||||
|
) : super(
|
||||||
|
i0.TableManagerState(
|
||||||
|
db: db,
|
||||||
|
table: table,
|
||||||
|
createFilteringComposer: () =>
|
||||||
|
i1.$ToolbarButtonConfigsFilterComposer($db: db, $table: table),
|
||||||
|
createOrderingComposer: () =>
|
||||||
|
i1.$ToolbarButtonConfigsOrderingComposer($db: db, $table: table),
|
||||||
|
createComputedFieldComposer: () => i1
|
||||||
|
.$ToolbarButtonConfigsAnnotationComposer($db: db, $table: table),
|
||||||
|
updateCompanionCallback:
|
||||||
|
({
|
||||||
|
i0.Value<String> buttonId = const i0.Value.absent(),
|
||||||
|
i0.Value<String> orderKey = const i0.Value.absent(),
|
||||||
|
i0.Value<bool> isVisible = const i0.Value.absent(),
|
||||||
|
i0.Value<String?> fallbackId = const i0.Value.absent(),
|
||||||
|
i0.Value<int> rowid = const i0.Value.absent(),
|
||||||
|
}) => i1.ToolbarButtonConfigsCompanion(
|
||||||
|
buttonId: buttonId,
|
||||||
|
orderKey: orderKey,
|
||||||
|
isVisible: isVisible,
|
||||||
|
fallbackId: fallbackId,
|
||||||
|
rowid: rowid,
|
||||||
|
),
|
||||||
|
createCompanionCallback:
|
||||||
|
({
|
||||||
|
required String buttonId,
|
||||||
|
required String orderKey,
|
||||||
|
i0.Value<bool> isVisible = const i0.Value.absent(),
|
||||||
|
i0.Value<String?> fallbackId = const i0.Value.absent(),
|
||||||
|
i0.Value<int> rowid = const i0.Value.absent(),
|
||||||
|
}) => i1.ToolbarButtonConfigsCompanion.insert(
|
||||||
|
buttonId: buttonId,
|
||||||
|
orderKey: orderKey,
|
||||||
|
isVisible: isVisible,
|
||||||
|
fallbackId: fallbackId,
|
||||||
|
rowid: rowid,
|
||||||
|
),
|
||||||
|
withReferenceMapper: (p0) => p0
|
||||||
|
.map((e) => (e.readTable(table), i0.BaseReferences(db, table, e)))
|
||||||
|
.toList(),
|
||||||
|
prefetchHooksCallback: null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef $ToolbarButtonConfigsProcessedTableManager =
|
||||||
|
i0.ProcessedTableManager<
|
||||||
|
i0.GeneratedDatabase,
|
||||||
|
i1.ToolbarButtonConfigs,
|
||||||
|
i1.ToolbarButtonConfig,
|
||||||
|
i1.$ToolbarButtonConfigsFilterComposer,
|
||||||
|
i1.$ToolbarButtonConfigsOrderingComposer,
|
||||||
|
i1.$ToolbarButtonConfigsAnnotationComposer,
|
||||||
|
$ToolbarButtonConfigsCreateCompanionBuilder,
|
||||||
|
$ToolbarButtonConfigsUpdateCompanionBuilder,
|
||||||
|
(
|
||||||
|
i1.ToolbarButtonConfig,
|
||||||
|
i0.BaseReferences<
|
||||||
|
i0.GeneratedDatabase,
|
||||||
|
i1.ToolbarButtonConfigs,
|
||||||
|
i1.ToolbarButtonConfig
|
||||||
|
>,
|
||||||
|
),
|
||||||
|
i1.ToolbarButtonConfig,
|
||||||
|
i0.PrefetchHooks Function()
|
||||||
|
>;
|
||||||
|
|
||||||
class Setting extends i0.Table with i0.TableInfo<Setting, i1.SettingData> {
|
class Setting extends i0.Table with i0.TableInfo<Setting, i1.SettingData> {
|
||||||
@override
|
@override
|
||||||
@@ -1558,8 +1751,313 @@ class RiverpodCompanion extends i0.UpdateCompanion<i1.RiverpodData> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ToolbarButtonConfigs extends i0.Table
|
||||||
|
with i0.TableInfo<ToolbarButtonConfigs, i1.ToolbarButtonConfig> {
|
||||||
|
@override
|
||||||
|
final i0.GeneratedDatabase attachedDatabase;
|
||||||
|
final String? _alias;
|
||||||
|
ToolbarButtonConfigs(this.attachedDatabase, [this._alias]);
|
||||||
|
late final i0.GeneratedColumn<String> buttonId = i0.GeneratedColumn<String>(
|
||||||
|
'button_id',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i0.DriftSqlType.string,
|
||||||
|
requiredDuringInsert: true,
|
||||||
|
$customConstraints: 'NOT NULL PRIMARY KEY',
|
||||||
|
);
|
||||||
|
late final i0.GeneratedColumn<String> orderKey = i0.GeneratedColumn<String>(
|
||||||
|
'order_key',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i0.DriftSqlType.string,
|
||||||
|
requiredDuringInsert: true,
|
||||||
|
$customConstraints: 'NOT NULL',
|
||||||
|
);
|
||||||
|
late final i0.GeneratedColumn<bool> isVisible = i0.GeneratedColumn<bool>(
|
||||||
|
'is_visible',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i0.DriftSqlType.bool,
|
||||||
|
requiredDuringInsert: false,
|
||||||
|
$customConstraints: 'NOT NULL DEFAULT TRUE',
|
||||||
|
defaultValue: const i0.CustomExpression('TRUE'),
|
||||||
|
);
|
||||||
|
late final i0.GeneratedColumn<String> fallbackId = i0.GeneratedColumn<String>(
|
||||||
|
'fallback_id',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: i0.DriftSqlType.string,
|
||||||
|
requiredDuringInsert: false,
|
||||||
|
$customConstraints:
|
||||||
|
'REFERENCES toolbar_button_configs(button_id)ON DELETE SET NULL',
|
||||||
|
);
|
||||||
|
@override
|
||||||
|
List<i0.GeneratedColumn> get $columns => [
|
||||||
|
buttonId,
|
||||||
|
orderKey,
|
||||||
|
isVisible,
|
||||||
|
fallbackId,
|
||||||
|
];
|
||||||
|
@override
|
||||||
|
String get aliasedName => _alias ?? actualTableName;
|
||||||
|
@override
|
||||||
|
String get actualTableName => $name;
|
||||||
|
static const String $name = 'toolbar_button_configs';
|
||||||
|
@override
|
||||||
|
Set<i0.GeneratedColumn> get $primaryKey => {buttonId};
|
||||||
|
@override
|
||||||
|
i1.ToolbarButtonConfig map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||||
|
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
|
||||||
|
return i1.ToolbarButtonConfig(
|
||||||
|
buttonId: attachedDatabase.typeMapping.read(
|
||||||
|
i0.DriftSqlType.string,
|
||||||
|
data['${effectivePrefix}button_id'],
|
||||||
|
)!,
|
||||||
|
orderKey: attachedDatabase.typeMapping.read(
|
||||||
|
i0.DriftSqlType.string,
|
||||||
|
data['${effectivePrefix}order_key'],
|
||||||
|
)!,
|
||||||
|
isVisible: attachedDatabase.typeMapping.read(
|
||||||
|
i0.DriftSqlType.bool,
|
||||||
|
data['${effectivePrefix}is_visible'],
|
||||||
|
)!,
|
||||||
|
fallbackId: attachedDatabase.typeMapping.read(
|
||||||
|
i0.DriftSqlType.string,
|
||||||
|
data['${effectivePrefix}fallback_id'],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
ToolbarButtonConfigs createAlias(String alias) {
|
||||||
|
return ToolbarButtonConfigs(attachedDatabase, alias);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get dontWriteConstraints => true;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ToolbarButtonConfig extends i0.DataClass
|
||||||
|
implements i0.Insertable<i1.ToolbarButtonConfig> {
|
||||||
|
final String buttonId;
|
||||||
|
final String orderKey;
|
||||||
|
final bool isVisible;
|
||||||
|
final String? fallbackId;
|
||||||
|
const ToolbarButtonConfig({
|
||||||
|
required this.buttonId,
|
||||||
|
required this.orderKey,
|
||||||
|
required this.isVisible,
|
||||||
|
this.fallbackId,
|
||||||
|
});
|
||||||
|
@override
|
||||||
|
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
|
||||||
|
final map = <String, i0.Expression>{};
|
||||||
|
map['button_id'] = i0.Variable<String>(buttonId);
|
||||||
|
map['order_key'] = i0.Variable<String>(orderKey);
|
||||||
|
map['is_visible'] = i0.Variable<bool>(isVisible);
|
||||||
|
if (!nullToAbsent || fallbackId != null) {
|
||||||
|
map['fallback_id'] = i0.Variable<String>(fallbackId);
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
factory ToolbarButtonConfig.fromJson(
|
||||||
|
Map<String, dynamic> json, {
|
||||||
|
i0.ValueSerializer? serializer,
|
||||||
|
}) {
|
||||||
|
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
|
||||||
|
return ToolbarButtonConfig(
|
||||||
|
buttonId: serializer.fromJson<String>(json['button_id']),
|
||||||
|
orderKey: serializer.fromJson<String>(json['order_key']),
|
||||||
|
isVisible: serializer.fromJson<bool>(json['is_visible']),
|
||||||
|
fallbackId: serializer.fromJson<String?>(json['fallback_id']),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson({i0.ValueSerializer? serializer}) {
|
||||||
|
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
|
||||||
|
return <String, dynamic>{
|
||||||
|
'button_id': serializer.toJson<String>(buttonId),
|
||||||
|
'order_key': serializer.toJson<String>(orderKey),
|
||||||
|
'is_visible': serializer.toJson<bool>(isVisible),
|
||||||
|
'fallback_id': serializer.toJson<String?>(fallbackId),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
i1.ToolbarButtonConfig copyWith({
|
||||||
|
String? buttonId,
|
||||||
|
String? orderKey,
|
||||||
|
bool? isVisible,
|
||||||
|
i0.Value<String?> fallbackId = const i0.Value.absent(),
|
||||||
|
}) => i1.ToolbarButtonConfig(
|
||||||
|
buttonId: buttonId ?? this.buttonId,
|
||||||
|
orderKey: orderKey ?? this.orderKey,
|
||||||
|
isVisible: isVisible ?? this.isVisible,
|
||||||
|
fallbackId: fallbackId.present ? fallbackId.value : this.fallbackId,
|
||||||
|
);
|
||||||
|
ToolbarButtonConfig copyWithCompanion(i1.ToolbarButtonConfigsCompanion data) {
|
||||||
|
return ToolbarButtonConfig(
|
||||||
|
buttonId: data.buttonId.present ? data.buttonId.value : this.buttonId,
|
||||||
|
orderKey: data.orderKey.present ? data.orderKey.value : this.orderKey,
|
||||||
|
isVisible: data.isVisible.present ? data.isVisible.value : this.isVisible,
|
||||||
|
fallbackId: data.fallbackId.present
|
||||||
|
? data.fallbackId.value
|
||||||
|
: this.fallbackId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return (StringBuffer('ToolbarButtonConfig(')
|
||||||
|
..write('buttonId: $buttonId, ')
|
||||||
|
..write('orderKey: $orderKey, ')
|
||||||
|
..write('isVisible: $isVisible, ')
|
||||||
|
..write('fallbackId: $fallbackId')
|
||||||
|
..write(')'))
|
||||||
|
.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(buttonId, orderKey, isVisible, fallbackId);
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
(other is i1.ToolbarButtonConfig &&
|
||||||
|
other.buttonId == this.buttonId &&
|
||||||
|
other.orderKey == this.orderKey &&
|
||||||
|
other.isVisible == this.isVisible &&
|
||||||
|
other.fallbackId == this.fallbackId);
|
||||||
|
}
|
||||||
|
|
||||||
|
class ToolbarButtonConfigsCompanion
|
||||||
|
extends i0.UpdateCompanion<i1.ToolbarButtonConfig> {
|
||||||
|
final i0.Value<String> buttonId;
|
||||||
|
final i0.Value<String> orderKey;
|
||||||
|
final i0.Value<bool> isVisible;
|
||||||
|
final i0.Value<String?> fallbackId;
|
||||||
|
final i0.Value<int> rowid;
|
||||||
|
const ToolbarButtonConfigsCompanion({
|
||||||
|
this.buttonId = const i0.Value.absent(),
|
||||||
|
this.orderKey = const i0.Value.absent(),
|
||||||
|
this.isVisible = const i0.Value.absent(),
|
||||||
|
this.fallbackId = const i0.Value.absent(),
|
||||||
|
this.rowid = const i0.Value.absent(),
|
||||||
|
});
|
||||||
|
ToolbarButtonConfigsCompanion.insert({
|
||||||
|
required String buttonId,
|
||||||
|
required String orderKey,
|
||||||
|
this.isVisible = const i0.Value.absent(),
|
||||||
|
this.fallbackId = const i0.Value.absent(),
|
||||||
|
this.rowid = const i0.Value.absent(),
|
||||||
|
}) : buttonId = i0.Value(buttonId),
|
||||||
|
orderKey = i0.Value(orderKey);
|
||||||
|
static i0.Insertable<i1.ToolbarButtonConfig> custom({
|
||||||
|
i0.Expression<String>? buttonId,
|
||||||
|
i0.Expression<String>? orderKey,
|
||||||
|
i0.Expression<bool>? isVisible,
|
||||||
|
i0.Expression<String>? fallbackId,
|
||||||
|
i0.Expression<int>? rowid,
|
||||||
|
}) {
|
||||||
|
return i0.RawValuesInsertable({
|
||||||
|
if (buttonId != null) 'button_id': buttonId,
|
||||||
|
if (orderKey != null) 'order_key': orderKey,
|
||||||
|
if (isVisible != null) 'is_visible': isVisible,
|
||||||
|
if (fallbackId != null) 'fallback_id': fallbackId,
|
||||||
|
if (rowid != null) 'rowid': rowid,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
i1.ToolbarButtonConfigsCompanion copyWith({
|
||||||
|
i0.Value<String>? buttonId,
|
||||||
|
i0.Value<String>? orderKey,
|
||||||
|
i0.Value<bool>? isVisible,
|
||||||
|
i0.Value<String?>? fallbackId,
|
||||||
|
i0.Value<int>? rowid,
|
||||||
|
}) {
|
||||||
|
return i1.ToolbarButtonConfigsCompanion(
|
||||||
|
buttonId: buttonId ?? this.buttonId,
|
||||||
|
orderKey: orderKey ?? this.orderKey,
|
||||||
|
isVisible: isVisible ?? this.isVisible,
|
||||||
|
fallbackId: fallbackId ?? this.fallbackId,
|
||||||
|
rowid: rowid ?? this.rowid,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
|
||||||
|
final map = <String, i0.Expression>{};
|
||||||
|
if (buttonId.present) {
|
||||||
|
map['button_id'] = i0.Variable<String>(buttonId.value);
|
||||||
|
}
|
||||||
|
if (orderKey.present) {
|
||||||
|
map['order_key'] = i0.Variable<String>(orderKey.value);
|
||||||
|
}
|
||||||
|
if (isVisible.present) {
|
||||||
|
map['is_visible'] = i0.Variable<bool>(isVisible.value);
|
||||||
|
}
|
||||||
|
if (fallbackId.present) {
|
||||||
|
map['fallback_id'] = i0.Variable<String>(fallbackId.value);
|
||||||
|
}
|
||||||
|
if (rowid.present) {
|
||||||
|
map['rowid'] = i0.Variable<int>(rowid.value);
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return (StringBuffer('ToolbarButtonConfigsCompanion(')
|
||||||
|
..write('buttonId: $buttonId, ')
|
||||||
|
..write('orderKey: $orderKey, ')
|
||||||
|
..write('isVisible: $isVisible, ')
|
||||||
|
..write('fallbackId: $fallbackId, ')
|
||||||
|
..write('rowid: $rowid')
|
||||||
|
..write(')'))
|
||||||
|
.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
i0.Index get idxToolbarOrderKey => i0.Index(
|
||||||
|
'idx_toolbar_order_key',
|
||||||
|
'CREATE INDEX idx_toolbar_order_key ON toolbar_button_configs (order_key)',
|
||||||
|
);
|
||||||
|
|
||||||
class DefinitionsDrift extends i3.ModularAccessor {
|
class DefinitionsDrift extends i3.ModularAccessor {
|
||||||
DefinitionsDrift(i0.GeneratedDatabase db) : super(db);
|
DefinitionsDrift(i0.GeneratedDatabase db) : super(db);
|
||||||
|
i0.Selectable<String> toolbarLeadingOrderKey({required int bucket}) {
|
||||||
|
return customSelect(
|
||||||
|
'SELECT lexo_rank_previous(?1, (SELECT order_key FROM toolbar_button_configs ORDER BY order_key LIMIT 1)) AS _c0',
|
||||||
|
variables: [i0.Variable<int>(bucket)],
|
||||||
|
readsFrom: {toolbarButtonConfigs},
|
||||||
|
).map((i0.QueryRow row) => row.read<String>('_c0'));
|
||||||
|
}
|
||||||
|
|
||||||
|
i0.Selectable<String> toolbarTrailingOrderKey({required int bucket}) {
|
||||||
|
return customSelect(
|
||||||
|
'SELECT lexo_rank_next(?1, (SELECT order_key FROM toolbar_button_configs ORDER BY order_key DESC LIMIT 1)) AS _c0',
|
||||||
|
variables: [i0.Variable<int>(bucket)],
|
||||||
|
readsFrom: {toolbarButtonConfigs},
|
||||||
|
).map((i0.QueryRow row) => row.read<String>('_c0'));
|
||||||
|
}
|
||||||
|
|
||||||
|
i0.Selectable<String> toolbarOrderKeyAfterButton({required String buttonId}) {
|
||||||
|
return customSelect(
|
||||||
|
'WITH ordered_table AS (SELECT button_id, order_key, LEAD(order_key)OVER (ORDER BY order_key RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW EXCLUDE NO OTHERS) AS next_order_key FROM toolbar_button_configs) SELECT lexo_rank_reorder_after(order_key, next_order_key) AS _c0 FROM ordered_table WHERE button_id = ?1',
|
||||||
|
variables: [i0.Variable<String>(buttonId)],
|
||||||
|
readsFrom: {toolbarButtonConfigs},
|
||||||
|
).map((i0.QueryRow row) => row.read<String>('_c0'));
|
||||||
|
}
|
||||||
|
|
||||||
|
i0.Selectable<String> toolbarOrderKeyBeforeButton({
|
||||||
|
required String buttonId,
|
||||||
|
}) {
|
||||||
|
return customSelect(
|
||||||
|
'WITH ordered_table AS (SELECT button_id, order_key, LAG(order_key)OVER (ORDER BY order_key RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW EXCLUDE NO OTHERS) AS prev_order_key FROM toolbar_button_configs) SELECT lexo_rank_reorder_before(order_key, prev_order_key) AS _c0 FROM ordered_table WHERE button_id = ?1',
|
||||||
|
variables: [i0.Variable<String>(buttonId)],
|
||||||
|
readsFrom: {toolbarButtonConfigs},
|
||||||
|
).map((i0.QueryRow row) => row.read<String>('_c0'));
|
||||||
|
}
|
||||||
|
|
||||||
Future<int> evictCacheEntries({required int limit}) {
|
Future<int> evictCacheEntries({required int limit}) {
|
||||||
return customUpdate(
|
return customUpdate(
|
||||||
'DELETE FROM icon_cache WHERE "rowid" IN (SELECT "rowid" FROM icon_cache ORDER BY fetch_date DESC LIMIT -1 OFFSET ?1)',
|
'DELETE FROM icon_cache WHERE "rowid" IN (SELECT "rowid" FROM icon_cache ORDER BY fetch_date DESC LIMIT -1 OFFSET ?1)',
|
||||||
@@ -1569,6 +2067,9 @@ class DefinitionsDrift extends i3.ModularAccessor {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
i1.ToolbarButtonConfigs get toolbarButtonConfigs => i3.ReadDatabaseContainer(
|
||||||
|
attachedDatabase,
|
||||||
|
).resultSet<i1.ToolbarButtonConfigs>('toolbar_button_configs');
|
||||||
i1.IconCache get iconCache => i3.ReadDatabaseContainer(
|
i1.IconCache get iconCache => i3.ReadDatabaseContainer(
|
||||||
attachedDatabase,
|
attachedDatabase,
|
||||||
).resultSet<i1.IconCache>('icon_cache');
|
).resultSet<i1.IconCache>('icon_cache');
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
|
|||||||
import 'package:sqlite3_flutter_libs/sqlite3_flutter_libs.dart';
|
import 'package:sqlite3_flutter_libs/sqlite3_flutter_libs.dart';
|
||||||
|
|
||||||
import 'package:weblibre/core/filesystem.dart';
|
import 'package:weblibre/core/filesystem.dart';
|
||||||
|
import 'package:weblibre/data/database/functions/lexo_rank_functions.dart';
|
||||||
import 'package:weblibre/features/user/data/database/database.dart';
|
import 'package:weblibre/features/user/data/database/database.dart';
|
||||||
import 'package:weblibre/features/user/data/database/riverpod_storage.dart';
|
import 'package:weblibre/features/user/data/database/riverpod_storage.dart';
|
||||||
|
|
||||||
@@ -43,7 +44,12 @@ UserDatabase userDatabase(Ref ref) {
|
|||||||
await applyWorkaroundToOpenSqlite3OnOldAndroidVersions();
|
await applyWorkaroundToOpenSqlite3OnOldAndroidVersions();
|
||||||
}
|
}
|
||||||
|
|
||||||
return NativeDatabase.createInBackground(file);
|
return NativeDatabase.createInBackground(
|
||||||
|
file,
|
||||||
|
setup: (database) {
|
||||||
|
registerLexorankFunctions(database);
|
||||||
|
},
|
||||||
|
);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ final class UserDatabaseProvider
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$userDatabaseHash() => r'8cc40197f9dbb85ccd8263984218753d3690954b';
|
String _$userDatabaseHash() => r'1d14f39dd506ec8890a0a15cc2546b3294a34a1c';
|
||||||
|
|
||||||
@ProviderFor(riverpodDatabaseStorage)
|
@ProviderFor(riverpodDatabaseStorage)
|
||||||
final riverpodDatabaseStorageProvider = RiverpodDatabaseStorageProvider._();
|
final riverpodDatabaseStorageProvider = RiverpodDatabaseStorageProvider._();
|
||||||
|
|||||||
@@ -0,0 +1,298 @@
|
|||||||
|
{
|
||||||
|
"_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": "setting",
|
||||||
|
"was_declared_in_moor": true,
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "key",
|
||||||
|
"getter_name": "key",
|
||||||
|
"moor_type": "string",
|
||||||
|
"nullable": false,
|
||||||
|
"customConstraints": "PRIMARY KEY NOT NULL",
|
||||||
|
"default_dart": null,
|
||||||
|
"default_client_dart": null,
|
||||||
|
"dsl_features": [
|
||||||
|
"primary-key"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "partition_key",
|
||||||
|
"getter_name": "partitionKey",
|
||||||
|
"moor_type": "string",
|
||||||
|
"nullable": true,
|
||||||
|
"customConstraints": "",
|
||||||
|
"default_dart": null,
|
||||||
|
"default_client_dart": null,
|
||||||
|
"dsl_features": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "value",
|
||||||
|
"getter_name": "value",
|
||||||
|
"moor_type": "any",
|
||||||
|
"nullable": true,
|
||||||
|
"customConstraints": "",
|
||||||
|
"default_dart": null,
|
||||||
|
"default_client_dart": null,
|
||||||
|
"dsl_features": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_virtual": false,
|
||||||
|
"without_rowid": false,
|
||||||
|
"constraints": [],
|
||||||
|
"strict": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"references": [],
|
||||||
|
"type": "table",
|
||||||
|
"data": {
|
||||||
|
"name": "icon_cache",
|
||||||
|
"was_declared_in_moor": true,
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "origin",
|
||||||
|
"getter_name": "origin",
|
||||||
|
"moor_type": "string",
|
||||||
|
"nullable": false,
|
||||||
|
"customConstraints": "PRIMARY KEY NOT NULL",
|
||||||
|
"default_dart": null,
|
||||||
|
"default_client_dart": null,
|
||||||
|
"dsl_features": [
|
||||||
|
"primary-key"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "icon_data",
|
||||||
|
"getter_name": "iconData",
|
||||||
|
"moor_type": "blob",
|
||||||
|
"nullable": false,
|
||||||
|
"customConstraints": "NOT NULL",
|
||||||
|
"default_dart": null,
|
||||||
|
"default_client_dart": null,
|
||||||
|
"dsl_features": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "fetch_date",
|
||||||
|
"getter_name": "fetchDate",
|
||||||
|
"moor_type": "dateTime",
|
||||||
|
"nullable": false,
|
||||||
|
"customConstraints": "NOT NULL",
|
||||||
|
"default_dart": null,
|
||||||
|
"default_client_dart": null,
|
||||||
|
"dsl_features": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_virtual": false,
|
||||||
|
"without_rowid": false,
|
||||||
|
"constraints": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"references": [],
|
||||||
|
"type": "table",
|
||||||
|
"data": {
|
||||||
|
"name": "onboarding",
|
||||||
|
"was_declared_in_moor": true,
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "revision",
|
||||||
|
"getter_name": "revision",
|
||||||
|
"moor_type": "int",
|
||||||
|
"nullable": false,
|
||||||
|
"customConstraints": "NOT NULL",
|
||||||
|
"default_dart": null,
|
||||||
|
"default_client_dart": null,
|
||||||
|
"dsl_features": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "completion_date",
|
||||||
|
"getter_name": "completionDate",
|
||||||
|
"moor_type": "dateTime",
|
||||||
|
"nullable": false,
|
||||||
|
"customConstraints": "NOT NULL",
|
||||||
|
"default_dart": null,
|
||||||
|
"default_client_dart": null,
|
||||||
|
"dsl_features": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_virtual": false,
|
||||||
|
"without_rowid": false,
|
||||||
|
"constraints": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"references": [],
|
||||||
|
"type": "table",
|
||||||
|
"data": {
|
||||||
|
"name": "riverpod",
|
||||||
|
"was_declared_in_moor": true,
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "key",
|
||||||
|
"getter_name": "key",
|
||||||
|
"moor_type": "string",
|
||||||
|
"nullable": false,
|
||||||
|
"customConstraints": "PRIMARY KEY NOT NULL",
|
||||||
|
"default_dart": null,
|
||||||
|
"default_client_dart": null,
|
||||||
|
"dsl_features": [
|
||||||
|
"primary-key"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "json",
|
||||||
|
"getter_name": "json",
|
||||||
|
"moor_type": "string",
|
||||||
|
"nullable": false,
|
||||||
|
"customConstraints": "NOT NULL",
|
||||||
|
"default_dart": null,
|
||||||
|
"default_client_dart": null,
|
||||||
|
"dsl_features": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "expireAt",
|
||||||
|
"getter_name": "expireAt",
|
||||||
|
"moor_type": "dateTime",
|
||||||
|
"nullable": true,
|
||||||
|
"customConstraints": "",
|
||||||
|
"default_dart": null,
|
||||||
|
"default_client_dart": null,
|
||||||
|
"dsl_features": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "destroyKey",
|
||||||
|
"getter_name": "destroyKey",
|
||||||
|
"moor_type": "string",
|
||||||
|
"nullable": true,
|
||||||
|
"customConstraints": "",
|
||||||
|
"default_dart": null,
|
||||||
|
"default_client_dart": null,
|
||||||
|
"dsl_features": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_virtual": false,
|
||||||
|
"without_rowid": true,
|
||||||
|
"constraints": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"references": [],
|
||||||
|
"type": "table",
|
||||||
|
"data": {
|
||||||
|
"name": "toolbar_button_configs",
|
||||||
|
"was_declared_in_moor": true,
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "button_id",
|
||||||
|
"getter_name": "buttonId",
|
||||||
|
"moor_type": "string",
|
||||||
|
"nullable": false,
|
||||||
|
"customConstraints": "NOT NULL PRIMARY KEY",
|
||||||
|
"default_dart": null,
|
||||||
|
"default_client_dart": null,
|
||||||
|
"dsl_features": [
|
||||||
|
"primary-key"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "order_key",
|
||||||
|
"getter_name": "orderKey",
|
||||||
|
"moor_type": "string",
|
||||||
|
"nullable": false,
|
||||||
|
"customConstraints": "NOT NULL",
|
||||||
|
"default_dart": null,
|
||||||
|
"default_client_dart": null,
|
||||||
|
"dsl_features": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "is_visible",
|
||||||
|
"getter_name": "isVisible",
|
||||||
|
"moor_type": "int",
|
||||||
|
"nullable": false,
|
||||||
|
"customConstraints": "NOT NULL DEFAULT 1",
|
||||||
|
"default_dart": "const CustomExpression('1')",
|
||||||
|
"default_client_dart": null,
|
||||||
|
"dsl_features": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "fallback_id",
|
||||||
|
"getter_name": "fallbackId",
|
||||||
|
"moor_type": "string",
|
||||||
|
"nullable": true,
|
||||||
|
"customConstraints": "",
|
||||||
|
"default_dart": null,
|
||||||
|
"default_client_dart": null,
|
||||||
|
"dsl_features": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_virtual": false,
|
||||||
|
"without_rowid": false,
|
||||||
|
"constraints": [],
|
||||||
|
"strict": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fixed_sql": [
|
||||||
|
{
|
||||||
|
"name": "setting",
|
||||||
|
"sql": [
|
||||||
|
{
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"sql": "CREATE TABLE IF NOT EXISTS \"setting\" (\"key\" TEXT PRIMARY KEY NOT NULL, \"partition_key\" TEXT, \"value\" ANY) STRICT;"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "icon_cache",
|
||||||
|
"sql": [
|
||||||
|
{
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"sql": "CREATE TABLE IF NOT EXISTS \"icon_cache\" (\"origin\" TEXT PRIMARY KEY NOT NULL, \"icon_data\" BLOB NOT NULL, \"fetch_date\" INTEGER NOT NULL);"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "onboarding",
|
||||||
|
"sql": [
|
||||||
|
{
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"sql": "CREATE TABLE IF NOT EXISTS \"onboarding\" (\"revision\" INTEGER NOT NULL, \"completion_date\" INTEGER NOT NULL);"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "riverpod",
|
||||||
|
"sql": [
|
||||||
|
{
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"sql": "CREATE TABLE IF NOT EXISTS \"riverpod\" (\"key\" TEXT PRIMARY KEY NOT NULL, \"json\" TEXT NOT NULL, \"expireAt\" INTEGER, \"destroyKey\" TEXT) WITHOUT ROWID;"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "toolbar_button_configs",
|
||||||
|
"sql": [
|
||||||
|
{
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"sql": "CREATE TABLE IF NOT EXISTS \"toolbar_button_configs\" (\"button_id\" TEXT NOT NULL PRIMARY KEY, \"order_key\" TEXT NOT NULL, \"is_visible\" INTEGER NOT NULL DEFAULT 1, \"fallback_id\" TEXT) STRICT;"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import 'package:drift/drift.dart';
|
|||||||
import 'package:drift/internal/migrations.dart';
|
import 'package:drift/internal/migrations.dart';
|
||||||
import 'schema_v1.dart' as v1;
|
import 'schema_v1.dart' as v1;
|
||||||
import 'schema_v2.dart' as v2;
|
import 'schema_v2.dart' as v2;
|
||||||
|
import 'schema_v3.dart' as v3;
|
||||||
|
|
||||||
class GeneratedHelper implements SchemaInstantiationHelper {
|
class GeneratedHelper implements SchemaInstantiationHelper {
|
||||||
@override
|
@override
|
||||||
@@ -15,10 +16,12 @@ class GeneratedHelper implements SchemaInstantiationHelper {
|
|||||||
return v1.DatabaseAtV1(db);
|
return v1.DatabaseAtV1(db);
|
||||||
case 2:
|
case 2:
|
||||||
return v2.DatabaseAtV2(db);
|
return v2.DatabaseAtV2(db);
|
||||||
|
case 3:
|
||||||
|
return v3.DatabaseAtV3(db);
|
||||||
default:
|
default:
|
||||||
throw MissingSchemaException(version, versions);
|
throw MissingSchemaException(version, versions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static const versions = const [1, 2];
|
static const versions = const [1, 2, 3];
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,69 @@
|
|||||||
|
import 'package:drift/native.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:weblibre/data/database/functions/lexo_rank_functions.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/browser/features/contextual_toolbar/data/providers/toolbar_button_configs.dart';
|
||||||
|
import 'package:weblibre/features/user/data/database/database.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
late UserDatabase db;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
db = UserDatabase(
|
||||||
|
NativeDatabase.memory(
|
||||||
|
setup: (database) {
|
||||||
|
registerLexorankFunctions(database);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() async {
|
||||||
|
await db.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
group('ToolbarButtonConfigDao', () {
|
||||||
|
test(
|
||||||
|
'seedMissing inserts self-referential fallback rows in two phases',
|
||||||
|
() async {
|
||||||
|
await db.toolbarButtonConfigDao.seedMissing([
|
||||||
|
(
|
||||||
|
buttonId: 'back',
|
||||||
|
defaultVisible: true,
|
||||||
|
defaultFallback: 'bookmarks',
|
||||||
|
),
|
||||||
|
(buttonId: 'bookmarks', defaultVisible: false, defaultFallback: null),
|
||||||
|
(buttonId: 'forward', defaultVisible: true, defaultFallback: 'share'),
|
||||||
|
(buttonId: 'share', defaultVisible: false, defaultFallback: null),
|
||||||
|
]);
|
||||||
|
|
||||||
|
final configs = await db.toolbarButtonConfigDao.getAll();
|
||||||
|
final configsById = {
|
||||||
|
for (final config in configs) config.buttonId: config,
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(configsById['back']?.fallbackId, 'bookmarks');
|
||||||
|
expect(configsById['forward']?.fallbackId, 'share');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'replaceAll persists default toolbar fallbacks without FK failures',
|
||||||
|
() async {
|
||||||
|
await expectLater(
|
||||||
|
db.toolbarButtonConfigDao.replaceAll(
|
||||||
|
buildDefaultToolbarButtonConfigs(),
|
||||||
|
),
|
||||||
|
completes,
|
||||||
|
);
|
||||||
|
|
||||||
|
final configs = await db.toolbarButtonConfigDao.getAll();
|
||||||
|
final configsById = {
|
||||||
|
for (final config in configs) config.buttonId: config,
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(configsById['back']?.fallbackId, 'bookmarks');
|
||||||
|
expect(configsById['forward']?.fallbackId, 'share');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -34,6 +34,7 @@ void main() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
test('forces animations off when the app setting is enabled', () {
|
test('forces animations off when the app setting is enabled', () {
|
||||||
|
// ignore: avoid_redundant_argument_values
|
||||||
const mediaQuery = MediaQueryData(disableAnimations: false);
|
const mediaQuery = MediaQueryData(disableAnimations: false);
|
||||||
|
|
||||||
final result = applyAppMediaQueryOverrides(
|
final result = applyAppMediaQueryOverrides(
|
||||||
|
|||||||
Reference in New Issue
Block a user