This commit is contained in:
Fabian Freund
2026-03-13 19:14:39 +01:00
parent b9e97c4d41
commit 3262bb34b5
24 changed files with 8383 additions and 10 deletions
+1
View File
@@ -30,6 +30,7 @@ migrate_working_dir/
.pub-cache/
.pub/
/build/
assets/quotes/quotes.db
# Android related
**/android/**/gradle-wrapper.jar
+1
View File
@@ -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
@@ -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();
@@ -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;
}
}
@@ -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));
}
}
@@ -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);
}
}
+1
View File
@@ -115,6 +115,7 @@ flutter:
- assets/images/
- assets/landing/
- assets/preferences/
- assets/quotes/
fonts:
- family: TorIcons
fonts:
+112
View File
@@ -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