initial
This commit is contained in:
@@ -30,6 +30,7 @@ migrate_working_dir/
|
|||||||
.pub-cache/
|
.pub-cache/
|
||||||
.pub/
|
.pub/
|
||||||
/build/
|
/build/
|
||||||
|
assets/quotes/quotes.db
|
||||||
|
|
||||||
# Android related
|
# Android related
|
||||||
**/android/**/gradle-wrapper.jar
|
**/android/**/gradle-wrapper.jar
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ targets:
|
|||||||
named_parameters: true
|
named_parameters: true
|
||||||
databases:
|
databases:
|
||||||
bangs: lib/features/bangs/data/database/database.dart
|
bangs: lib/features/bangs/data/database/database.dart
|
||||||
|
quotes: lib/features/quotes/data/database/database.dart
|
||||||
tabs: lib/features/geckoview/features/tabs/data/database/database.dart
|
tabs: lib/features/geckoview/features/tabs/data/database/database.dart
|
||||||
user: lib/features/user/data/database/database.dart
|
user: lib/features/user/data/database/database.dart
|
||||||
web_feed: lib/features/web_feed/data/database/database.dart
|
web_feed: lib/features/web_feed/data/database/database.dart
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
{
|
||||||
|
"_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": "quotes",
|
||||||
|
"was_declared_in_moor": true,
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "author",
|
||||||
|
"getter_name": "author",
|
||||||
|
"moor_type": "string",
|
||||||
|
"nullable": false,
|
||||||
|
"customConstraints": "NOT NULL",
|
||||||
|
"default_dart": null,
|
||||||
|
"default_client_dart": null,
|
||||||
|
"dsl_features": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "quote",
|
||||||
|
"getter_name": "quote",
|
||||||
|
"moor_type": "string",
|
||||||
|
"nullable": false,
|
||||||
|
"customConstraints": "NOT NULL",
|
||||||
|
"default_dart": null,
|
||||||
|
"default_client_dart": null,
|
||||||
|
"dsl_features": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "source",
|
||||||
|
"getter_name": "source",
|
||||||
|
"moor_type": "string",
|
||||||
|
"nullable": true,
|
||||||
|
"customConstraints": "",
|
||||||
|
"default_dart": null,
|
||||||
|
"default_client_dart": null,
|
||||||
|
"dsl_features": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "tags",
|
||||||
|
"getter_name": "tags",
|
||||||
|
"moor_type": "string",
|
||||||
|
"nullable": true,
|
||||||
|
"customConstraints": "",
|
||||||
|
"default_dart": null,
|
||||||
|
"default_client_dart": null,
|
||||||
|
"dsl_features": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_virtual": false,
|
||||||
|
"without_rowid": false,
|
||||||
|
"constraints": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fixed_sql": [
|
||||||
|
{
|
||||||
|
"name": "quotes",
|
||||||
|
"sql": [
|
||||||
|
{
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"sql": "CREATE TABLE IF NOT EXISTS \"quotes\" (\"author\" TEXT NOT NULL, \"quote\" TEXT NOT NULL, \"source\" TEXT, \"tags\" TEXT);"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -41,7 +41,7 @@ final class TabRepositoryProvider
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$tabRepositoryHash() => r'4dd31901b4d02ca075d3dcec3822f5a2832d164c';
|
String _$tabRepositoryHash() => r'22fdd97ad7f235590926915169c9e7230de94bdb';
|
||||||
|
|
||||||
abstract class _$TabRepository extends $Notifier<void> {
|
abstract class _$TabRepository extends $Notifier<void> {
|
||||||
void build();
|
void build();
|
||||||
|
|||||||
+81
@@ -0,0 +1,81 @@
|
|||||||
|
/*
|
||||||
|
* 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:math' as math;
|
||||||
|
|
||||||
|
import 'package:copy_with_extension/copy_with_extension.dart';
|
||||||
|
import 'package:fast_equatable/fast_equatable.dart';
|
||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
|
part 'browser_viewport_toolbar_insets.g.dart';
|
||||||
|
|
||||||
|
@CopyWith()
|
||||||
|
class BrowserViewportToolbarInsetsState with FastEquatable {
|
||||||
|
final int dynamicToolbarMaxHeightPx;
|
||||||
|
final int verticalClippingPx;
|
||||||
|
|
||||||
|
BrowserViewportToolbarInsetsState({
|
||||||
|
this.dynamicToolbarMaxHeightPx = 0,
|
||||||
|
this.verticalClippingPx = 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
static final zero = BrowserViewportToolbarInsetsState();
|
||||||
|
|
||||||
|
int get effectiveBottomInsetPx =>
|
||||||
|
math.max(0, dynamicToolbarMaxHeightPx + verticalClippingPx);
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get hashParameters => [
|
||||||
|
dynamicToolbarMaxHeightPx,
|
||||||
|
verticalClippingPx,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
@Riverpod(keepAlive: true)
|
||||||
|
class BrowserViewportToolbarInsetsController
|
||||||
|
extends _$BrowserViewportToolbarInsetsController {
|
||||||
|
@override
|
||||||
|
BrowserViewportToolbarInsetsState build() {
|
||||||
|
return BrowserViewportToolbarInsetsState.zero;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setDynamicToolbarMaxHeight(int heightPx) {
|
||||||
|
if (state.dynamicToolbarMaxHeightPx == heightPx) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
state = state.copyWith(dynamicToolbarMaxHeightPx: heightPx);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setVerticalClipping(int clippingPx) {
|
||||||
|
if (state.verticalClippingPx == clippingPx) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
state = state.copyWith(verticalClippingPx: clippingPx);
|
||||||
|
}
|
||||||
|
|
||||||
|
void reset() {
|
||||||
|
if (state == BrowserViewportToolbarInsetsState.zero) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
state = BrowserViewportToolbarInsetsState.zero;
|
||||||
|
}
|
||||||
|
}
|
||||||
+161
@@ -0,0 +1,161 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'browser_viewport_toolbar_insets.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// CopyWithGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
abstract class _$BrowserViewportToolbarInsetsStateCWProxy {
|
||||||
|
BrowserViewportToolbarInsetsState dynamicToolbarMaxHeightPx(
|
||||||
|
int dynamicToolbarMaxHeightPx,
|
||||||
|
);
|
||||||
|
|
||||||
|
BrowserViewportToolbarInsetsState verticalClippingPx(int verticalClippingPx);
|
||||||
|
|
||||||
|
/// Creates a new instance with the provided field values.
|
||||||
|
/// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `BrowserViewportToolbarInsetsState(...).copyWith.fieldName(value)`.
|
||||||
|
///
|
||||||
|
/// Example:
|
||||||
|
/// ```dart
|
||||||
|
/// BrowserViewportToolbarInsetsState(...).copyWith(id: 12, name: "My name")
|
||||||
|
/// ```
|
||||||
|
BrowserViewportToolbarInsetsState call({
|
||||||
|
int dynamicToolbarMaxHeightPx,
|
||||||
|
int verticalClippingPx,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Callable proxy for `copyWith` functionality.
|
||||||
|
/// Use as `instanceOfBrowserViewportToolbarInsetsState.copyWith(...)` or call `instanceOfBrowserViewportToolbarInsetsState.copyWith.fieldName(value)` for a single field.
|
||||||
|
class _$BrowserViewportToolbarInsetsStateCWProxyImpl
|
||||||
|
implements _$BrowserViewportToolbarInsetsStateCWProxy {
|
||||||
|
const _$BrowserViewportToolbarInsetsStateCWProxyImpl(this._value);
|
||||||
|
|
||||||
|
final BrowserViewportToolbarInsetsState _value;
|
||||||
|
|
||||||
|
@override
|
||||||
|
BrowserViewportToolbarInsetsState dynamicToolbarMaxHeightPx(
|
||||||
|
int dynamicToolbarMaxHeightPx,
|
||||||
|
) => call(dynamicToolbarMaxHeightPx: dynamicToolbarMaxHeightPx);
|
||||||
|
|
||||||
|
@override
|
||||||
|
BrowserViewportToolbarInsetsState verticalClippingPx(
|
||||||
|
int verticalClippingPx,
|
||||||
|
) => call(verticalClippingPx: verticalClippingPx);
|
||||||
|
|
||||||
|
@override
|
||||||
|
/// Creates a new instance with the provided field values.
|
||||||
|
/// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `BrowserViewportToolbarInsetsState(...).copyWith.fieldName(value)`.
|
||||||
|
///
|
||||||
|
/// Example:
|
||||||
|
/// ```dart
|
||||||
|
/// BrowserViewportToolbarInsetsState(...).copyWith(id: 12, name: "My name")
|
||||||
|
/// ```
|
||||||
|
BrowserViewportToolbarInsetsState call({
|
||||||
|
Object? dynamicToolbarMaxHeightPx = const $CopyWithPlaceholder(),
|
||||||
|
Object? verticalClippingPx = const $CopyWithPlaceholder(),
|
||||||
|
}) {
|
||||||
|
return BrowserViewportToolbarInsetsState(
|
||||||
|
dynamicToolbarMaxHeightPx:
|
||||||
|
dynamicToolbarMaxHeightPx == const $CopyWithPlaceholder() ||
|
||||||
|
dynamicToolbarMaxHeightPx == null
|
||||||
|
? _value.dynamicToolbarMaxHeightPx
|
||||||
|
// ignore: cast_nullable_to_non_nullable
|
||||||
|
: dynamicToolbarMaxHeightPx as int,
|
||||||
|
verticalClippingPx:
|
||||||
|
verticalClippingPx == const $CopyWithPlaceholder() ||
|
||||||
|
verticalClippingPx == null
|
||||||
|
? _value.verticalClippingPx
|
||||||
|
// ignore: cast_nullable_to_non_nullable
|
||||||
|
: verticalClippingPx as int,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension $BrowserViewportToolbarInsetsStateCopyWith
|
||||||
|
on BrowserViewportToolbarInsetsState {
|
||||||
|
/// Returns a callable class used to build a new instance with modified fields.
|
||||||
|
/// Example: `instanceOfBrowserViewportToolbarInsetsState.copyWith(...)` or `instanceOfBrowserViewportToolbarInsetsState.copyWith.fieldName(...)`.
|
||||||
|
// ignore: library_private_types_in_public_api
|
||||||
|
_$BrowserViewportToolbarInsetsStateCWProxy get copyWith =>
|
||||||
|
_$BrowserViewportToolbarInsetsStateCWProxyImpl(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// RiverpodGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
// ignore_for_file: type=lint, type=warning
|
||||||
|
|
||||||
|
@ProviderFor(BrowserViewportToolbarInsetsController)
|
||||||
|
final browserViewportToolbarInsetsControllerProvider =
|
||||||
|
BrowserViewportToolbarInsetsControllerProvider._();
|
||||||
|
|
||||||
|
final class BrowserViewportToolbarInsetsControllerProvider
|
||||||
|
extends
|
||||||
|
$NotifierProvider<
|
||||||
|
BrowserViewportToolbarInsetsController,
|
||||||
|
BrowserViewportToolbarInsetsState
|
||||||
|
> {
|
||||||
|
BrowserViewportToolbarInsetsControllerProvider._()
|
||||||
|
: super(
|
||||||
|
from: null,
|
||||||
|
argument: null,
|
||||||
|
retry: null,
|
||||||
|
name: r'browserViewportToolbarInsetsControllerProvider',
|
||||||
|
isAutoDispose: false,
|
||||||
|
dependencies: null,
|
||||||
|
$allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String debugGetCreateSourceHash() =>
|
||||||
|
_$browserViewportToolbarInsetsControllerHash();
|
||||||
|
|
||||||
|
@$internal
|
||||||
|
@override
|
||||||
|
BrowserViewportToolbarInsetsController create() =>
|
||||||
|
BrowserViewportToolbarInsetsController();
|
||||||
|
|
||||||
|
/// {@macro riverpod.override_with_value}
|
||||||
|
Override overrideWithValue(BrowserViewportToolbarInsetsState value) {
|
||||||
|
return $ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
providerOverride: $SyncValueProvider<BrowserViewportToolbarInsetsState>(
|
||||||
|
value,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$browserViewportToolbarInsetsControllerHash() =>
|
||||||
|
r'ae6444dc84383bef7d957e3bbd1c48d9ebba15ec';
|
||||||
|
|
||||||
|
abstract class _$BrowserViewportToolbarInsetsController
|
||||||
|
extends $Notifier<BrowserViewportToolbarInsetsState> {
|
||||||
|
BrowserViewportToolbarInsetsState build();
|
||||||
|
@$mustCallSuper
|
||||||
|
@override
|
||||||
|
void runBuild() {
|
||||||
|
final ref =
|
||||||
|
this.ref
|
||||||
|
as $Ref<
|
||||||
|
BrowserViewportToolbarInsetsState,
|
||||||
|
BrowserViewportToolbarInsetsState
|
||||||
|
>;
|
||||||
|
final element =
|
||||||
|
ref.element
|
||||||
|
as $ClassProviderElement<
|
||||||
|
AnyNotifier<
|
||||||
|
BrowserViewportToolbarInsetsState,
|
||||||
|
BrowserViewportToolbarInsetsState
|
||||||
|
>,
|
||||||
|
BrowserViewportToolbarInsetsState,
|
||||||
|
Object?,
|
||||||
|
Object?
|
||||||
|
>;
|
||||||
|
element.handleCreate(ref, build);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -43,6 +43,7 @@ import 'package:weblibre/features/geckoview/features/browser/domain/providers.da
|
|||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/tab_view_controllers.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/tab_view_controllers.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/toolbar_visibility.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/toolbar_visibility.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/dialogs/keep_tab_dialog.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/dialogs/keep_tab_dialog.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/browser/presentation/providers/browser_viewport_toolbar_insets.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/browser_modules/browser_fab.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/browser_modules/browser_fab.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/browser_modules/browser_view.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/browser_modules/browser_view.dart';
|
||||||
@@ -428,9 +429,26 @@ class BrowserScreen extends HookConsumerWidget {
|
|||||||
|
|
||||||
final lastToolbarMaxHeightPx = useRef<int?>(null);
|
final lastToolbarMaxHeightPx = useRef<int?>(null);
|
||||||
|
|
||||||
|
useEffect(() {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
ref
|
||||||
|
.read(browserViewportToolbarInsetsControllerProvider.notifier)
|
||||||
|
.reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
if (lastToolbarMaxHeightPx.value != effectiveToolbarHeightPx) {
|
if (lastToolbarMaxHeightPx.value != effectiveToolbarHeightPx) {
|
||||||
lastToolbarMaxHeightPx.value = effectiveToolbarHeightPx;
|
lastToolbarMaxHeightPx.value = effectiveToolbarHeightPx;
|
||||||
|
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
ref
|
||||||
|
.read(browserViewportToolbarInsetsControllerProvider.notifier)
|
||||||
|
.setDynamicToolbarMaxHeight(effectiveToolbarHeightPx);
|
||||||
|
});
|
||||||
|
|
||||||
unawaited(
|
unawaited(
|
||||||
viewportService.setDynamicToolbarMaxHeight(effectiveToolbarHeightPx),
|
viewportService.setDynamicToolbarMaxHeight(effectiveToolbarHeightPx),
|
||||||
);
|
);
|
||||||
@@ -465,6 +483,13 @@ class BrowserScreen extends HookConsumerWidget {
|
|||||||
|
|
||||||
if (targetClippingPx != lastClippingPx.value) {
|
if (targetClippingPx != lastClippingPx.value) {
|
||||||
lastClippingPx.value = targetClippingPx;
|
lastClippingPx.value = targetClippingPx;
|
||||||
|
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
ref
|
||||||
|
.read(browserViewportToolbarInsetsControllerProvider.notifier)
|
||||||
|
.setVerticalClipping(targetClippingPx);
|
||||||
|
});
|
||||||
|
|
||||||
unawaited(viewportService.setVerticalClipping(targetClippingPx));
|
unawaited(viewportService.setVerticalClipping(targetClippingPx));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-8
@@ -45,6 +45,7 @@ import 'package:weblibre/features/geckoview/features/browser/domain/providers/li
|
|||||||
import 'package:weblibre/features/geckoview/features/browser/domain/services/browser_data.dart';
|
import 'package:weblibre/features/geckoview/features/browser/domain/services/browser_data.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/domain/services/engine_settings_replication.dart';
|
import 'package:weblibre/features/geckoview/features/browser/domain/services/engine_settings_replication.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/domain/services/proxy_settings_replication.dart';
|
import 'package:weblibre/features/geckoview/features/browser/domain/services/proxy_settings_replication.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tabs_empty.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/history/domain/repositories/history.dart';
|
import 'package:weblibre/features/geckoview/features/history/domain/repositories/history.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/preferences/data/repositories/preference_observer.dart';
|
import 'package:weblibre/features/geckoview/features/preferences/data/repositories/preference_observer.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/pwa/domain/providers.dart';
|
import 'package:weblibre/features/geckoview/features/pwa/domain/providers.dart';
|
||||||
@@ -108,8 +109,6 @@ class _BrowserViewState extends ConsumerState<BrowserView>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final colorScheme = Theme.of(context).colorScheme;
|
|
||||||
|
|
||||||
useOnInitialization(() async {
|
useOnInitialization(() async {
|
||||||
await ref
|
await ref
|
||||||
.read(generalSettingsRepositoryProvider.notifier)
|
.read(generalSettingsRepositoryProvider.notifier)
|
||||||
@@ -292,12 +291,7 @@ class _BrowserViewState extends ConsumerState<BrowserView>
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (!hasTab)
|
if (!hasTab) const Positioned.fill(child: TabsEmptyPlaceholder()),
|
||||||
Positioned.fill(
|
|
||||||
child: SizedBox.expand(
|
|
||||||
child: Container(color: colorScheme.surfaceContainerHighest),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,328 @@
|
|||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
|
||||||
|
import 'package:flutter_svg/svg.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:weblibre/core/routing/routes.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/browser/presentation/providers/browser_viewport_toolbar_insets.dart';
|
||||||
|
import 'package:weblibre/features/quotes/data/database/definitions.drift.dart';
|
||||||
|
import 'package:weblibre/features/quotes/domain/providers.dart';
|
||||||
|
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
||||||
|
|
||||||
|
class TabsEmptyPlaceholder extends ConsumerWidget {
|
||||||
|
const TabsEmptyPlaceholder();
|
||||||
|
|
||||||
|
static const _brandPurple = Color(0xFF9C83F8);
|
||||||
|
static const _brandYellow = Color(0xFFFBDC6B);
|
||||||
|
static const _brandGrey = Color(0xFFA7A7A7);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final colorScheme = theme.colorScheme;
|
||||||
|
final settings = ref.watch(generalSettingsWithDefaultsProvider);
|
||||||
|
final quoteAsync = ref.watch(randomQuoteProvider);
|
||||||
|
final viewportToolbarInsets = ref.watch(
|
||||||
|
browserViewportToolbarInsetsControllerProvider,
|
||||||
|
);
|
||||||
|
|
||||||
|
final pixelRatio = MediaQuery.devicePixelRatioOf(context);
|
||||||
|
|
||||||
|
final bottomViewportInset =
|
||||||
|
viewportToolbarInsets.effectiveBottomInsetPx / pixelRatio;
|
||||||
|
|
||||||
|
Future<void> openNewTab() {
|
||||||
|
return SearchRoute(
|
||||||
|
tabType: settings.effectiveDefaultCreateTabType,
|
||||||
|
).push(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
return DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
colors: [
|
||||||
|
Color.alphaBlend(
|
||||||
|
_brandPurple.withValues(alpha: 0.06),
|
||||||
|
colorScheme.surfaceContainerLowest,
|
||||||
|
),
|
||||||
|
colorScheme.surface,
|
||||||
|
Color.alphaBlend(
|
||||||
|
_brandYellow.withValues(alpha: 0.05),
|
||||||
|
colorScheme.surfaceContainerHigh,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Stack(
|
||||||
|
fit: StackFit.expand,
|
||||||
|
children: [
|
||||||
|
Align(
|
||||||
|
alignment: const Alignment(-0.9, -0.95),
|
||||||
|
child: _BackdropOrb(
|
||||||
|
size: 220,
|
||||||
|
color: _brandPurple.withValues(alpha: 0.14),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Align(
|
||||||
|
alignment: const Alignment(1.0, -0.35),
|
||||||
|
child: _BackdropOrb(
|
||||||
|
size: 180,
|
||||||
|
color: _brandYellow.withValues(alpha: 0.14),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Align(
|
||||||
|
alignment: const Alignment(-0.75, 0.9),
|
||||||
|
child: _BackdropOrb(
|
||||||
|
size: 160,
|
||||||
|
color: _brandGrey.withValues(alpha: 0.12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
return SingleChildScrollView(
|
||||||
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
|
padding: EdgeInsets.fromLTRB(
|
||||||
|
24,
|
||||||
|
32,
|
||||||
|
24,
|
||||||
|
32 + bottomViewportInset,
|
||||||
|
),
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
minHeight: math.max(
|
||||||
|
0,
|
||||||
|
constraints.maxHeight - 64 - bottomViewportInset,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 560),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 112,
|
||||||
|
height: 112,
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(32),
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
colors: [
|
||||||
|
Color.alphaBlend(
|
||||||
|
_brandPurple.withValues(alpha: 0.18),
|
||||||
|
colorScheme.surfaceContainerHighest,
|
||||||
|
),
|
||||||
|
Color.alphaBlend(
|
||||||
|
_brandYellow.withValues(alpha: 0.12),
|
||||||
|
colorScheme.surfaceContainer,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
border: Border.all(
|
||||||
|
color: colorScheme.outlineVariant.withValues(
|
||||||
|
alpha: 0.45,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: colorScheme.shadow.withValues(
|
||||||
|
alpha: 0.08,
|
||||||
|
),
|
||||||
|
blurRadius: 32,
|
||||||
|
offset: const Offset(0, 18),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: SvgPicture.asset(
|
||||||
|
'assets/icon/icon.svg',
|
||||||
|
width: 72,
|
||||||
|
height: 72,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
Text(
|
||||||
|
'WebLibre is ready',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: theme.textTheme.headlineSmall?.copyWith(
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Text(
|
||||||
|
'A browser that respects you. Open a tab and experience the web, libre.',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: theme.textTheme.bodyLarge?.copyWith(
|
||||||
|
color: colorScheme.onSurfaceVariant,
|
||||||
|
height: 1.45,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 28),
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: colorScheme.surfaceContainer.withValues(
|
||||||
|
alpha: 0.9,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(28),
|
||||||
|
border: Border.all(
|
||||||
|
color: Color.alphaBlend(
|
||||||
|
_brandGrey.withValues(alpha: 0.18),
|
||||||
|
colorScheme.outlineVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 36,
|
||||||
|
height: 36,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: theme
|
||||||
|
.colorScheme
|
||||||
|
.surfaceContainerHigh,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: const Icon(
|
||||||
|
MdiIcons.formatQuoteOpen,
|
||||||
|
size: 18,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
'A thought for the road',
|
||||||
|
style: theme.textTheme.titleMedium
|
||||||
|
?.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton.filledTonal(
|
||||||
|
tooltip: 'Refresh quote',
|
||||||
|
onPressed: () {
|
||||||
|
ref.invalidate(randomQuoteProvider);
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.refresh_rounded),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
switch (quoteAsync) {
|
||||||
|
AsyncData(:final value) => _QuoteBlock(
|
||||||
|
quote: value,
|
||||||
|
),
|
||||||
|
AsyncError() => Text(
|
||||||
|
'Open a new tab and make this space your own.',
|
||||||
|
style: theme.textTheme.bodyLarge?.copyWith(
|
||||||
|
color: colorScheme.onSurfaceVariant,
|
||||||
|
height: 1.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
_ => const LinearProgressIndicator(
|
||||||
|
minHeight: 3,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
FilledButton.icon(
|
||||||
|
onPressed: openNewTab,
|
||||||
|
icon: const Icon(Icons.add_rounded),
|
||||||
|
label: const Text('Open new tab'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _QuoteBlock extends StatelessWidget {
|
||||||
|
final Quote? quote;
|
||||||
|
|
||||||
|
const _QuoteBlock({required this.quote});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final colorScheme = theme.colorScheme;
|
||||||
|
|
||||||
|
if (quote == null) {
|
||||||
|
return Text(
|
||||||
|
'Open a new tab and make this space your own.',
|
||||||
|
style: theme.textTheme.bodyLarge?.copyWith(
|
||||||
|
color: colorScheme.onSurfaceVariant,
|
||||||
|
height: 1.5,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'"${quote!.quote}"',
|
||||||
|
style: theme.textTheme.bodyLarge?.copyWith(
|
||||||
|
height: 1.55,
|
||||||
|
color: colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text(
|
||||||
|
'- ${quote!.author}',
|
||||||
|
style: theme.textTheme.titleSmall?.copyWith(
|
||||||
|
color: colorScheme.onSurfaceVariant,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (quote!.source case final String source when source.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
source,
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BackdropOrb extends StatelessWidget {
|
||||||
|
final double size;
|
||||||
|
final Color color;
|
||||||
|
|
||||||
|
const _BackdropOrb({required this.size, required this.color});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return IgnorePointer(
|
||||||
|
child: Container(
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
decoration: BoxDecoration(shape: BoxShape.circle, color: color),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
* 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/quotes/data/database/daos/quote.drift.dart';
|
||||||
|
import 'package:weblibre/features/quotes/data/database/database.dart';
|
||||||
|
import 'package:weblibre/features/quotes/data/database/definitions.drift.dart';
|
||||||
|
|
||||||
|
@DriftAccessor()
|
||||||
|
class QuoteDao extends DatabaseAccessor<QuotesDatabase> with $QuoteDaoMixin {
|
||||||
|
QuoteDao(super.db);
|
||||||
|
|
||||||
|
SingleOrNullSelectable<Quote> getRandomQuote() {
|
||||||
|
final statement = select(db.quotes)
|
||||||
|
..orderBy([(t) => OrderingTerm.random()])
|
||||||
|
..limit(1);
|
||||||
|
|
||||||
|
return statement;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
// dart format width=80
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
import 'package:drift/drift.dart' as i0;
|
||||||
|
import 'package:weblibre/features/quotes/data/database/database.dart' as i1;
|
||||||
|
|
||||||
|
mixin $QuoteDaoMixin on i0.DatabaseAccessor<i1.QuotesDatabase> {
|
||||||
|
QuoteDaoManager get managers => QuoteDaoManager(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
class QuoteDaoManager {
|
||||||
|
final $QuoteDaoMixin _db;
|
||||||
|
QuoteDaoManager(this._db);
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* 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:drift_dev/api/migrations_native.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:weblibre/features/quotes/data/database/daos/quote.dart';
|
||||||
|
import 'package:weblibre/features/quotes/data/database/database.drift.dart';
|
||||||
|
|
||||||
|
@DriftDatabase(include: {'definitions.drift'}, daos: [QuoteDao])
|
||||||
|
class QuotesDatabase extends $QuotesDatabase {
|
||||||
|
@override
|
||||||
|
final int schemaVersion = 1;
|
||||||
|
|
||||||
|
@override
|
||||||
|
MigrationStrategy get migration => MigrationStrategy(
|
||||||
|
beforeOpen: (details) async {
|
||||||
|
if (kDebugMode) {
|
||||||
|
await validateDatabaseSchema();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
QuotesDatabase(super.e);
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
// dart format width=80
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
import 'package:drift/drift.dart' as i0;
|
||||||
|
import 'package:weblibre/features/quotes/data/database/definitions.drift.dart'
|
||||||
|
as i1;
|
||||||
|
import 'package:weblibre/features/quotes/data/database/daos/quote.dart' as i2;
|
||||||
|
import 'package:weblibre/features/quotes/data/database/database.dart' as i3;
|
||||||
|
import 'package:sqlite3/common.dart' as i4;
|
||||||
|
|
||||||
|
abstract class $QuotesDatabase extends i0.GeneratedDatabase {
|
||||||
|
$QuotesDatabase(i0.QueryExecutor e) : super(e);
|
||||||
|
$QuotesDatabaseManager get managers => $QuotesDatabaseManager(this);
|
||||||
|
late final i1.Quotes quotes = i1.Quotes(this);
|
||||||
|
late final i2.QuoteDao quoteDao = i2.QuoteDao(this as i3.QuotesDatabase);
|
||||||
|
@override
|
||||||
|
Iterable<i0.TableInfo<i0.Table, Object?>> get allTables =>
|
||||||
|
allSchemaEntities.whereType<i0.TableInfo<i0.Table, Object?>>();
|
||||||
|
@override
|
||||||
|
List<i0.DatabaseSchemaEntity> get allSchemaEntities => [quotes];
|
||||||
|
}
|
||||||
|
|
||||||
|
class $QuotesDatabaseManager {
|
||||||
|
final $QuotesDatabase _db;
|
||||||
|
$QuotesDatabaseManager(this._db);
|
||||||
|
i1.$QuotesTableManager get quotes => i1.$QuotesTableManager(_db, _db.quotes);
|
||||||
|
}
|
||||||
|
|
||||||
|
extension DefineFunctions on i4.CommonDatabase {
|
||||||
|
void defineFunctions({
|
||||||
|
required String Function(int, String?) lexoRankNext,
|
||||||
|
required String Function(int, String?) lexoRankPrevious,
|
||||||
|
required String Function(String?, String?) lexoRankReorderAfter,
|
||||||
|
required String Function(String?, String?) lexoRankReorderBefore,
|
||||||
|
}) {
|
||||||
|
createFunction(
|
||||||
|
functionName: 'lexo_rank_next',
|
||||||
|
argumentCount: const i4.AllowedArgumentCount(2),
|
||||||
|
function: (args) {
|
||||||
|
final arg0 = args[0] as int;
|
||||||
|
final arg1 = args[1] as String?;
|
||||||
|
return lexoRankNext(arg0, arg1);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
createFunction(
|
||||||
|
functionName: 'lexo_rank_previous',
|
||||||
|
argumentCount: const i4.AllowedArgumentCount(2),
|
||||||
|
function: (args) {
|
||||||
|
final arg0 = args[0] as int;
|
||||||
|
final arg1 = args[1] as String?;
|
||||||
|
return lexoRankPrevious(arg0, arg1);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
createFunction(
|
||||||
|
functionName: 'lexo_rank_reorder_after',
|
||||||
|
argumentCount: const i4.AllowedArgumentCount(2),
|
||||||
|
function: (args) {
|
||||||
|
final arg0 = args[0] as String?;
|
||||||
|
final arg1 = args[1] as String?;
|
||||||
|
return lexoRankReorderAfter(arg0, arg1);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
createFunction(
|
||||||
|
functionName: 'lexo_rank_reorder_before',
|
||||||
|
argumentCount: const i4.AllowedArgumentCount(2),
|
||||||
|
function: (args) {
|
||||||
|
final arg0 = args[0] as String?;
|
||||||
|
final arg1 = args[1] as String?;
|
||||||
|
return lexoRankReorderBefore(arg0, arg1);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
CREATE TABLE quotes (
|
||||||
|
author TEXT NOT NULL,
|
||||||
|
quote TEXT NOT NULL,
|
||||||
|
source TEXT,
|
||||||
|
tags TEXT
|
||||||
|
);
|
||||||
@@ -0,0 +1,440 @@
|
|||||||
|
// dart format width=80
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
import 'package:drift/drift.dart' as i0;
|
||||||
|
import 'package:weblibre/features/quotes/data/database/definitions.drift.dart'
|
||||||
|
as i1;
|
||||||
|
|
||||||
|
typedef $QuotesCreateCompanionBuilder =
|
||||||
|
i1.QuotesCompanion Function({
|
||||||
|
required String author,
|
||||||
|
required String quote,
|
||||||
|
i0.Value<String?> source,
|
||||||
|
i0.Value<String?> tags,
|
||||||
|
i0.Value<int> rowid,
|
||||||
|
});
|
||||||
|
typedef $QuotesUpdateCompanionBuilder =
|
||||||
|
i1.QuotesCompanion Function({
|
||||||
|
i0.Value<String> author,
|
||||||
|
i0.Value<String> quote,
|
||||||
|
i0.Value<String?> source,
|
||||||
|
i0.Value<String?> tags,
|
||||||
|
i0.Value<int> rowid,
|
||||||
|
});
|
||||||
|
|
||||||
|
class $QuotesFilterComposer
|
||||||
|
extends i0.Composer<i0.GeneratedDatabase, i1.Quotes> {
|
||||||
|
$QuotesFilterComposer({
|
||||||
|
required super.$db,
|
||||||
|
required super.$table,
|
||||||
|
super.joinBuilder,
|
||||||
|
super.$addJoinBuilderToRootComposer,
|
||||||
|
super.$removeJoinBuilderFromRootComposer,
|
||||||
|
});
|
||||||
|
i0.ColumnFilters<String> get author => $composableBuilder(
|
||||||
|
column: $table.author,
|
||||||
|
builder: (column) => i0.ColumnFilters(column),
|
||||||
|
);
|
||||||
|
|
||||||
|
i0.ColumnFilters<String> get quote => $composableBuilder(
|
||||||
|
column: $table.quote,
|
||||||
|
builder: (column) => i0.ColumnFilters(column),
|
||||||
|
);
|
||||||
|
|
||||||
|
i0.ColumnFilters<String> get source => $composableBuilder(
|
||||||
|
column: $table.source,
|
||||||
|
builder: (column) => i0.ColumnFilters(column),
|
||||||
|
);
|
||||||
|
|
||||||
|
i0.ColumnFilters<String> get tags => $composableBuilder(
|
||||||
|
column: $table.tags,
|
||||||
|
builder: (column) => i0.ColumnFilters(column),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class $QuotesOrderingComposer
|
||||||
|
extends i0.Composer<i0.GeneratedDatabase, i1.Quotes> {
|
||||||
|
$QuotesOrderingComposer({
|
||||||
|
required super.$db,
|
||||||
|
required super.$table,
|
||||||
|
super.joinBuilder,
|
||||||
|
super.$addJoinBuilderToRootComposer,
|
||||||
|
super.$removeJoinBuilderFromRootComposer,
|
||||||
|
});
|
||||||
|
i0.ColumnOrderings<String> get author => $composableBuilder(
|
||||||
|
column: $table.author,
|
||||||
|
builder: (column) => i0.ColumnOrderings(column),
|
||||||
|
);
|
||||||
|
|
||||||
|
i0.ColumnOrderings<String> get quote => $composableBuilder(
|
||||||
|
column: $table.quote,
|
||||||
|
builder: (column) => i0.ColumnOrderings(column),
|
||||||
|
);
|
||||||
|
|
||||||
|
i0.ColumnOrderings<String> get source => $composableBuilder(
|
||||||
|
column: $table.source,
|
||||||
|
builder: (column) => i0.ColumnOrderings(column),
|
||||||
|
);
|
||||||
|
|
||||||
|
i0.ColumnOrderings<String> get tags => $composableBuilder(
|
||||||
|
column: $table.tags,
|
||||||
|
builder: (column) => i0.ColumnOrderings(column),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class $QuotesAnnotationComposer
|
||||||
|
extends i0.Composer<i0.GeneratedDatabase, i1.Quotes> {
|
||||||
|
$QuotesAnnotationComposer({
|
||||||
|
required super.$db,
|
||||||
|
required super.$table,
|
||||||
|
super.joinBuilder,
|
||||||
|
super.$addJoinBuilderToRootComposer,
|
||||||
|
super.$removeJoinBuilderFromRootComposer,
|
||||||
|
});
|
||||||
|
i0.GeneratedColumn<String> get author =>
|
||||||
|
$composableBuilder(column: $table.author, builder: (column) => column);
|
||||||
|
|
||||||
|
i0.GeneratedColumn<String> get quote =>
|
||||||
|
$composableBuilder(column: $table.quote, builder: (column) => column);
|
||||||
|
|
||||||
|
i0.GeneratedColumn<String> get source =>
|
||||||
|
$composableBuilder(column: $table.source, builder: (column) => column);
|
||||||
|
|
||||||
|
i0.GeneratedColumn<String> get tags =>
|
||||||
|
$composableBuilder(column: $table.tags, builder: (column) => column);
|
||||||
|
}
|
||||||
|
|
||||||
|
class $QuotesTableManager
|
||||||
|
extends
|
||||||
|
i0.RootTableManager<
|
||||||
|
i0.GeneratedDatabase,
|
||||||
|
i1.Quotes,
|
||||||
|
i1.Quote,
|
||||||
|
i1.$QuotesFilterComposer,
|
||||||
|
i1.$QuotesOrderingComposer,
|
||||||
|
i1.$QuotesAnnotationComposer,
|
||||||
|
$QuotesCreateCompanionBuilder,
|
||||||
|
$QuotesUpdateCompanionBuilder,
|
||||||
|
(
|
||||||
|
i1.Quote,
|
||||||
|
i0.BaseReferences<i0.GeneratedDatabase, i1.Quotes, i1.Quote>,
|
||||||
|
),
|
||||||
|
i1.Quote,
|
||||||
|
i0.PrefetchHooks Function()
|
||||||
|
> {
|
||||||
|
$QuotesTableManager(i0.GeneratedDatabase db, i1.Quotes table)
|
||||||
|
: super(
|
||||||
|
i0.TableManagerState(
|
||||||
|
db: db,
|
||||||
|
table: table,
|
||||||
|
createFilteringComposer: () =>
|
||||||
|
i1.$QuotesFilterComposer($db: db, $table: table),
|
||||||
|
createOrderingComposer: () =>
|
||||||
|
i1.$QuotesOrderingComposer($db: db, $table: table),
|
||||||
|
createComputedFieldComposer: () =>
|
||||||
|
i1.$QuotesAnnotationComposer($db: db, $table: table),
|
||||||
|
updateCompanionCallback:
|
||||||
|
({
|
||||||
|
i0.Value<String> author = const i0.Value.absent(),
|
||||||
|
i0.Value<String> quote = const i0.Value.absent(),
|
||||||
|
i0.Value<String?> source = const i0.Value.absent(),
|
||||||
|
i0.Value<String?> tags = const i0.Value.absent(),
|
||||||
|
i0.Value<int> rowid = const i0.Value.absent(),
|
||||||
|
}) => i1.QuotesCompanion(
|
||||||
|
author: author,
|
||||||
|
quote: quote,
|
||||||
|
source: source,
|
||||||
|
tags: tags,
|
||||||
|
rowid: rowid,
|
||||||
|
),
|
||||||
|
createCompanionCallback:
|
||||||
|
({
|
||||||
|
required String author,
|
||||||
|
required String quote,
|
||||||
|
i0.Value<String?> source = const i0.Value.absent(),
|
||||||
|
i0.Value<String?> tags = const i0.Value.absent(),
|
||||||
|
i0.Value<int> rowid = const i0.Value.absent(),
|
||||||
|
}) => i1.QuotesCompanion.insert(
|
||||||
|
author: author,
|
||||||
|
quote: quote,
|
||||||
|
source: source,
|
||||||
|
tags: tags,
|
||||||
|
rowid: rowid,
|
||||||
|
),
|
||||||
|
withReferenceMapper: (p0) => p0
|
||||||
|
.map((e) => (e.readTable(table), i0.BaseReferences(db, table, e)))
|
||||||
|
.toList(),
|
||||||
|
prefetchHooksCallback: null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef $QuotesProcessedTableManager =
|
||||||
|
i0.ProcessedTableManager<
|
||||||
|
i0.GeneratedDatabase,
|
||||||
|
i1.Quotes,
|
||||||
|
i1.Quote,
|
||||||
|
i1.$QuotesFilterComposer,
|
||||||
|
i1.$QuotesOrderingComposer,
|
||||||
|
i1.$QuotesAnnotationComposer,
|
||||||
|
$QuotesCreateCompanionBuilder,
|
||||||
|
$QuotesUpdateCompanionBuilder,
|
||||||
|
(i1.Quote, i0.BaseReferences<i0.GeneratedDatabase, i1.Quotes, i1.Quote>),
|
||||||
|
i1.Quote,
|
||||||
|
i0.PrefetchHooks Function()
|
||||||
|
>;
|
||||||
|
|
||||||
|
class Quotes extends i0.Table with i0.TableInfo<Quotes, i1.Quote> {
|
||||||
|
@override
|
||||||
|
final i0.GeneratedDatabase attachedDatabase;
|
||||||
|
final String? _alias;
|
||||||
|
Quotes(this.attachedDatabase, [this._alias]);
|
||||||
|
late final i0.GeneratedColumn<String> author = i0.GeneratedColumn<String>(
|
||||||
|
'author',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i0.DriftSqlType.string,
|
||||||
|
requiredDuringInsert: true,
|
||||||
|
$customConstraints: 'NOT NULL',
|
||||||
|
);
|
||||||
|
late final i0.GeneratedColumn<String> quote = i0.GeneratedColumn<String>(
|
||||||
|
'quote',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i0.DriftSqlType.string,
|
||||||
|
requiredDuringInsert: true,
|
||||||
|
$customConstraints: 'NOT NULL',
|
||||||
|
);
|
||||||
|
late final i0.GeneratedColumn<String> source = i0.GeneratedColumn<String>(
|
||||||
|
'source',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: i0.DriftSqlType.string,
|
||||||
|
requiredDuringInsert: false,
|
||||||
|
$customConstraints: '',
|
||||||
|
);
|
||||||
|
late final i0.GeneratedColumn<String> tags = i0.GeneratedColumn<String>(
|
||||||
|
'tags',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: i0.DriftSqlType.string,
|
||||||
|
requiredDuringInsert: false,
|
||||||
|
$customConstraints: '',
|
||||||
|
);
|
||||||
|
@override
|
||||||
|
List<i0.GeneratedColumn> get $columns => [author, quote, source, tags];
|
||||||
|
@override
|
||||||
|
String get aliasedName => _alias ?? actualTableName;
|
||||||
|
@override
|
||||||
|
String get actualTableName => $name;
|
||||||
|
static const String $name = 'quotes';
|
||||||
|
@override
|
||||||
|
Set<i0.GeneratedColumn> get $primaryKey => const {};
|
||||||
|
@override
|
||||||
|
i1.Quote map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||||
|
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
|
||||||
|
return i1.Quote(
|
||||||
|
author: attachedDatabase.typeMapping.read(
|
||||||
|
i0.DriftSqlType.string,
|
||||||
|
data['${effectivePrefix}author'],
|
||||||
|
)!,
|
||||||
|
quote: attachedDatabase.typeMapping.read(
|
||||||
|
i0.DriftSqlType.string,
|
||||||
|
data['${effectivePrefix}quote'],
|
||||||
|
)!,
|
||||||
|
source: attachedDatabase.typeMapping.read(
|
||||||
|
i0.DriftSqlType.string,
|
||||||
|
data['${effectivePrefix}source'],
|
||||||
|
),
|
||||||
|
tags: attachedDatabase.typeMapping.read(
|
||||||
|
i0.DriftSqlType.string,
|
||||||
|
data['${effectivePrefix}tags'],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Quotes createAlias(String alias) {
|
||||||
|
return Quotes(attachedDatabase, alias);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get dontWriteConstraints => true;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Quote extends i0.DataClass implements i0.Insertable<i1.Quote> {
|
||||||
|
final String author;
|
||||||
|
final String quote;
|
||||||
|
final String? source;
|
||||||
|
final String? tags;
|
||||||
|
const Quote({
|
||||||
|
required this.author,
|
||||||
|
required this.quote,
|
||||||
|
this.source,
|
||||||
|
this.tags,
|
||||||
|
});
|
||||||
|
@override
|
||||||
|
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
|
||||||
|
final map = <String, i0.Expression>{};
|
||||||
|
map['author'] = i0.Variable<String>(author);
|
||||||
|
map['quote'] = i0.Variable<String>(quote);
|
||||||
|
if (!nullToAbsent || source != null) {
|
||||||
|
map['source'] = i0.Variable<String>(source);
|
||||||
|
}
|
||||||
|
if (!nullToAbsent || tags != null) {
|
||||||
|
map['tags'] = i0.Variable<String>(tags);
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
factory Quote.fromJson(
|
||||||
|
Map<String, dynamic> json, {
|
||||||
|
i0.ValueSerializer? serializer,
|
||||||
|
}) {
|
||||||
|
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
|
||||||
|
return Quote(
|
||||||
|
author: serializer.fromJson<String>(json['author']),
|
||||||
|
quote: serializer.fromJson<String>(json['quote']),
|
||||||
|
source: serializer.fromJson<String?>(json['source']),
|
||||||
|
tags: serializer.fromJson<String?>(json['tags']),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson({i0.ValueSerializer? serializer}) {
|
||||||
|
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
|
||||||
|
return <String, dynamic>{
|
||||||
|
'author': serializer.toJson<String>(author),
|
||||||
|
'quote': serializer.toJson<String>(quote),
|
||||||
|
'source': serializer.toJson<String?>(source),
|
||||||
|
'tags': serializer.toJson<String?>(tags),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
i1.Quote copyWith({
|
||||||
|
String? author,
|
||||||
|
String? quote,
|
||||||
|
i0.Value<String?> source = const i0.Value.absent(),
|
||||||
|
i0.Value<String?> tags = const i0.Value.absent(),
|
||||||
|
}) => i1.Quote(
|
||||||
|
author: author ?? this.author,
|
||||||
|
quote: quote ?? this.quote,
|
||||||
|
source: source.present ? source.value : this.source,
|
||||||
|
tags: tags.present ? tags.value : this.tags,
|
||||||
|
);
|
||||||
|
Quote copyWithCompanion(i1.QuotesCompanion data) {
|
||||||
|
return Quote(
|
||||||
|
author: data.author.present ? data.author.value : this.author,
|
||||||
|
quote: data.quote.present ? data.quote.value : this.quote,
|
||||||
|
source: data.source.present ? data.source.value : this.source,
|
||||||
|
tags: data.tags.present ? data.tags.value : this.tags,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return (StringBuffer('Quote(')
|
||||||
|
..write('author: $author, ')
|
||||||
|
..write('quote: $quote, ')
|
||||||
|
..write('source: $source, ')
|
||||||
|
..write('tags: $tags')
|
||||||
|
..write(')'))
|
||||||
|
.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(author, quote, source, tags);
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
(other is i1.Quote &&
|
||||||
|
other.author == this.author &&
|
||||||
|
other.quote == this.quote &&
|
||||||
|
other.source == this.source &&
|
||||||
|
other.tags == this.tags);
|
||||||
|
}
|
||||||
|
|
||||||
|
class QuotesCompanion extends i0.UpdateCompanion<i1.Quote> {
|
||||||
|
final i0.Value<String> author;
|
||||||
|
final i0.Value<String> quote;
|
||||||
|
final i0.Value<String?> source;
|
||||||
|
final i0.Value<String?> tags;
|
||||||
|
final i0.Value<int> rowid;
|
||||||
|
const QuotesCompanion({
|
||||||
|
this.author = const i0.Value.absent(),
|
||||||
|
this.quote = const i0.Value.absent(),
|
||||||
|
this.source = const i0.Value.absent(),
|
||||||
|
this.tags = const i0.Value.absent(),
|
||||||
|
this.rowid = const i0.Value.absent(),
|
||||||
|
});
|
||||||
|
QuotesCompanion.insert({
|
||||||
|
required String author,
|
||||||
|
required String quote,
|
||||||
|
this.source = const i0.Value.absent(),
|
||||||
|
this.tags = const i0.Value.absent(),
|
||||||
|
this.rowid = const i0.Value.absent(),
|
||||||
|
}) : author = i0.Value(author),
|
||||||
|
quote = i0.Value(quote);
|
||||||
|
static i0.Insertable<i1.Quote> custom({
|
||||||
|
i0.Expression<String>? author,
|
||||||
|
i0.Expression<String>? quote,
|
||||||
|
i0.Expression<String>? source,
|
||||||
|
i0.Expression<String>? tags,
|
||||||
|
i0.Expression<int>? rowid,
|
||||||
|
}) {
|
||||||
|
return i0.RawValuesInsertable({
|
||||||
|
if (author != null) 'author': author,
|
||||||
|
if (quote != null) 'quote': quote,
|
||||||
|
if (source != null) 'source': source,
|
||||||
|
if (tags != null) 'tags': tags,
|
||||||
|
if (rowid != null) 'rowid': rowid,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
i1.QuotesCompanion copyWith({
|
||||||
|
i0.Value<String>? author,
|
||||||
|
i0.Value<String>? quote,
|
||||||
|
i0.Value<String?>? source,
|
||||||
|
i0.Value<String?>? tags,
|
||||||
|
i0.Value<int>? rowid,
|
||||||
|
}) {
|
||||||
|
return i1.QuotesCompanion(
|
||||||
|
author: author ?? this.author,
|
||||||
|
quote: quote ?? this.quote,
|
||||||
|
source: source ?? this.source,
|
||||||
|
tags: tags ?? this.tags,
|
||||||
|
rowid: rowid ?? this.rowid,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
|
||||||
|
final map = <String, i0.Expression>{};
|
||||||
|
if (author.present) {
|
||||||
|
map['author'] = i0.Variable<String>(author.value);
|
||||||
|
}
|
||||||
|
if (quote.present) {
|
||||||
|
map['quote'] = i0.Variable<String>(quote.value);
|
||||||
|
}
|
||||||
|
if (source.present) {
|
||||||
|
map['source'] = i0.Variable<String>(source.value);
|
||||||
|
}
|
||||||
|
if (tags.present) {
|
||||||
|
map['tags'] = i0.Variable<String>(tags.value);
|
||||||
|
}
|
||||||
|
if (rowid.present) {
|
||||||
|
map['rowid'] = i0.Variable<int>(rowid.value);
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return (StringBuffer('QuotesCompanion(')
|
||||||
|
..write('author: $author, ')
|
||||||
|
..write('quote: $quote, ')
|
||||||
|
..write('source: $source, ')
|
||||||
|
..write('tags: $tags, ')
|
||||||
|
..write('rowid: $rowid')
|
||||||
|
..write(')'))
|
||||||
|
.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
/*
|
||||||
|
* 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:io';
|
||||||
|
|
||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:drift/native.dart';
|
||||||
|
import 'package:flutter/services.dart' show rootBundle;
|
||||||
|
import 'package:path/path.dart' as p;
|
||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
import 'package:sqlite3_flutter_libs/sqlite3_flutter_libs.dart';
|
||||||
|
|
||||||
|
import 'package:weblibre/core/filesystem.dart';
|
||||||
|
import 'package:weblibre/features/quotes/data/database/database.dart';
|
||||||
|
|
||||||
|
part 'providers.g.dart';
|
||||||
|
|
||||||
|
const _quotesAssetPath = 'assets/quotes/quotes.db';
|
||||||
|
const _quotesDbFileName = 'quotes.db';
|
||||||
|
|
||||||
|
@Riverpod(keepAlive: true)
|
||||||
|
QuotesDatabase quotesDatabase(Ref ref) {
|
||||||
|
final db = QuotesDatabase(
|
||||||
|
LazyDatabase(() async {
|
||||||
|
final file = File(
|
||||||
|
p.join(filesystem.profileDatabasesDir.path, _quotesDbFileName),
|
||||||
|
);
|
||||||
|
|
||||||
|
await file.parent.create(recursive: true);
|
||||||
|
|
||||||
|
final blob = await rootBundle.load(_quotesAssetPath);
|
||||||
|
final buffer = blob.buffer;
|
||||||
|
await file.writeAsBytes(
|
||||||
|
buffer.asUint8List(blob.offsetInBytes, blob.lengthInBytes),
|
||||||
|
flush: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (Platform.isAndroid) {
|
||||||
|
await applyWorkaroundToOpenSqlite3OnOldAndroidVersions();
|
||||||
|
}
|
||||||
|
|
||||||
|
return NativeDatabase.createInBackground(file);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
ref.onDispose(() async {
|
||||||
|
await db.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
return db;
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'providers.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// RiverpodGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
// ignore_for_file: type=lint, type=warning
|
||||||
|
|
||||||
|
@ProviderFor(quotesDatabase)
|
||||||
|
final quotesDatabaseProvider = QuotesDatabaseProvider._();
|
||||||
|
|
||||||
|
final class QuotesDatabaseProvider
|
||||||
|
extends $FunctionalProvider<QuotesDatabase, QuotesDatabase, QuotesDatabase>
|
||||||
|
with $Provider<QuotesDatabase> {
|
||||||
|
QuotesDatabaseProvider._()
|
||||||
|
: super(
|
||||||
|
from: null,
|
||||||
|
argument: null,
|
||||||
|
retry: null,
|
||||||
|
name: r'quotesDatabaseProvider',
|
||||||
|
isAutoDispose: false,
|
||||||
|
dependencies: null,
|
||||||
|
$allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String debugGetCreateSourceHash() => _$quotesDatabaseHash();
|
||||||
|
|
||||||
|
@$internal
|
||||||
|
@override
|
||||||
|
$ProviderElement<QuotesDatabase> $createElement($ProviderPointer pointer) =>
|
||||||
|
$ProviderElement(pointer);
|
||||||
|
|
||||||
|
@override
|
||||||
|
QuotesDatabase create(Ref ref) {
|
||||||
|
return quotesDatabase(ref);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// {@macro riverpod.override_with_value}
|
||||||
|
Override overrideWithValue(QuotesDatabase value) {
|
||||||
|
return $ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
providerOverride: $SyncValueProvider<QuotesDatabase>(value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$quotesDatabaseHash() => r'e5a15556bef876f3a4941957f01bc1457a12a956';
|
||||||
@@ -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/>.
|
||||||
|
*/
|
||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
import 'package:weblibre/features/quotes/data/database/definitions.drift.dart';
|
||||||
|
import 'package:weblibre/features/quotes/domain/repositories/quotes.dart';
|
||||||
|
|
||||||
|
part 'providers.g.dart';
|
||||||
|
|
||||||
|
@Riverpod()
|
||||||
|
Future<Quote?> randomQuote(Ref ref) {
|
||||||
|
return ref.watch(quotesRepositoryProvider.notifier).getRandomQuote();
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'providers.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// RiverpodGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
// ignore_for_file: type=lint, type=warning
|
||||||
|
|
||||||
|
@ProviderFor(randomQuote)
|
||||||
|
final randomQuoteProvider = RandomQuoteProvider._();
|
||||||
|
|
||||||
|
final class RandomQuoteProvider
|
||||||
|
extends $FunctionalProvider<AsyncValue<Quote?>, Quote?, FutureOr<Quote?>>
|
||||||
|
with $FutureModifier<Quote?>, $FutureProvider<Quote?> {
|
||||||
|
RandomQuoteProvider._()
|
||||||
|
: super(
|
||||||
|
from: null,
|
||||||
|
argument: null,
|
||||||
|
retry: null,
|
||||||
|
name: r'randomQuoteProvider',
|
||||||
|
isAutoDispose: true,
|
||||||
|
dependencies: null,
|
||||||
|
$allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String debugGetCreateSourceHash() => _$randomQuoteHash();
|
||||||
|
|
||||||
|
@$internal
|
||||||
|
@override
|
||||||
|
$FutureProviderElement<Quote?> $createElement($ProviderPointer pointer) =>
|
||||||
|
$FutureProviderElement(pointer);
|
||||||
|
|
||||||
|
@override
|
||||||
|
FutureOr<Quote?> create(Ref ref) {
|
||||||
|
return randomQuote(ref);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$randomQuoteHash() => r'045fe6256f03e90d2fa5e42c574d58b4aa2cc1d7';
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
/*
|
||||||
|
* 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/quotes/data/database/definitions.drift.dart';
|
||||||
|
import 'package:weblibre/features/quotes/data/providers.dart';
|
||||||
|
|
||||||
|
part 'quotes.g.dart';
|
||||||
|
|
||||||
|
@Riverpod(keepAlive: true)
|
||||||
|
class QuotesRepository extends _$QuotesRepository {
|
||||||
|
Future<Quote?> getRandomQuote() {
|
||||||
|
return ref
|
||||||
|
.read(quotesDatabaseProvider)
|
||||||
|
.quoteDao
|
||||||
|
.getRandomQuote()
|
||||||
|
.getSingleOrNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void build() {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'quotes.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// RiverpodGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
// ignore_for_file: type=lint, type=warning
|
||||||
|
|
||||||
|
@ProviderFor(QuotesRepository)
|
||||||
|
final quotesRepositoryProvider = QuotesRepositoryProvider._();
|
||||||
|
|
||||||
|
final class QuotesRepositoryProvider
|
||||||
|
extends $NotifierProvider<QuotesRepository, void> {
|
||||||
|
QuotesRepositoryProvider._()
|
||||||
|
: super(
|
||||||
|
from: null,
|
||||||
|
argument: null,
|
||||||
|
retry: null,
|
||||||
|
name: r'quotesRepositoryProvider',
|
||||||
|
isAutoDispose: false,
|
||||||
|
dependencies: null,
|
||||||
|
$allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String debugGetCreateSourceHash() => _$quotesRepositoryHash();
|
||||||
|
|
||||||
|
@$internal
|
||||||
|
@override
|
||||||
|
QuotesRepository create() => QuotesRepository();
|
||||||
|
|
||||||
|
/// {@macro riverpod.override_with_value}
|
||||||
|
Override overrideWithValue(void value) {
|
||||||
|
return $ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
providerOverride: $SyncValueProvider<void>(value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$quotesRepositoryHash() => r'2e30c503da5619a1d15918029908890738bf5aab';
|
||||||
|
|
||||||
|
abstract class _$QuotesRepository extends $Notifier<void> {
|
||||||
|
void build();
|
||||||
|
@$mustCallSuper
|
||||||
|
@override
|
||||||
|
void runBuild() {
|
||||||
|
final ref = this.ref as $Ref<void, void>;
|
||||||
|
final element =
|
||||||
|
ref.element
|
||||||
|
as $ClassProviderElement<
|
||||||
|
AnyNotifier<void, void>,
|
||||||
|
void,
|
||||||
|
Object?,
|
||||||
|
Object?
|
||||||
|
>;
|
||||||
|
element.handleCreate(ref, build);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -115,6 +115,7 @@ flutter:
|
|||||||
- assets/images/
|
- assets/images/
|
||||||
- assets/landing/
|
- assets/landing/
|
||||||
- assets/preferences/
|
- assets/preferences/
|
||||||
|
- assets/quotes/
|
||||||
fonts:
|
fonts:
|
||||||
- family: TorIcons
|
- family: TorIcons
|
||||||
fonts:
|
fonts:
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import sqlite3
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||||
|
DEFAULT_TABLE_NAME = "quotes"
|
||||||
|
DEFAULT_INPUT_PATH = REPO_ROOT / "scripts" / "quotes" / "quotes.json"
|
||||||
|
DEFAULT_OUTPUT_PATH = REPO_ROOT / "app" / "assets" / "quotes" / "quotes.db"
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args() -> argparse.Namespace:
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Build a SQLite quotes database from a JSON file."
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--input",
|
||||||
|
default=str(DEFAULT_INPUT_PATH),
|
||||||
|
help="Path to the source JSON file (default: quotes.json)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--output",
|
||||||
|
default=str(DEFAULT_OUTPUT_PATH),
|
||||||
|
help="Path to the SQLite database file (default: quotes.db)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--table",
|
||||||
|
default=DEFAULT_TABLE_NAME,
|
||||||
|
help=f"Table name to create (default: {DEFAULT_TABLE_NAME})",
|
||||||
|
)
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def load_quotes(json_path: Path) -> list[tuple[str, str, str | None, str | None]]:
|
||||||
|
with json_path.open("r", encoding="utf-8") as handle:
|
||||||
|
payload = json.load(handle)
|
||||||
|
|
||||||
|
if not isinstance(payload, list):
|
||||||
|
raise ValueError("Expected the JSON file to contain a top-level array.")
|
||||||
|
|
||||||
|
rows = []
|
||||||
|
for index, item in enumerate(payload, start=1):
|
||||||
|
if not isinstance(item, dict):
|
||||||
|
raise ValueError(f"Entry {index} is not a JSON object.")
|
||||||
|
|
||||||
|
author = item.get("author")
|
||||||
|
text = item.get("text")
|
||||||
|
source = item.get("source")
|
||||||
|
tags = item.get("tags")
|
||||||
|
|
||||||
|
if not isinstance(author, str) or not author.strip():
|
||||||
|
raise ValueError(f"Entry {index} is missing a valid 'author' string.")
|
||||||
|
if not isinstance(text, str) or not text.strip():
|
||||||
|
raise ValueError(f"Entry {index} is missing a valid 'text' string.")
|
||||||
|
if source is not None and not isinstance(source, str):
|
||||||
|
raise ValueError(f"Entry {index} has a non-string 'source' value.")
|
||||||
|
if tags is not None and not isinstance(tags, str):
|
||||||
|
raise ValueError(f"Entry {index} has a non-string 'tags' value.")
|
||||||
|
|
||||||
|
rows.append((author, text, source, tags))
|
||||||
|
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def quote_identifier(name: str) -> str:
|
||||||
|
return '"' + name.replace('"', '""') + '"'
|
||||||
|
|
||||||
|
|
||||||
|
def build_database(db_path: Path, table_name: str, rows: list[tuple[str, str, str | None, str | None]]) -> None:
|
||||||
|
db_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
table_identifier = quote_identifier(table_name)
|
||||||
|
|
||||||
|
connection = sqlite3.connect(db_path)
|
||||||
|
try:
|
||||||
|
cursor = connection.cursor()
|
||||||
|
cursor.execute(f"DROP TABLE IF EXISTS {table_identifier}")
|
||||||
|
cursor.execute(
|
||||||
|
f"""
|
||||||
|
CREATE TABLE {table_identifier} (
|
||||||
|
author TEXT NOT NULL,
|
||||||
|
quote TEXT NOT NULL,
|
||||||
|
source TEXT,
|
||||||
|
tags TEXT
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
cursor.executemany(
|
||||||
|
f"INSERT INTO {table_identifier} (author, quote, source, tags) VALUES (?, ?, ?, ?)",
|
||||||
|
rows,
|
||||||
|
)
|
||||||
|
connection.commit()
|
||||||
|
cursor.execute("VACUUM")
|
||||||
|
finally:
|
||||||
|
connection.close()
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
args = parse_args()
|
||||||
|
json_path = Path(args.input).expanduser().resolve()
|
||||||
|
db_path = Path(args.output).expanduser().resolve()
|
||||||
|
|
||||||
|
rows = load_quotes(json_path)
|
||||||
|
build_database(db_path, args.table, rows)
|
||||||
|
|
||||||
|
print(f"Created {db_path} with {len(rows)} rows in table '{args.table}'.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user