initial
This commit is contained in:
@@ -30,6 +30,7 @@ migrate_working_dir/
|
||||
.pub-cache/
|
||||
.pub/
|
||||
/build/
|
||||
assets/quotes/quotes.db
|
||||
|
||||
# Android related
|
||||
**/android/**/gradle-wrapper.jar
|
||||
|
||||
+2
-1
@@ -15,6 +15,7 @@ targets:
|
||||
named_parameters: true
|
||||
databases:
|
||||
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
|
||||
user: lib/features/user/data/database/database.dart
|
||||
web_feed: lib/features/web_feed/data/database/database.dart
|
||||
@@ -42,4 +43,4 @@ global_options:
|
||||
drift_dev:modular:
|
||||
runs_before:
|
||||
- riverpod_generator
|
||||
|
||||
|
||||
|
||||
@@ -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> {
|
||||
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/toolbar_visibility.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/browser_fab.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);
|
||||
|
||||
useEffect(() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
ref
|
||||
.read(browserViewportToolbarInsetsControllerProvider.notifier)
|
||||
.reset();
|
||||
});
|
||||
|
||||
return null;
|
||||
}, []);
|
||||
|
||||
useEffect(() {
|
||||
if (lastToolbarMaxHeightPx.value != effectiveToolbarHeightPx) {
|
||||
lastToolbarMaxHeightPx.value = effectiveToolbarHeightPx;
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
ref
|
||||
.read(browserViewportToolbarInsetsControllerProvider.notifier)
|
||||
.setDynamicToolbarMaxHeight(effectiveToolbarHeightPx);
|
||||
});
|
||||
|
||||
unawaited(
|
||||
viewportService.setDynamicToolbarMaxHeight(effectiveToolbarHeightPx),
|
||||
);
|
||||
@@ -465,6 +483,13 @@ class BrowserScreen extends HookConsumerWidget {
|
||||
|
||||
if (targetClippingPx != lastClippingPx.value) {
|
||||
lastClippingPx.value = targetClippingPx;
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
ref
|
||||
.read(browserViewportToolbarInsetsControllerProvider.notifier)
|
||||
.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/engine_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/preferences/data/repositories/preference_observer.dart';
|
||||
import 'package:weblibre/features/geckoview/features/pwa/domain/providers.dart';
|
||||
@@ -108,8 +109,6 @@ class _BrowserViewState extends ConsumerState<BrowserView>
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
useOnInitialization(() async {
|
||||
await ref
|
||||
.read(generalSettingsRepositoryProvider.notifier)
|
||||
@@ -292,12 +291,7 @@ class _BrowserViewState extends ConsumerState<BrowserView>
|
||||
},
|
||||
),
|
||||
),
|
||||
if (!hasTab)
|
||||
Positioned.fill(
|
||||
child: SizedBox.expand(
|
||||
child: Container(color: colorScheme.surfaceContainerHighest),
|
||||
),
|
||||
),
|
||||
if (!hasTab) const Positioned.fill(child: TabsEmptyPlaceholder()),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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/landing/
|
||||
- assets/preferences/
|
||||
- assets/quotes/
|
||||
fonts:
|
||||
- family: TorIcons
|
||||
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