prepare for multiple apps
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* 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/core/logger.dart';
|
||||
|
||||
class DatabaseRegistry {
|
||||
DatabaseRegistry._();
|
||||
static final instance = DatabaseRegistry._();
|
||||
|
||||
final _databases = <String, GeneratedDatabase>{};
|
||||
|
||||
void register(String name, GeneratedDatabase db) {
|
||||
_databases[name] = db;
|
||||
}
|
||||
|
||||
Future<void> closeAll() async {
|
||||
for (final entry in _databases.entries) {
|
||||
try {
|
||||
await entry.value.close();
|
||||
logger.i('${entry.key} database closed');
|
||||
} catch (e, st) {
|
||||
logger.e(
|
||||
'Failed to close ${entry.key} database',
|
||||
error: e,
|
||||
stackTrace: st,
|
||||
);
|
||||
}
|
||||
}
|
||||
_databases.clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
@immutable
|
||||
class AppColors extends ThemeExtension<AppColors> {
|
||||
const AppColors._({
|
||||
required this.seedColor,
|
||||
required this.privateTabPurple,
|
||||
required this.privateTabBackground,
|
||||
required this.privateTabForeground,
|
||||
required this.privateSelectionOverlay,
|
||||
required this.isolatedTabTeal,
|
||||
required this.isolatedTabBackground,
|
||||
required this.isolatedTabForeground,
|
||||
required this.isolatedSelectionOverlay,
|
||||
required this.torPurple,
|
||||
required this.torActiveGreen,
|
||||
required this.torBackgroundGrey,
|
||||
required this.warningAmber,
|
||||
required this.auraPurple,
|
||||
required this.auraGold,
|
||||
required this.auraShadow,
|
||||
required this.auraShadowHighlight,
|
||||
required this.auraTint,
|
||||
required this.brandLink,
|
||||
});
|
||||
|
||||
final Color seedColor;
|
||||
final Color privateTabPurple;
|
||||
final Color privateTabBackground;
|
||||
final Color privateTabForeground;
|
||||
final Color privateSelectionOverlay;
|
||||
final Color isolatedTabTeal;
|
||||
final Color isolatedTabBackground;
|
||||
final Color isolatedTabForeground;
|
||||
final Color isolatedSelectionOverlay;
|
||||
final Color torPurple;
|
||||
final Color torActiveGreen;
|
||||
final Color torBackgroundGrey;
|
||||
final Color warningAmber;
|
||||
final Color auraPurple;
|
||||
final Color auraGold;
|
||||
final Color auraShadow;
|
||||
final Color auraShadowHighlight;
|
||||
final Color auraTint;
|
||||
final Color brandLink;
|
||||
|
||||
/// Brand colors derived from the logo (constant across themes).
|
||||
static const brandPurple = Color(0xFF9C83F8);
|
||||
static const brandYellow = Color(0xFFFBDC6B);
|
||||
static const brandGrey = Color(0xFFA7A7A7);
|
||||
|
||||
static const light = AppColors._(
|
||||
seedColor: Color(0xFF167C80),
|
||||
privateTabPurple: Color(0xFF8000D7),
|
||||
privateTabBackground: Color(0xFFF3E5F5),
|
||||
privateTabForeground: Color(0xFF4A0072),
|
||||
privateSelectionOverlay: Color(0x648000D7),
|
||||
isolatedTabTeal: Color(0xFF00897B),
|
||||
isolatedTabBackground: Color(0xFFE0F2F1),
|
||||
isolatedTabForeground: Color(0xFF004D40),
|
||||
isolatedSelectionOverlay: Color(0x6400897B),
|
||||
torPurple: Color(0xFF7D4698),
|
||||
torActiveGreen: Color(0xFF68B030),
|
||||
torBackgroundGrey: Color(0xFFECEFF1),
|
||||
warningAmber: Color(0xFFFFA000),
|
||||
// Aura: brand colors lightened toward white
|
||||
auraPurple: Color(0xFFE0D6FC),
|
||||
auraGold: Color(0xFFFDF1D6),
|
||||
auraShadow: Color(0xFFEDEDF0),
|
||||
auraShadowHighlight: Color(0xFFE2E2E7),
|
||||
auraTint: Color(0xFFFFFFFF),
|
||||
brandLink: Color(0xFF7A5AF0),
|
||||
);
|
||||
|
||||
static const dark = AppColors._(
|
||||
seedColor: Color(0xFF167C80),
|
||||
privateTabPurple: Color(0xFF8000D7),
|
||||
privateTabBackground: Color(0xFF25003E),
|
||||
privateTabForeground: Color(0xFFFFFFFF),
|
||||
privateSelectionOverlay: Color(0x648000D7),
|
||||
isolatedTabTeal: Color(0xFF00897B),
|
||||
isolatedTabBackground: Color(0xFF003D36),
|
||||
isolatedTabForeground: Color(0xFFFFFFFF),
|
||||
isolatedSelectionOverlay: Color(0x6400897B),
|
||||
torPurple: Color(0xFF7D4698),
|
||||
torActiveGreen: Color(0xFF68B030),
|
||||
torBackgroundGrey: Color(0xFF333A41),
|
||||
warningAmber: Color(0xFFFFA000),
|
||||
// Aura: brand colors darkened toward black
|
||||
auraPurple: Color(0xFF2C2543),
|
||||
auraGold: Color(0xFF3C3827),
|
||||
auraShadow: Color(0xFF222224),
|
||||
auraShadowHighlight: Color(0xFF2D2D31),
|
||||
auraTint: Color(0xFF000000),
|
||||
brandLink: Color(0xFFFBDC6B),
|
||||
);
|
||||
|
||||
@override
|
||||
AppColors copyWith({
|
||||
Color? seedColor,
|
||||
Color? privateTabPurple,
|
||||
Color? privateTabBackground,
|
||||
Color? privateTabForeground,
|
||||
Color? privateSelectionOverlay,
|
||||
Color? isolatedTabTeal,
|
||||
Color? isolatedTabBackground,
|
||||
Color? isolatedTabForeground,
|
||||
Color? isolatedSelectionOverlay,
|
||||
Color? torPurple,
|
||||
Color? torActiveGreen,
|
||||
Color? torBackgroundGrey,
|
||||
Color? warningAmber,
|
||||
Color? auraPurple,
|
||||
Color? auraGold,
|
||||
Color? auraShadow,
|
||||
Color? auraShadowHighlight,
|
||||
Color? auraTint,
|
||||
Color? brandLink,
|
||||
}) {
|
||||
return AppColors._(
|
||||
seedColor: seedColor ?? this.seedColor,
|
||||
privateTabPurple: privateTabPurple ?? this.privateTabPurple,
|
||||
privateTabBackground: privateTabBackground ?? this.privateTabBackground,
|
||||
privateTabForeground: privateTabForeground ?? this.privateTabForeground,
|
||||
privateSelectionOverlay:
|
||||
privateSelectionOverlay ?? this.privateSelectionOverlay,
|
||||
isolatedTabTeal: isolatedTabTeal ?? this.isolatedTabTeal,
|
||||
isolatedTabBackground:
|
||||
isolatedTabBackground ?? this.isolatedTabBackground,
|
||||
isolatedTabForeground:
|
||||
isolatedTabForeground ?? this.isolatedTabForeground,
|
||||
isolatedSelectionOverlay:
|
||||
isolatedSelectionOverlay ?? this.isolatedSelectionOverlay,
|
||||
torPurple: torPurple ?? this.torPurple,
|
||||
torActiveGreen: torActiveGreen ?? this.torActiveGreen,
|
||||
torBackgroundGrey: torBackgroundGrey ?? this.torBackgroundGrey,
|
||||
warningAmber: warningAmber ?? this.warningAmber,
|
||||
auraPurple: auraPurple ?? this.auraPurple,
|
||||
auraGold: auraGold ?? this.auraGold,
|
||||
auraShadow: auraShadow ?? this.auraShadow,
|
||||
auraShadowHighlight: auraShadowHighlight ?? this.auraShadowHighlight,
|
||||
auraTint: auraTint ?? this.auraTint,
|
||||
brandLink: brandLink ?? this.brandLink,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
AppColors lerp(covariant ThemeExtension<AppColors>? other, double t) {
|
||||
if (other is! AppColors) {
|
||||
return this;
|
||||
}
|
||||
|
||||
return AppColors._(
|
||||
seedColor: Color.lerp(seedColor, other.seedColor, t)!,
|
||||
privateTabPurple: Color.lerp(
|
||||
privateTabPurple,
|
||||
other.privateTabPurple,
|
||||
t,
|
||||
)!,
|
||||
privateTabBackground: Color.lerp(
|
||||
privateTabBackground,
|
||||
other.privateTabBackground,
|
||||
t,
|
||||
)!,
|
||||
privateTabForeground: Color.lerp(
|
||||
privateTabForeground,
|
||||
other.privateTabForeground,
|
||||
t,
|
||||
)!,
|
||||
privateSelectionOverlay: Color.lerp(
|
||||
privateSelectionOverlay,
|
||||
other.privateSelectionOverlay,
|
||||
t,
|
||||
)!,
|
||||
isolatedTabTeal: Color.lerp(isolatedTabTeal, other.isolatedTabTeal, t)!,
|
||||
isolatedTabBackground: Color.lerp(
|
||||
isolatedTabBackground,
|
||||
other.isolatedTabBackground,
|
||||
t,
|
||||
)!,
|
||||
isolatedTabForeground: Color.lerp(
|
||||
isolatedTabForeground,
|
||||
other.isolatedTabForeground,
|
||||
t,
|
||||
)!,
|
||||
isolatedSelectionOverlay: Color.lerp(
|
||||
isolatedSelectionOverlay,
|
||||
other.isolatedSelectionOverlay,
|
||||
t,
|
||||
)!,
|
||||
torPurple: Color.lerp(torPurple, other.torPurple, t)!,
|
||||
torActiveGreen: Color.lerp(torActiveGreen, other.torActiveGreen, t)!,
|
||||
torBackgroundGrey: Color.lerp(
|
||||
torBackgroundGrey,
|
||||
other.torBackgroundGrey,
|
||||
t,
|
||||
)!,
|
||||
warningAmber: Color.lerp(warningAmber, other.warningAmber, t)!,
|
||||
auraPurple: Color.lerp(auraPurple, other.auraPurple, t)!,
|
||||
auraGold: Color.lerp(auraGold, other.auraGold, t)!,
|
||||
auraShadow: Color.lerp(auraShadow, other.auraShadow, t)!,
|
||||
auraShadowHighlight: Color.lerp(
|
||||
auraShadowHighlight,
|
||||
other.auraShadowHighlight,
|
||||
t,
|
||||
)!,
|
||||
auraTint: Color.lerp(auraTint, other.auraTint, t)!,
|
||||
brandLink: Color.lerp(brandLink, other.brandLink, t)!,
|
||||
);
|
||||
}
|
||||
|
||||
/// Get AppColors from the current theme
|
||||
static AppColors of(BuildContext context) {
|
||||
return Theme.of(context).extension<AppColors>() ??
|
||||
switch (Theme.of(context).brightness) {
|
||||
Brightness.dark => dark,
|
||||
Brightness.light => light,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* 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/riverpod.dart';
|
||||
import 'package:weblibre/core/logger.dart';
|
||||
|
||||
final class ErrorObserver extends ProviderObserver {
|
||||
const ErrorObserver();
|
||||
|
||||
@override
|
||||
void providerDidFail(
|
||||
ProviderObserverContext context,
|
||||
Object error,
|
||||
StackTrace stackTrace,
|
||||
) {
|
||||
logger.e('Provider ${context.provider} threw $error at $stackTrace');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,429 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart' as path_provider;
|
||||
import 'package:sqlite3/sqlite3.dart';
|
||||
|
||||
import 'package:uuid/uuid.dart';
|
||||
import 'package:weblibre/core/logger.dart';
|
||||
import 'package:weblibre/domain/entities/profile.dart';
|
||||
import 'package:weblibre/utils/filesystem.dart' as fs;
|
||||
|
||||
final filesystem = _Filesystem();
|
||||
|
||||
class _Filesystem {
|
||||
late final Directory dataDir;
|
||||
late final Directory profilesDir;
|
||||
|
||||
late final UuidValue selectedProfile;
|
||||
late final Directory selectedProfileDir;
|
||||
late final Directory profileDatabasesDir;
|
||||
|
||||
late final String relativeProfilePath;
|
||||
|
||||
Future<List<Directory>> getAvailableProfileDirectories() {
|
||||
return profilesDir.list().transform(fs.profileTransformer).toList();
|
||||
}
|
||||
|
||||
Future<Profile?> readProfileMetadata(Directory profileDir) {
|
||||
return fs.readProfileMetadata(profileDir);
|
||||
}
|
||||
|
||||
Directory getProfileDir(UuidValue uuid) {
|
||||
return fs.getProfileDir(profilesDir, uuid);
|
||||
}
|
||||
|
||||
Future<bool> createNewProfile(Profile profile) {
|
||||
return fs.createNewProfile(profilesDir, profile);
|
||||
}
|
||||
|
||||
Future<void> updateProfileMetadata(Profile profile) {
|
||||
return fs.writeProfileMetadata(getProfileDir(profile.uuidValue), profile);
|
||||
}
|
||||
|
||||
Future<void> setStartupProfile(UuidValue profile) {
|
||||
return fs.writeStartupProfile(profilesDir, profile, flush: true);
|
||||
}
|
||||
|
||||
Future<void> clearMozillaProfileCache(String profileId) {
|
||||
return fs.clearMozillaProfileCache(selectedProfileDir, profileId);
|
||||
}
|
||||
|
||||
List<String> getMozillaProfileIds(UuidValue uuid) {
|
||||
return fs.getMozillaProfileIds(getProfileDir(uuid));
|
||||
}
|
||||
|
||||
/// If the old canonical location `{profileDir}/mozilla/` exists as a real
|
||||
/// directory and the new location `{profileDir}/files/mozilla/` does not,
|
||||
/// rename the former to the latter.
|
||||
/// Returns `true` if a migration was performed.
|
||||
static Future<bool> _migrateMozillaDirToFiles(Directory profileDir) async {
|
||||
final oldDir = Directory(p.join(profileDir.path, 'mozilla'));
|
||||
final newDir = Directory(p.join(profileDir.path, 'files', 'mozilla'));
|
||||
|
||||
final oldType = await FileSystemEntity.type(
|
||||
oldDir.path,
|
||||
followLinks: false,
|
||||
);
|
||||
|
||||
if (oldType == FileSystemEntityType.directory && !await newDir.exists()) {
|
||||
await Directory(p.join(profileDir.path, 'files')).create(recursive: true);
|
||||
await oldDir.rename(newDir.path);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<void> _migrateGeckoCache(Directory profileDir) async {
|
||||
final profileIds = fs.getMozillaProfileIds(profileDir);
|
||||
final globalCacheDir = Directory(p.join(dataDir.path, 'cache'));
|
||||
|
||||
for (final profileId in profileIds) {
|
||||
final oldCache = Directory(p.join(globalCacheDir.path, profileId));
|
||||
final newCache = Directory(p.join(profileDir.path, 'cache', profileId));
|
||||
|
||||
if (await oldCache.exists() && !await newCache.exists()) {
|
||||
try {
|
||||
await oldCache.rename(newCache.path);
|
||||
} catch (e, s) {
|
||||
logger.w(
|
||||
'Failed to migrate Gecko cache for $profileId',
|
||||
error: e,
|
||||
stackTrace: s,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Ensure the top-level `{filesDir}/mozilla` symlink points to the given
|
||||
/// profile's `files/mozilla/` directory. Old versions created this symlink
|
||||
/// targeting `{profile}/mozilla` which no longer exists after the migration
|
||||
/// moved it to `{profile}/files/mozilla`. GeckoView's `extensions.json`
|
||||
/// stores absolute paths through this symlink, so it must stay valid.
|
||||
static Future<void> _linkMozillaDir(
|
||||
Directory filesDir,
|
||||
Directory profileDir,
|
||||
) async {
|
||||
final mozillaDir = Directory(p.join(profileDir.path, 'files', 'mozilla'));
|
||||
await mozillaDir.create(recursive: true);
|
||||
|
||||
final mozillaPath = p.join(filesDir.path, 'mozilla');
|
||||
|
||||
final currentType = await FileSystemEntity.type(
|
||||
mozillaPath,
|
||||
followLinks: false,
|
||||
);
|
||||
|
||||
switch (currentType) {
|
||||
case FileSystemEntityType.notFound:
|
||||
break;
|
||||
case FileSystemEntityType.link:
|
||||
final link = Link(mozillaPath);
|
||||
try {
|
||||
if (await link.target() == mozillaDir.path) {
|
||||
return;
|
||||
}
|
||||
} on FileSystemException {
|
||||
// Replace unreadable or broken links.
|
||||
}
|
||||
await link.delete();
|
||||
default:
|
||||
// Move aside any non-link entity (directory, file, etc.)
|
||||
final backupPath = p.join(
|
||||
filesDir.path,
|
||||
'mozilla.backup.${DateTime.now().millisecondsSinceEpoch}',
|
||||
);
|
||||
if (currentType == FileSystemEntityType.directory) {
|
||||
await Directory(mozillaPath).rename(backupPath);
|
||||
} else {
|
||||
await File(mozillaPath).rename(backupPath);
|
||||
}
|
||||
}
|
||||
|
||||
await Link(mozillaPath).create(mozillaDir.path);
|
||||
}
|
||||
|
||||
/// Remove path-sensitive Gecko caches that may contain stale absolute paths.
|
||||
/// Gecko regenerates these on next startup.
|
||||
static Future<void> _healGeckoStartupCaches(Directory profileDir) async {
|
||||
final profileIds = fs.getMozillaProfileIds(profileDir);
|
||||
|
||||
for (final profileId in profileIds) {
|
||||
final mozProfileDir = Directory(
|
||||
p.join(profileDir.path, 'files', 'mozilla', profileId),
|
||||
);
|
||||
if (!await mozProfileDir.exists()) continue;
|
||||
|
||||
// addonStartup.json.lz4 caches absolute addon paths
|
||||
final addonStartup = File(
|
||||
p.join(mozProfileDir.path, 'addonStartup.json.lz4'),
|
||||
);
|
||||
if (await addonStartup.exists()) {
|
||||
try {
|
||||
await addonStartup.delete();
|
||||
logger.i('Cleared addonStartup.json.lz4 for $profileId');
|
||||
} catch (e, s) {
|
||||
logger.w(
|
||||
'Failed to clear addonStartup.json.lz4 for $profileId',
|
||||
error: e,
|
||||
stackTrace: s,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// startupCache may contain stale path references
|
||||
final startupCache = Directory(
|
||||
p.join(mozProfileDir.path, 'startupCache'),
|
||||
);
|
||||
if (await startupCache.exists()) {
|
||||
try {
|
||||
await startupCache.delete(recursive: true);
|
||||
logger.i('Cleared startupCache for $profileId');
|
||||
} catch (e, s) {
|
||||
logger.w(
|
||||
'Failed to clear startupCache for $profileId',
|
||||
error: e,
|
||||
stackTrace: s,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Rewrite absolute paths in `extensions.json` that still reference the
|
||||
/// old pre-migration layout (`{filesDir}/mozilla/…`). After migration the
|
||||
/// real location is `{filesDir}/weblibre_profiles/profile-…/files/mozilla/…`
|
||||
/// and the top-level symlink bridges the two, but fixing the stored paths
|
||||
/// removes the permanent dependency on the symlink.
|
||||
/// Returns `true` if any paths were rewritten.
|
||||
static Future<bool> _migrateExtensionPaths(
|
||||
Directory filesDir,
|
||||
Directory profileDir,
|
||||
) async {
|
||||
final profileIds = fs.getMozillaProfileIds(profileDir);
|
||||
// Pattern: /data/user/0/eu.weblibre.gecko/files/mozilla/
|
||||
// Should become: /data/user/0/eu.weblibre.gecko/files/weblibre_profiles/profile-…/files/mozilla/
|
||||
final oldPrefix = '${filesDir.path}/mozilla/';
|
||||
final newPrefix = '${profileDir.path}/files/mozilla/';
|
||||
|
||||
if (oldPrefix == newPrefix) return false;
|
||||
|
||||
var migrated = false;
|
||||
|
||||
for (final profileId in profileIds) {
|
||||
final extensionsFile = File(
|
||||
p.join(
|
||||
profileDir.path,
|
||||
'files',
|
||||
'mozilla',
|
||||
profileId,
|
||||
'extensions.json',
|
||||
),
|
||||
);
|
||||
|
||||
if (!await extensionsFile.exists()) continue;
|
||||
|
||||
try {
|
||||
final content = await extensionsFile.readAsString();
|
||||
if (!content.contains(oldPrefix)) continue;
|
||||
|
||||
final json = jsonDecode(content);
|
||||
var changed = false;
|
||||
|
||||
if (json is Map<String, dynamic> && json['addons'] is List) {
|
||||
for (final addon in json['addons'] as List) {
|
||||
if (addon is! Map<String, dynamic>) continue;
|
||||
|
||||
final path = addon['path'];
|
||||
if (path is String && path.startsWith(oldPrefix)) {
|
||||
addon['path'] = path.replaceFirst(oldPrefix, newPrefix);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
final rootURI = addon['rootURI'];
|
||||
if (rootURI is String && rootURI.contains(oldPrefix)) {
|
||||
addon['rootURI'] = rootURI.replaceFirst(oldPrefix, newPrefix);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
await extensionsFile.writeAsString(jsonEncode(json), flush: true);
|
||||
logger.i('Migrated extension paths in $profileId/extensions.json');
|
||||
migrated = true;
|
||||
}
|
||||
} catch (e, s) {
|
||||
logger.w(
|
||||
'Failed to migrate extension paths for $profileId',
|
||||
error: e,
|
||||
stackTrace: s,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return migrated;
|
||||
}
|
||||
|
||||
/// Run all post-migration healing for a profile directory.
|
||||
/// Called during init for the active profile and after backup restore.
|
||||
Future<void> healProfile(Directory profileDir) async {
|
||||
final filesDir = profilesDir.parent;
|
||||
final dirMigrated = await _migrateMozillaDirToFiles(profileDir);
|
||||
await _migrateGeckoCache(profileDir);
|
||||
final pathsMigrated = await _migrateExtensionPaths(filesDir, profileDir);
|
||||
|
||||
// Only update the global symlink for the currently active profile —
|
||||
// restoring a non-active profile must not repoint it.
|
||||
final isActiveProfile = profileDir.path == selectedProfileDir.path;
|
||||
if (isActiveProfile) {
|
||||
await _linkMozillaDir(filesDir, profileDir);
|
||||
}
|
||||
|
||||
// Only clear Gecko startup caches when a path migration actually happened,
|
||||
// to avoid a recurring startup performance penalty.
|
||||
if (dirMigrated || pathsMigrated) {
|
||||
await _healGeckoStartupCaches(profileDir);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _setupSqliteCache() async {
|
||||
// Make sqlite3 pick a more suitable location for temporary files - the
|
||||
// one from the system may be inaccessible due to sandboxing.
|
||||
final cachebase = (await path_provider.getTemporaryDirectory()).path;
|
||||
// We can't access /tmp on Android, which sqlite3 would try by default.
|
||||
// Explicitly tell it about the correct temporary directory.
|
||||
sqlite3.tempDirectory = cachebase;
|
||||
}
|
||||
|
||||
Future<void> _copyDirectory(
|
||||
Directory source,
|
||||
Directory destination,
|
||||
bool Function(FileSystemEntity e) filter,
|
||||
) async {
|
||||
// Create destination directory
|
||||
await destination.create(recursive: true);
|
||||
|
||||
// List all contents
|
||||
await for (final entity in source.list().where(filter)) {
|
||||
final newPath = p.join(destination.path, p.basename(entity.path));
|
||||
|
||||
if (entity is Directory) {
|
||||
// Recursively copy subdirectory
|
||||
await _copyDirectory(entity, Directory(newPath), filter);
|
||||
} else if (entity is File) {
|
||||
// Copy file
|
||||
await entity.copy(newPath);
|
||||
} else if (entity is Link) {
|
||||
// Copy link
|
||||
await Link(newPath).create(await entity.target());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> init() async {
|
||||
final filesDir = await path_provider.getApplicationSupportDirectory();
|
||||
|
||||
dataDir = filesDir.parent;
|
||||
|
||||
profilesDir = Directory(p.join(filesDir.path, fs.profilesDirName));
|
||||
await profilesDir.create();
|
||||
|
||||
final selectedProfile = await fs.selectStartupProfile(profilesDir);
|
||||
if (selectedProfile == null) {
|
||||
final defaultProfile = Profile.create(name: 'Default');
|
||||
if (!await fs.createNewProfile(profilesDir, defaultProfile)) {
|
||||
throw Exception('Unable to create default profile');
|
||||
}
|
||||
|
||||
this.selectedProfile = defaultProfile.uuidValue;
|
||||
await fs.writeStartupProfile(profilesDir, defaultProfile.uuidValue);
|
||||
|
||||
final mozillaDir = Directory(p.join(filesDir.path, 'mozilla'));
|
||||
if (await mozillaDir.exists()) {
|
||||
final type = await FileSystemEntity.type(mozillaDir.path);
|
||||
if (type != FileSystemEntityType.link) {
|
||||
await _migrate(defaultProfile, mozillaDir, filesDir);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.selectedProfile = selectedProfile;
|
||||
}
|
||||
|
||||
relativeProfilePath = p.join(
|
||||
fs.profilesDirName,
|
||||
'${fs.profileDirPrefix}${this.selectedProfile.uuid}',
|
||||
);
|
||||
selectedProfileDir = Directory(p.join(filesDir.path, relativeProfilePath));
|
||||
await selectedProfileDir.create();
|
||||
|
||||
profileDatabasesDir = Directory(
|
||||
p.join(selectedProfileDir.path, 'databases'),
|
||||
);
|
||||
await profileDatabasesDir.create();
|
||||
|
||||
await healProfile(selectedProfileDir);
|
||||
await _setupSqliteCache();
|
||||
}
|
||||
|
||||
Future<void> _migrate(
|
||||
Profile defaultProfile,
|
||||
Directory mozillaDir,
|
||||
Directory filesDir,
|
||||
) async {
|
||||
final profileDir = getProfileDir(defaultProfile.uuidValue);
|
||||
|
||||
final newMozillaDir = Directory(
|
||||
p.join(profileDir.path, 'files', 'mozilla'),
|
||||
);
|
||||
await newMozillaDir.create(recursive: true);
|
||||
await mozillaDir.rename(newMozillaDir.path);
|
||||
|
||||
await _copyDirectory(
|
||||
filesDir,
|
||||
Directory(p.join(profileDir.path, 'files')),
|
||||
(e) => e is! Directory || p.basename(e.path) != fs.profilesDirName,
|
||||
);
|
||||
|
||||
final profileDatabasesDir = Directory(p.join(profileDir.path, 'databases'));
|
||||
|
||||
await _copyDirectory(
|
||||
Directory(p.join(dataDir.path, 'databases')),
|
||||
profileDatabasesDir,
|
||||
(e) => true,
|
||||
);
|
||||
|
||||
final dbFolder = await path_provider.getApplicationDocumentsDirectory();
|
||||
|
||||
final bangDb = File(p.join(dbFolder.path, 'bang3.db'));
|
||||
await bangDb.copy(p.join(profileDatabasesDir.path, 'bang.db'));
|
||||
final feedDb = File(p.join(dbFolder.path, 'feed.db'));
|
||||
await feedDb.copy(p.join(profileDatabasesDir.path, 'feed.db'));
|
||||
final tabDb = File(p.join(dbFolder.path, 'tab2.db'));
|
||||
await tabDb.copy(p.join(profileDatabasesDir.path, 'tab.db'));
|
||||
final userDb = File(p.join(dbFolder.path, 'user.db'));
|
||||
await userDb.copy(p.join(profileDatabasesDir.path, 'user.db'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* 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:exceptions/exceptions.dart';
|
||||
import 'package:http/http.dart';
|
||||
|
||||
ErrorMessage handleHttpError(Exception exception, StackTrace stackTrace) {
|
||||
return switch (exception) {
|
||||
SocketException() => const ErrorMessage(
|
||||
source: 'http',
|
||||
message: 'Could not contact remote service',
|
||||
),
|
||||
HttpException() => const ErrorMessage(
|
||||
source: 'http',
|
||||
message: 'Web request returned error',
|
||||
),
|
||||
FormatException() => const ErrorMessage(
|
||||
source: 'http',
|
||||
message: 'Bad response format',
|
||||
),
|
||||
ClientException() => const ErrorMessage(
|
||||
source: 'http',
|
||||
message: 'Could not contact remote service',
|
||||
),
|
||||
_ => ErrorMessage.fromException(exception, stackTrace),
|
||||
};
|
||||
}
|
||||
@@ -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:logger/logger.dart';
|
||||
|
||||
class _DebugLogFilter extends LogFilter {
|
||||
@override
|
||||
bool shouldLog(LogEvent event) {
|
||||
//Log all events
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
final loggerMemory = MemoryOutput(
|
||||
bufferSize: 255,
|
||||
secondOutput: ConsoleOutput(),
|
||||
);
|
||||
final logger = Logger(
|
||||
filter: _DebugLogFilter(),
|
||||
printer: PrettyPrinter(dateTimeFormat: DateTimeFormat.onlyTimeAndSinceStart),
|
||||
output: loggerMemory,
|
||||
);
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'app_state.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class AppStateKey extends _$AppStateKey {
|
||||
void reset() {
|
||||
state = GlobalKey(debugLabel: 'RootKey');
|
||||
}
|
||||
|
||||
@override
|
||||
GlobalKey build() {
|
||||
return GlobalKey(debugLabel: 'RootKey');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'app_state.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(AppStateKey)
|
||||
final appStateKeyProvider = AppStateKeyProvider._();
|
||||
|
||||
final class AppStateKeyProvider
|
||||
extends $NotifierProvider<AppStateKey, GlobalKey<State<StatefulWidget>>> {
|
||||
AppStateKeyProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'appStateKeyProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$appStateKeyHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
AppStateKey create() => AppStateKey();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(GlobalKey<State<StatefulWidget>> value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<GlobalKey<State<StatefulWidget>>>(
|
||||
value,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$appStateKeyHash() => r'113c208139d84cdd52f623c2c6298b430fc1a889';
|
||||
|
||||
abstract class _$AppStateKey
|
||||
extends $Notifier<GlobalKey<State<StatefulWidget>>> {
|
||||
GlobalKey<State<StatefulWidget>> build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final ref =
|
||||
this.ref
|
||||
as $Ref<
|
||||
GlobalKey<State<StatefulWidget>>,
|
||||
GlobalKey<State<StatefulWidget>>
|
||||
>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<
|
||||
GlobalKey<State<StatefulWidget>>,
|
||||
GlobalKey<State<StatefulWidget>>
|
||||
>,
|
||||
GlobalKey<State<StatefulWidget>>,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleCreate(ref, build);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:weblibre/core/design/app_colors.dart';
|
||||
|
||||
part 'defaults.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
Color lightSeedColorFallback(Ref ref) => AppColors.light.seedColor;
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
Color darkSeedColorFallback(Ref ref) => AppColors.dark.seedColor;
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
Uri docsUri(Ref ref) => Uri.parse('https://docs.weblibre.eu/');
|
||||
@@ -0,0 +1,134 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'defaults.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(lightSeedColorFallback)
|
||||
final lightSeedColorFallbackProvider = LightSeedColorFallbackProvider._();
|
||||
|
||||
final class LightSeedColorFallbackProvider
|
||||
extends $FunctionalProvider<Color, Color, Color>
|
||||
with $Provider<Color> {
|
||||
LightSeedColorFallbackProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'lightSeedColorFallbackProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$lightSeedColorFallbackHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<Color> $createElement($ProviderPointer pointer) =>
|
||||
$ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
Color create(Ref ref) {
|
||||
return lightSeedColorFallback(ref);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(Color value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<Color>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$lightSeedColorFallbackHash() =>
|
||||
r'851efdcb11e4367ea2e54f1884a73fd4cd841d4a';
|
||||
|
||||
@ProviderFor(darkSeedColorFallback)
|
||||
final darkSeedColorFallbackProvider = DarkSeedColorFallbackProvider._();
|
||||
|
||||
final class DarkSeedColorFallbackProvider
|
||||
extends $FunctionalProvider<Color, Color, Color>
|
||||
with $Provider<Color> {
|
||||
DarkSeedColorFallbackProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'darkSeedColorFallbackProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$darkSeedColorFallbackHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<Color> $createElement($ProviderPointer pointer) =>
|
||||
$ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
Color create(Ref ref) {
|
||||
return darkSeedColorFallback(ref);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(Color value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<Color>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$darkSeedColorFallbackHash() =>
|
||||
r'161a8c4318108c31d5c300441bc3332d08c24496';
|
||||
|
||||
@ProviderFor(docsUri)
|
||||
final docsUriProvider = DocsUriProvider._();
|
||||
|
||||
final class DocsUriProvider extends $FunctionalProvider<Uri, Uri, Uri>
|
||||
with $Provider<Uri> {
|
||||
DocsUriProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'docsUriProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$docsUriHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<Uri> $createElement($ProviderPointer pointer) =>
|
||||
$ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
Uri create(Ref ref) {
|
||||
return docsUri(ref);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(Uri value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<Uri>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$docsUriHash() => r'6456efcf97ddc7ee87a67e2d3380f7241e3d76ea';
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'device_info.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class AndroidDeviceInfo extends _$AndroidDeviceInfo {
|
||||
@override
|
||||
Future<AndroidDeviceInfoData?> build() async {
|
||||
if (!Platform.isAndroid) return null;
|
||||
|
||||
final deviceInfo = DeviceInfoPlugin();
|
||||
final androidInfo = await deviceInfo.androidInfo;
|
||||
|
||||
return AndroidDeviceInfoData(sdkInt: androidInfo.version.sdkInt);
|
||||
}
|
||||
}
|
||||
|
||||
class AndroidDeviceInfoData {
|
||||
final int sdkInt;
|
||||
|
||||
const AndroidDeviceInfoData({required this.sdkInt});
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'device_info.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(AndroidDeviceInfo)
|
||||
final androidDeviceInfoProvider = AndroidDeviceInfoProvider._();
|
||||
|
||||
final class AndroidDeviceInfoProvider
|
||||
extends $AsyncNotifierProvider<AndroidDeviceInfo, AndroidDeviceInfoData?> {
|
||||
AndroidDeviceInfoProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'androidDeviceInfoProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$androidDeviceInfoHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
AndroidDeviceInfo create() => AndroidDeviceInfo();
|
||||
}
|
||||
|
||||
String _$androidDeviceInfoHash() => r'05c6cc63a6ee34f137aef538d65e8ab94b9cca89';
|
||||
|
||||
abstract class _$AndroidDeviceInfo
|
||||
extends $AsyncNotifier<AndroidDeviceInfoData?> {
|
||||
FutureOr<AndroidDeviceInfoData?> build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final ref =
|
||||
this.ref
|
||||
as $Ref<AsyncValue<AndroidDeviceInfoData?>, AndroidDeviceInfoData?>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<
|
||||
AsyncValue<AndroidDeviceInfoData?>,
|
||||
AndroidDeviceInfoData?
|
||||
>,
|
||||
AsyncValue<AndroidDeviceInfoData?>,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleCreate(ref, build);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* 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:intl/date_symbol_data_local.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'format.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class Format extends _$Format {
|
||||
String fullDateTime(DateTime date) {
|
||||
final pattern = DateFormat('yMMMMd').addPattern('Hm');
|
||||
|
||||
return pattern.format(date);
|
||||
}
|
||||
|
||||
String shortDate(DateTime date) {
|
||||
return DateFormat('yyyy-MM-dd').format(date);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> build() async {
|
||||
await initializeDateFormatting();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'format.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(Format)
|
||||
final formatProvider = FormatProvider._();
|
||||
|
||||
final class FormatProvider extends $AsyncNotifierProvider<Format, void> {
|
||||
FormatProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'formatProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$formatHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
Format create() => Format();
|
||||
}
|
||||
|
||||
String _$formatHash() => r'695b94cc4b25596e4ef953d08ba954424a5ec33e';
|
||||
|
||||
abstract class _$Format extends $AsyncNotifier<void> {
|
||||
FutureOr<void> build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final ref = this.ref as $Ref<AsyncValue<void>, void>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<AsyncValue<void>, void>,
|
||||
AsyncValue<void>,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleCreate(ref, build);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* 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/data/models/drag_data.dart';
|
||||
|
||||
part 'global_drop.g.dart';
|
||||
|
||||
@Riverpod()
|
||||
class WillAcceptDrop extends _$WillAcceptDrop {
|
||||
// ignore: use_setters_to_change_properties api decision
|
||||
void setData(DropTargetData data) {
|
||||
state = data;
|
||||
}
|
||||
|
||||
void clear() {
|
||||
state = null;
|
||||
}
|
||||
|
||||
@override
|
||||
DropTargetData? build() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'global_drop.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(WillAcceptDrop)
|
||||
final willAcceptDropProvider = WillAcceptDropProvider._();
|
||||
|
||||
final class WillAcceptDropProvider
|
||||
extends $NotifierProvider<WillAcceptDrop, DropTargetData?> {
|
||||
WillAcceptDropProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'willAcceptDropProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$willAcceptDropHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
WillAcceptDrop create() => WillAcceptDrop();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(DropTargetData? value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<DropTargetData?>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$willAcceptDropHash() => r'97b47784ef9101757602b4408d1f545ef2308830';
|
||||
|
||||
abstract class _$WillAcceptDrop extends $Notifier<DropTargetData?> {
|
||||
DropTargetData? build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final ref = this.ref as $Ref<DropTargetData?, DropTargetData?>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<DropTargetData?, DropTargetData?>,
|
||||
DropTargetData?,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleCreate(ref, build);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* 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:convert';
|
||||
|
||||
import 'package:riverpod/experimental/persist.dart';
|
||||
import 'package:riverpod_annotation/experimental/persist.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:weblibre/features/user/data/providers.dart';
|
||||
|
||||
part 'persisted_bool.g.dart';
|
||||
|
||||
enum PersistedBoolKey {
|
||||
extensionsExpanded(key: 'ExtensionsExpanded', defaultValue: false),
|
||||
searchSuggestionsExpanded(
|
||||
key: 'SearchSuggestionsExpanded',
|
||||
defaultValue: true,
|
||||
),
|
||||
tabSuggestions(key: 'TabSuggestions', defaultValue: false);
|
||||
|
||||
const PersistedBoolKey({required this.key, required this.defaultValue});
|
||||
|
||||
final String key;
|
||||
final bool defaultValue;
|
||||
}
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class PersistedBool extends _$PersistedBool {
|
||||
void toggle() => state = !state;
|
||||
|
||||
// ignore: use_setters_to_change_properties
|
||||
void set(bool value) => state = value;
|
||||
|
||||
@override
|
||||
bool build(PersistedBoolKey key) {
|
||||
persist(
|
||||
ref.watch(riverpodDatabaseStorageProvider),
|
||||
key: key.key,
|
||||
encode: (state) => jsonEncode([state]),
|
||||
decode: (encoded) => (jsonDecode(encoded) as List<dynamic>).first as bool,
|
||||
);
|
||||
|
||||
return stateOrNull ?? key.defaultValue;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'persisted_bool.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(PersistedBool)
|
||||
final persistedBoolProvider = PersistedBoolFamily._();
|
||||
|
||||
final class PersistedBoolProvider
|
||||
extends $NotifierProvider<PersistedBool, bool> {
|
||||
PersistedBoolProvider._({
|
||||
required PersistedBoolFamily super.from,
|
||||
required PersistedBoolKey super.argument,
|
||||
}) : super(
|
||||
retry: null,
|
||||
name: r'persistedBoolProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$persistedBoolHash();
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return r'persistedBoolProvider'
|
||||
''
|
||||
'($argument)';
|
||||
}
|
||||
|
||||
@$internal
|
||||
@override
|
||||
PersistedBool create() => PersistedBool();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(bool value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<bool>(value),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is PersistedBoolProvider && other.argument == argument;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return argument.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
String _$persistedBoolHash() => r'97fd7ed82dfde444fddf4475a3982be00f5f197a';
|
||||
|
||||
final class PersistedBoolFamily extends $Family
|
||||
with
|
||||
$ClassFamilyOverride<
|
||||
PersistedBool,
|
||||
bool,
|
||||
bool,
|
||||
bool,
|
||||
PersistedBoolKey
|
||||
> {
|
||||
PersistedBoolFamily._()
|
||||
: super(
|
||||
retry: null,
|
||||
name: r'persistedBoolProvider',
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
isAutoDispose: false,
|
||||
);
|
||||
|
||||
PersistedBoolProvider call(PersistedBoolKey key) =>
|
||||
PersistedBoolProvider._(argument: key, from: this);
|
||||
|
||||
@override
|
||||
String toString() => r'persistedBoolProvider';
|
||||
}
|
||||
|
||||
abstract class _$PersistedBool extends $Notifier<bool> {
|
||||
late final _$args = ref.$arg as PersistedBoolKey;
|
||||
PersistedBoolKey get key => _$args;
|
||||
|
||||
bool build(PersistedBoolKey key);
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final ref = this.ref as $Ref<bool, bool>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<bool, bool>,
|
||||
bool,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleCreate(ref, () => build(_$args));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:weblibre/core/providers/app_state.dart';
|
||||
import 'package:weblibre/core/routing/routes.dart';
|
||||
import 'package:weblibre/features/user/domain/providers/profile_auth.dart';
|
||||
import 'package:weblibre/features/user/domain/repositories/onboarding.dart';
|
||||
|
||||
part 'router.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
Future<GoRouter> router(Ref ref) async {
|
||||
ref.watch(appStateKeyProvider); //Rebuild router on key changes
|
||||
|
||||
final onboardingRepository = ref.read(onboardingRepositoryProvider.notifier);
|
||||
unawaited(ref.read(profileAuthStateProvider.notifier).bootstrapFromProfile());
|
||||
|
||||
String? initialLocation;
|
||||
|
||||
final onboardingMandatory = await onboardingRepository.isOutdated();
|
||||
|
||||
if (onboardingMandatory) {
|
||||
final current = await onboardingRepository.getCurrentRevision();
|
||||
|
||||
final route = OnboardingRoute(
|
||||
currentRevision: current ?? -1,
|
||||
targetRevision: OnboardingRepository.targetRevision,
|
||||
);
|
||||
|
||||
initialLocation = route.location;
|
||||
}
|
||||
|
||||
final profileAuthRefreshListenable = ref.watch(profileAuthProvider);
|
||||
|
||||
return GoRouter(
|
||||
debugLogDiagnostics: true,
|
||||
routes: $appRoutes,
|
||||
initialLocation: initialLocation ?? const LockRoute().location,
|
||||
refreshListenable: profileAuthRefreshListenable,
|
||||
redirect: (context, state) {
|
||||
final authenticated = ref.read(profileAuthStateProvider);
|
||||
final currentTopRouteName = state.topRoute?.name;
|
||||
final isOnLockRoute = currentTopRouteName == LockRoute.name;
|
||||
final isOnOnboarding = currentTopRouteName == OnboardingRoute.name;
|
||||
|
||||
// Don't redirect during onboarding
|
||||
if (isOnOnboarding) return null;
|
||||
|
||||
if (!authenticated && !isOnLockRoute) {
|
||||
return const LockRoute().location;
|
||||
}
|
||||
|
||||
if (authenticated && isOnLockRoute) {
|
||||
return const BrowserRoute().location;
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class CurrentTopRoute extends _$CurrentTopRoute {
|
||||
@override
|
||||
RouteBase? build() {
|
||||
final router = ref.watch(routerProvider).value;
|
||||
if (router == null) return null;
|
||||
|
||||
GoRoute? getCurrentRoute() {
|
||||
final config = router.routerDelegate.currentConfiguration;
|
||||
|
||||
if (config.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final match = config.last;
|
||||
return match.route;
|
||||
}
|
||||
|
||||
void update() {
|
||||
unawaited(
|
||||
Future(() {
|
||||
state = getCurrentRoute();
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
router.routerDelegate.addListener(update);
|
||||
ref.onDispose(() => router.routerDelegate.removeListener(update));
|
||||
|
||||
return getCurrentRoute();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'router.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(router)
|
||||
final routerProvider = RouterProvider._();
|
||||
|
||||
final class RouterProvider
|
||||
extends
|
||||
$FunctionalProvider<AsyncValue<GoRouter>, GoRouter, FutureOr<GoRouter>>
|
||||
with $FutureModifier<GoRouter>, $FutureProvider<GoRouter> {
|
||||
RouterProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'routerProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$routerHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$FutureProviderElement<GoRouter> $createElement($ProviderPointer pointer) =>
|
||||
$FutureProviderElement(pointer);
|
||||
|
||||
@override
|
||||
FutureOr<GoRouter> create(Ref ref) {
|
||||
return router(ref);
|
||||
}
|
||||
}
|
||||
|
||||
String _$routerHash() => r'4402ca2d7061945c395f3963d6bde29be8999f96';
|
||||
|
||||
@ProviderFor(CurrentTopRoute)
|
||||
final currentTopRouteProvider = CurrentTopRouteProvider._();
|
||||
|
||||
final class CurrentTopRouteProvider
|
||||
extends $NotifierProvider<CurrentTopRoute, RouteBase?> {
|
||||
CurrentTopRouteProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'currentTopRouteProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$currentTopRouteHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
CurrentTopRoute create() => CurrentTopRoute();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(RouteBase? value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<RouteBase?>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$currentTopRouteHash() => r'bf9956935ede399b815348926bb75c6bad22619e';
|
||||
|
||||
abstract class _$CurrentTopRoute extends $Notifier<RouteBase?> {
|
||||
RouteBase? build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final ref = this.ref as $Ref<RouteBase?, RouteBase?>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<RouteBase?, RouteBase?>,
|
||||
RouteBase?,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleCreate(ref, build);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
part of 'routes.dart';
|
||||
|
||||
@TypedGoRoute<BangMenuRoute>(
|
||||
name: 'BangRoute',
|
||||
path: '/bangs',
|
||||
routes: [
|
||||
TypedGoRoute<UserBangsRoute>(
|
||||
name: 'UserBangsRoute',
|
||||
path: 'user',
|
||||
routes: [
|
||||
TypedGoRoute<NewUserBangRoute>(name: 'NewUserBangRoute', path: 'new'),
|
||||
TypedGoRoute<EditUserBangRoute>(
|
||||
name: 'EditUserBangRoute',
|
||||
path: 'edit',
|
||||
),
|
||||
],
|
||||
),
|
||||
TypedGoRoute<BangSearchRoute>(
|
||||
name: 'BangSearchRoute',
|
||||
path: 'search/:searchText',
|
||||
),
|
||||
TypedGoRoute<BangCategoriesRoute>(
|
||||
name: 'BangCategoriesRoute',
|
||||
path: 'categories',
|
||||
routes: [
|
||||
TypedGoRoute<BangCategoryRoute>(
|
||||
name: 'BangCategoryRoute',
|
||||
path: 'category/:category',
|
||||
routes: [
|
||||
TypedGoRoute<BangSubCategoryRoute>(
|
||||
name: 'BangSubCategoryRoute',
|
||||
path: ':subCategory',
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
class BangMenuRoute extends GoRouteData with $BangMenuRoute {
|
||||
const BangMenuRoute();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const BangMenuScreen();
|
||||
}
|
||||
}
|
||||
|
||||
class BangCategoriesRoute extends GoRouteData with $BangCategoriesRoute {
|
||||
const BangCategoriesRoute();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const BangCategoriesScreen();
|
||||
}
|
||||
}
|
||||
|
||||
class BangCategoryRoute extends GoRouteData with $BangCategoryRoute {
|
||||
final String category;
|
||||
|
||||
const BangCategoryRoute({required this.category});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return BangCategoryScreen(category: category);
|
||||
}
|
||||
}
|
||||
|
||||
class BangSubCategoryRoute extends GoRouteData with $BangSubCategoryRoute {
|
||||
final String category;
|
||||
final String subCategory;
|
||||
|
||||
const BangSubCategoryRoute({
|
||||
required this.category,
|
||||
required this.subCategory,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return BangCategoryScreen(category: category, subCategory: subCategory);
|
||||
}
|
||||
}
|
||||
|
||||
class BangSearchRoute extends GoRouteData with $BangSearchRoute {
|
||||
static const String emptySearchText = ' ';
|
||||
|
||||
//This should be nullable but isnt allowed by go_router
|
||||
final String searchText;
|
||||
|
||||
const BangSearchRoute({this.searchText = BangSearchRoute.emptySearchText});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return BangSearchScreen(
|
||||
initialSearchText: (searchText.isEmpty || searchText == emptySearchText)
|
||||
? null
|
||||
: searchText,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class UserBangsRoute extends GoRouteData with $UserBangsRoute {
|
||||
const UserBangsRoute();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const UserBangs();
|
||||
}
|
||||
}
|
||||
|
||||
class NewUserBangRoute extends GoRouteData with $NewUserBangRoute {
|
||||
const NewUserBangRoute();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const EditBangScreen(initialBang: null);
|
||||
}
|
||||
}
|
||||
|
||||
class EditUserBangRoute extends GoRouteData with $EditUserBangRoute {
|
||||
final String initialBang;
|
||||
|
||||
const EditUserBangRoute({required this.initialBang});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return EditBangScreen(
|
||||
initialBang: Bang.fromJson(
|
||||
jsonDecode(initialBang) as Map<String, dynamic>,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
part of 'routes.dart';
|
||||
|
||||
@TypedGoRoute<BookmarksRoute>(
|
||||
name: 'BookmarksRoute',
|
||||
path: '/bookmarks',
|
||||
routes: [
|
||||
TypedGoRoute<BookmarkListRoute>(
|
||||
name: 'BookmarkListRoute',
|
||||
path: 'list/:entryGuid',
|
||||
),
|
||||
TypedGoRoute<BookmarkFolderAddRoute>(
|
||||
name: 'BookmarkFolderAddRoute',
|
||||
path: 'createFolder',
|
||||
),
|
||||
TypedGoRoute<BookmarkFolderEditRoute>(
|
||||
name: 'BookmarkFolderEditRoute',
|
||||
path: 'editFolder',
|
||||
),
|
||||
TypedGoRoute<BookmarkEntryAddRoute>(
|
||||
name: 'BookmarkEntryAddRoute',
|
||||
path: 'createEntry',
|
||||
),
|
||||
TypedGoRoute<BookmarkEntryEditRoute>(
|
||||
name: 'BookmarkEntryEditRoute',
|
||||
path: 'editEntry',
|
||||
),
|
||||
],
|
||||
)
|
||||
class BookmarksRoute extends GoRouteData with $BookmarksRoute {
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
}
|
||||
|
||||
class BookmarkListRoute extends GoRouteData with $BookmarkListRoute {
|
||||
final String entryGuid;
|
||||
|
||||
const BookmarkListRoute({required this.entryGuid});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return BookmarkListScreen(entryGuid: entryGuid);
|
||||
}
|
||||
}
|
||||
|
||||
class BookmarkFolderAddRoute extends GoRouteData with $BookmarkFolderAddRoute {
|
||||
final String? parentGuid;
|
||||
|
||||
const BookmarkFolderAddRoute({required this.parentGuid});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return BookmarkFolderEditScreen(parentGuid: parentGuid, folder: null);
|
||||
}
|
||||
}
|
||||
|
||||
class BookmarkFolderEditRoute extends GoRouteData
|
||||
with $BookmarkFolderEditRoute {
|
||||
final String folder;
|
||||
|
||||
const BookmarkFolderEditRoute({required this.folder});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return BookmarkFolderEditScreen(
|
||||
folder: BookmarkFolder.fromJson(
|
||||
jsonDecode(folder) as Map<String, dynamic>,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class BookmarkEntryAddRoute extends GoRouteData with $BookmarkEntryAddRoute {
|
||||
final String bookmarkInfo;
|
||||
|
||||
const BookmarkEntryAddRoute({required this.bookmarkInfo});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return BookmarkEntryEditScreen(
|
||||
initialInfo: BookmarkInfo.decode(jsonDecode(bookmarkInfo) as Object),
|
||||
exisitingEntry: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class BookmarkEntryEditRoute extends GoRouteData with $BookmarkEntryEditRoute {
|
||||
final String bookmarkEntry;
|
||||
|
||||
const BookmarkEntryEditRoute({required this.bookmarkEntry});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return BookmarkEntryEditScreen(
|
||||
initialInfo: null,
|
||||
exisitingEntry: BookmarkEntry.fromJson(
|
||||
jsonDecode(bookmarkEntry) as Map<String, dynamic>,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
part of 'routes.dart';
|
||||
|
||||
@TypedGoRoute<BrowserRoute>(
|
||||
name: BrowserRoute.name,
|
||||
path: '/browser',
|
||||
routes: [
|
||||
TypedGoRoute<SearchRoute>(
|
||||
name: 'SearchRoute',
|
||||
path: 'search/:tabType/:searchText',
|
||||
),
|
||||
TypedGoRoute<TabViewRoute>(name: 'TabViewRoute', path: 'tab_view'),
|
||||
TypedGoRoute<ContextMenuRoute>(
|
||||
name: 'ContextMenuRoute',
|
||||
path: 'context_menu',
|
||||
),
|
||||
TypedGoRoute<ContainerDraftRoute>(
|
||||
name: 'ContainerDraftRoute',
|
||||
path: 'container_draft',
|
||||
),
|
||||
TypedGoRoute<ContainerListRoute>(
|
||||
name: 'ContainerListRoute',
|
||||
path: 'containers',
|
||||
routes: [
|
||||
TypedGoRoute<ContainerCreateRoute>(
|
||||
name: 'ContainerCreateRoute',
|
||||
path: 'create/:containerData',
|
||||
),
|
||||
TypedGoRoute<ContainerEditRoute>(
|
||||
name: 'ContainerEditRoute',
|
||||
path: 'edit/:containerData',
|
||||
),
|
||||
],
|
||||
),
|
||||
TypedGoRoute<ContainerSelectionRoute>(
|
||||
name: 'ContainerSelectionRoute',
|
||||
path: 'select_container',
|
||||
),
|
||||
TypedGoRoute<TabTreeRoute>(
|
||||
name: 'TabTreeRoute',
|
||||
path: 'tab_tree/:rootTabId',
|
||||
),
|
||||
TypedGoRoute<OpenSharedContentRoute>(
|
||||
name: 'OpenSharedContentRoute',
|
||||
path: 'open_content',
|
||||
),
|
||||
TypedGoRoute<SelectProfileRoute>(
|
||||
name: 'SelectProfileRoute',
|
||||
path: 'profile',
|
||||
),
|
||||
],
|
||||
)
|
||||
class BrowserRoute extends GoRouteData with $BrowserRoute {
|
||||
static const name = 'BrowserRoute';
|
||||
|
||||
const BrowserRoute();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const BrowserScreen();
|
||||
}
|
||||
}
|
||||
|
||||
enum TabType { regular, private, child, isolated }
|
||||
|
||||
class SearchRoute extends GoRouteData with $SearchRoute {
|
||||
static const String emptySearchText = ' ';
|
||||
|
||||
final TabType tabType;
|
||||
|
||||
//This should be nullable but isnt allowed by go_router
|
||||
final String searchText;
|
||||
|
||||
final bool launchedFromIntent;
|
||||
|
||||
/// When provided, the search screen will load URLs into this existing tab
|
||||
/// instead of creating a new tab. This also changes the UI to show
|
||||
/// site-specific bangs instead of the tab type selector.
|
||||
final String? tabId;
|
||||
|
||||
const SearchRoute({
|
||||
required this.tabType,
|
||||
this.searchText = SearchRoute.emptySearchText,
|
||||
this.launchedFromIntent = false,
|
||||
this.tabId,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return SearchScreen(
|
||||
tabType: tabType,
|
||||
initialSearchText: (searchText.isEmpty || searchText == emptySearchText)
|
||||
? null
|
||||
: searchText,
|
||||
launchedFromIntent: launchedFromIntent,
|
||||
tabId: tabId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
bool _isContainerUiEnabled(BuildContext context) {
|
||||
final settings = ProviderScope.containerOf(
|
||||
context,
|
||||
listen: false,
|
||||
).read(generalSettingsWithDefaultsProvider);
|
||||
|
||||
return settings.showContainerUi;
|
||||
}
|
||||
|
||||
class ContainerDraftRoute extends GoRouteData with $ContainerDraftRoute {
|
||||
const ContainerDraftRoute();
|
||||
|
||||
@override
|
||||
String? redirect(BuildContext context, GoRouterState state) {
|
||||
return _isContainerUiEnabled(context)
|
||||
? null
|
||||
: const BrowserRoute().location;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const ContainerDraftSuggestionsScreen();
|
||||
}
|
||||
}
|
||||
|
||||
class ContainerListRoute extends GoRouteData with $ContainerListRoute {
|
||||
const ContainerListRoute();
|
||||
|
||||
@override
|
||||
String? redirect(BuildContext context, GoRouterState state) {
|
||||
return _isContainerUiEnabled(context)
|
||||
? null
|
||||
: const BrowserRoute().location;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const ContainerListScreen();
|
||||
}
|
||||
}
|
||||
|
||||
class ContainerSelectionRoute extends GoRouteData
|
||||
with $ContainerSelectionRoute {
|
||||
const ContainerSelectionRoute();
|
||||
|
||||
@override
|
||||
String? redirect(BuildContext context, GoRouterState state) {
|
||||
return _isContainerUiEnabled(context)
|
||||
? null
|
||||
: const BrowserRoute().location;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const ContainerSelectionScreen();
|
||||
}
|
||||
}
|
||||
|
||||
class ContainerEditRoute extends GoRouteData with $ContainerEditRoute {
|
||||
final String containerData;
|
||||
|
||||
const ContainerEditRoute({required this.containerData});
|
||||
|
||||
@override
|
||||
String? redirect(BuildContext context, GoRouterState state) {
|
||||
return _isContainerUiEnabled(context)
|
||||
? null
|
||||
: const BrowserRoute().location;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return ContainerEditScreen.edit(
|
||||
initialContainer: ContainerDataWithCount.fromJson(
|
||||
jsonDecode(containerData) as Map<String, dynamic>,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ContainerCreateRoute extends GoRouteData with $ContainerCreateRoute {
|
||||
final String containerData;
|
||||
final String tabIds;
|
||||
|
||||
ContainerCreateRoute({required this.containerData, this.tabIds = '[]'});
|
||||
|
||||
@override
|
||||
String? redirect(BuildContext context, GoRouterState state) {
|
||||
return _isContainerUiEnabled(context)
|
||||
? null
|
||||
: const BrowserRoute().location;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
final tabIdsList = jsonDecode(tabIds) as List;
|
||||
final tabIdsSet = tabIdsList.cast<String>().toSet();
|
||||
|
||||
return ContainerEditScreen.create(
|
||||
initialContainer: ContainerData.fromJson(
|
||||
jsonDecode(containerData) as Map<String, dynamic>,
|
||||
),
|
||||
tabIds: tabIdsSet.isNotEmpty ? tabIdsSet : null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ContextMenuRoute extends GoRouteData with $ContextMenuRoute {
|
||||
final String hitResult;
|
||||
|
||||
const ContextMenuRoute({required this.hitResult});
|
||||
|
||||
@override
|
||||
Page<void> buildPage(BuildContext context, GoRouterState state) {
|
||||
return DialogPage(
|
||||
builder: (_) =>
|
||||
ContextMenuDialog(hitResult: HitResultJson.fromJson(hitResult)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TabTreeRoute extends GoRouteData with $TabTreeRoute {
|
||||
final String rootTabId;
|
||||
|
||||
const TabTreeRoute(this.rootTabId);
|
||||
|
||||
@override
|
||||
Page<void> buildPage(BuildContext context, GoRouterState state) {
|
||||
return DialogPage(builder: (_) => TabTreeDialog(rootTabId));
|
||||
}
|
||||
}
|
||||
|
||||
class OpenSharedContentRoute extends GoRouteData with $OpenSharedContentRoute {
|
||||
final String sharedUrl;
|
||||
|
||||
const OpenSharedContentRoute({this.sharedUrl = 'about:blank'});
|
||||
|
||||
@override
|
||||
Page<void> buildPage(BuildContext context, GoRouterState state) {
|
||||
return BottomSheetPage(
|
||||
builder: (_) => OpenSharedContent(
|
||||
sharedUrl: Uri.tryParse(sharedUrl) ?? Uri.parse('about:blank'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TabViewRoute extends GoRouteData with $TabViewRoute {
|
||||
const TabViewRoute();
|
||||
|
||||
@override
|
||||
Page<void> buildPage(BuildContext context, GoRouterState state) {
|
||||
return DialogPage(builder: (_) => const TabViewScreen());
|
||||
}
|
||||
}
|
||||
|
||||
class SelectProfileRoute extends GoRouteData with $SelectProfileRoute {
|
||||
const SelectProfileRoute();
|
||||
|
||||
@override
|
||||
Page<void> buildPage(BuildContext context, GoRouterState state) {
|
||||
return BottomSheetPage(builder: (_) => const SelectProfileDialog());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
/*
|
||||
* 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:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:nullability/nullability.dart';
|
||||
import 'package:weblibre/core/routing/widgets/bottom_sheet_page.dart';
|
||||
import 'package:weblibre/core/routing/widgets/dialog_page.dart';
|
||||
import 'package:weblibre/domain/entities/profile.dart';
|
||||
import 'package:weblibre/features/about/presentation/screens/about.dart';
|
||||
import 'package:weblibre/features/bangs/data/models/bang.dart';
|
||||
import 'package:weblibre/features/bangs/presentation/screens/categories.dart';
|
||||
import 'package:weblibre/features/bangs/presentation/screens/category.dart';
|
||||
import 'package:weblibre/features/bangs/presentation/screens/edit.dart';
|
||||
import 'package:weblibre/features/bangs/presentation/screens/menu.dart';
|
||||
import 'package:weblibre/features/bangs/presentation/screens/search.dart';
|
||||
import 'package:weblibre/features/bangs/presentation/screens/user.dart';
|
||||
import 'package:weblibre/features/geckoview/features/bookmarks/domain/entities/bookmark_item.dart';
|
||||
import 'package:weblibre/features/geckoview/features/bookmarks/presentation/screens/bookmark_entry_edit.dart';
|
||||
import 'package:weblibre/features/geckoview/features/bookmarks/presentation/screens/bookmark_folder_edit.dart';
|
||||
import 'package:weblibre/features/geckoview/features/bookmarks/presentation/screens/bookmark_list.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/dialogs/tab_tree.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/screens/browser.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/screens/tab_view.dart';
|
||||
import 'package:weblibre/features/geckoview/features/contextmenu/extensions/hit_result.dart';
|
||||
import 'package:weblibre/features/geckoview/features/contextmenu/presentation/context_menu_dialog.dart';
|
||||
import 'package:weblibre/features/geckoview/features/history/presentation/screens/history.dart';
|
||||
import 'package:weblibre/features/geckoview/features/open_link_tools/presentation/dialogs/open_shared_content.dart';
|
||||
import 'package:weblibre/features/geckoview/features/open_link_tools/presentation/screens/unshortener_settings.dart';
|
||||
import 'package:weblibre/features/geckoview/features/open_link_tools/presentation/screens/url_cleaner_settings.dart';
|
||||
import 'package:weblibre/features/geckoview/features/search/presentation/screens/search.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/data/models/container_data.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/presentation/screens/container_draft_suggestions.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/presentation/screens/container_edit.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/presentation/screens/container_list.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/presentation/screens/container_selection.dart';
|
||||
import 'package:weblibre/features/onboarding/presentation/onboarding.dart';
|
||||
import 'package:weblibre/features/settings/presentation/screens/addon_collection.dart';
|
||||
import 'package:weblibre/features/settings/presentation/screens/advanced_settings.dart';
|
||||
import 'package:weblibre/features/settings/presentation/screens/bang_settings.dart';
|
||||
import 'package:weblibre/features/settings/presentation/screens/browsing_settings.dart';
|
||||
import 'package:weblibre/features/settings/presentation/screens/contextual_toolbar_settings.dart';
|
||||
import 'package:weblibre/features/settings/presentation/screens/custom_tracking_protection.dart';
|
||||
import 'package:weblibre/features/settings/presentation/screens/doh_settings.dart';
|
||||
import 'package:weblibre/features/settings/presentation/screens/error_logs_screen.dart';
|
||||
import 'package:weblibre/features/settings/presentation/screens/experimental_settings.dart';
|
||||
import 'package:weblibre/features/settings/presentation/screens/extensions_settings.dart';
|
||||
import 'package:weblibre/features/settings/presentation/screens/fingerprint_settings.dart';
|
||||
import 'package:weblibre/features/settings/presentation/screens/general_settings.dart';
|
||||
import 'package:weblibre/features/settings/presentation/screens/locale_settings.dart';
|
||||
import 'package:weblibre/features/settings/presentation/screens/privacy_security_settings.dart';
|
||||
import 'package:weblibre/features/settings/presentation/screens/search_settings.dart';
|
||||
import 'package:weblibre/features/settings/presentation/screens/settings.dart';
|
||||
import 'package:weblibre/features/settings/presentation/screens/toolbar_layout_settings.dart';
|
||||
import 'package:weblibre/features/settings/presentation/screens/tracking_protection_exceptions.dart';
|
||||
import 'package:weblibre/features/settings/presentation/screens/web_content_settings.dart';
|
||||
import 'package:weblibre/features/settings/presentation/screens/web_engine_hardening.dart';
|
||||
import 'package:weblibre/features/settings/presentation/screens/web_engine_hardening_group.dart';
|
||||
import 'package:weblibre/features/sync/presentation/screens/sync_settings.dart';
|
||||
import 'package:weblibre/features/tor/presentation/screens/country_picker.dart';
|
||||
import 'package:weblibre/features/tor/presentation/screens/tor_proxy.dart';
|
||||
import 'package:weblibre/features/user/domain/presentation/dialogs/select_profile.dart';
|
||||
import 'package:weblibre/features/user/domain/presentation/screens/profile_backup.dart';
|
||||
import 'package:weblibre/features/user/domain/presentation/screens/profile_backup_list.dart';
|
||||
import 'package:weblibre/features/user/domain/presentation/screens/profile_edit.dart';
|
||||
import 'package:weblibre/features/user/domain/presentation/screens/profile_list.dart';
|
||||
import 'package:weblibre/features/user/domain/presentation/screens/profile_restore.dart';
|
||||
import 'package:weblibre/features/user/domain/presentation/widgets/auth_gate.dart';
|
||||
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
||||
import 'package:weblibre/features/web_feed/presentation/add_feed_dialog.dart';
|
||||
import 'package:weblibre/features/web_feed/presentation/screens/feed_article.dart';
|
||||
import 'package:weblibre/features/web_feed/presentation/screens/feed_article_list.dart';
|
||||
import 'package:weblibre/features/web_feed/presentation/screens/feed_edit.dart';
|
||||
import 'package:weblibre/features/web_feed/presentation/screens/feed_list.dart';
|
||||
import 'package:weblibre/features/web_feed/presentation/select_feed_dialog.dart';
|
||||
|
||||
part 'routes.bangs.dart';
|
||||
part 'routes.bookmarks.dart';
|
||||
part 'routes.browser.dart';
|
||||
part 'routes.feeds.dart';
|
||||
part 'routes.g.dart';
|
||||
part 'routes.history.dart';
|
||||
part 'routes.profiles.dart';
|
||||
part 'routes.settings.dart';
|
||||
part 'routes.tor.dart';
|
||||
|
||||
@TypedGoRoute<AboutRoute>(name: 'AboutRoute', path: '/about')
|
||||
class AboutRoute extends GoRouteData with $AboutRoute {
|
||||
@override
|
||||
Page<void> buildPage(BuildContext context, GoRouterState state) {
|
||||
return DialogPage(builder: (_) => const AboutDialogScreen());
|
||||
}
|
||||
}
|
||||
|
||||
@TypedGoRoute<OnboardingRoute>(
|
||||
name: OnboardingRoute.name,
|
||||
path: '${OnboardingRoute.pathPrefix}/:currentRevision/:targetRevision',
|
||||
)
|
||||
class OnboardingRoute extends GoRouteData with $OnboardingRoute {
|
||||
static const name = 'OnboardingRoute';
|
||||
static const pathPrefix = '/onboarding';
|
||||
|
||||
final int currentRevision;
|
||||
final int targetRevision;
|
||||
|
||||
const OnboardingRoute({
|
||||
required this.currentRevision,
|
||||
required this.targetRevision,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return OnboardingScreen(
|
||||
currentRevision: currentRevision,
|
||||
targetRevision: targetRevision,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@TypedGoRoute<LockRoute>(name: LockRoute.name, path: LockRoute.path)
|
||||
class LockRoute extends GoRouteData with $LockRoute {
|
||||
static const name = 'LockRoute';
|
||||
static const path = '/lock';
|
||||
|
||||
const LockRoute();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const LockScreen();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
part of 'routes.dart';
|
||||
|
||||
@TypedGoRoute<FeedListRoute>(
|
||||
name: 'FeedListRoute',
|
||||
path: '/feeds',
|
||||
routes: [
|
||||
TypedGoRoute<FeedAddRoute>(name: FeedAddRoute.name, path: 'add'),
|
||||
TypedGoRoute<FeedArticleListRoute>(
|
||||
name: 'FeedArticleListRoute',
|
||||
path: 'articles/:feedId',
|
||||
),
|
||||
TypedGoRoute<FeedArticleRoute>(
|
||||
name: 'FeedArticleRoute',
|
||||
path: 'article/:articleId',
|
||||
),
|
||||
TypedGoRoute<FeedCreateRoute>(
|
||||
name: 'FeedCreateRoute',
|
||||
path: 'create/:feedId',
|
||||
),
|
||||
TypedGoRoute<SelectFeedDialogRoute>(
|
||||
name: 'SelectFeedDialogRoute',
|
||||
path: 'available/:feedsJson',
|
||||
),
|
||||
TypedGoRoute<FeedEditRoute>(name: 'FeedEditRoute', path: 'edit/:feedId'),
|
||||
],
|
||||
)
|
||||
class FeedListRoute extends GoRouteData with $FeedListRoute {
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const FeedListScreen();
|
||||
}
|
||||
}
|
||||
|
||||
class FeedCreateRoute extends GoRouteData with $FeedCreateRoute {
|
||||
final Uri feedId;
|
||||
|
||||
FeedCreateRoute({required this.feedId});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return FeedEditScreen.create(feedId: feedId);
|
||||
}
|
||||
}
|
||||
|
||||
class SelectFeedDialogRoute extends GoRouteData with $SelectFeedDialogRoute {
|
||||
final String feedsJson;
|
||||
|
||||
const SelectFeedDialogRoute({required this.feedsJson});
|
||||
|
||||
@override
|
||||
Page<void> buildPage(BuildContext context, GoRouterState state) {
|
||||
final feedUris = Set<Uri>.from(
|
||||
(jsonDecode(feedsJson) as List<dynamic>).map(
|
||||
(url) => Uri.parse(url as String),
|
||||
),
|
||||
);
|
||||
|
||||
return BottomSheetPage(
|
||||
builder: (_) => SelectFeedDialog(feedUris: feedUris),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FeedEditRoute extends GoRouteData with $FeedEditRoute {
|
||||
final Uri feedId;
|
||||
|
||||
const FeedEditRoute({required this.feedId});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return FeedEditScreen.edit(feedId: feedId);
|
||||
}
|
||||
}
|
||||
|
||||
class FeedAddRoute extends GoRouteData with $FeedAddRoute {
|
||||
final String? uri;
|
||||
|
||||
static const name = 'FeedAddRoute';
|
||||
|
||||
const FeedAddRoute({required this.uri});
|
||||
|
||||
@override
|
||||
Page<void> buildPage(BuildContext context, GoRouterState state) {
|
||||
return DialogPage(
|
||||
builder: (_) =>
|
||||
AddFeedDialog(initialUri: uri.mapNotNull((uri) => Uri.tryParse(uri))),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FeedArticleListRoute extends GoRouteData with $FeedArticleListRoute {
|
||||
final Uri feedId;
|
||||
|
||||
FeedArticleListRoute({required this.feedId});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return FeedArticleListScreen(feedId: feedId);
|
||||
}
|
||||
}
|
||||
|
||||
class FeedArticleRoute extends GoRouteData with $FeedArticleRoute {
|
||||
final String articleId;
|
||||
|
||||
FeedArticleRoute({required this.articleId});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return FeedArticleScreen(articleId: articleId);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
part of 'routes.dart';
|
||||
|
||||
@TypedGoRoute<HistoryRoute>(
|
||||
name: 'HistoryRoute',
|
||||
path: '/history',
|
||||
routes: [
|
||||
TypedGoRoute<HistoryDownloadsRoute>(
|
||||
name: 'HistoryDownloadsRoute',
|
||||
path: 'downloads',
|
||||
),
|
||||
],
|
||||
)
|
||||
class HistoryRoute extends GoRouteData with $HistoryRoute {
|
||||
const HistoryRoute();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const HistoryScreen();
|
||||
}
|
||||
}
|
||||
|
||||
class HistoryDownloadsRoute extends GoRouteData with $HistoryDownloadsRoute {
|
||||
const HistoryDownloadsRoute();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const HistoryScreen(mode: HistoryScreenMode.downloads);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
part of 'routes.dart';
|
||||
|
||||
@TypedGoRoute<ProfileListRoute>(
|
||||
name: 'ProfileListRoute',
|
||||
path: '/profiles',
|
||||
routes: [
|
||||
TypedGoRoute<EditProfileRoute>(name: 'ProfileEditRoute', path: 'edit'),
|
||||
TypedGoRoute<ProfileBackupListRoute>(
|
||||
name: 'ProfileBackupListRoute',
|
||||
path: 'backup_list',
|
||||
),
|
||||
TypedGoRoute<RestoreProfileRoute>(
|
||||
name: 'RestoreProfileRoute',
|
||||
path: 'restore',
|
||||
),
|
||||
TypedGoRoute<BackupProfileRoute>(
|
||||
name: 'BackupProfileRoute',
|
||||
path: 'backup',
|
||||
),
|
||||
TypedGoRoute<CreateProfileRoute>(
|
||||
name: 'CreateProfileRoute',
|
||||
path: 'create',
|
||||
),
|
||||
],
|
||||
)
|
||||
class ProfileListRoute extends GoRouteData with $ProfileListRoute {
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const ProfileListScreen();
|
||||
}
|
||||
}
|
||||
|
||||
class CreateProfileRoute extends GoRouteData with $CreateProfileRoute {
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const ProfileEditScreen(profile: null);
|
||||
}
|
||||
}
|
||||
|
||||
class EditProfileRoute extends GoRouteData with $EditProfileRoute {
|
||||
final String profile;
|
||||
|
||||
const EditProfileRoute({required this.profile});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return ProfileEditScreen(
|
||||
profile: Profile.fromJson(jsonDecode(profile) as Map<String, dynamic>),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class BackupProfileRoute extends GoRouteData with $BackupProfileRoute {
|
||||
final String profile;
|
||||
|
||||
const BackupProfileRoute({required this.profile});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return ProfileBackupScreen(
|
||||
profile: Profile.fromJson(jsonDecode(profile) as Map<String, dynamic>),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RestoreProfileRoute extends GoRouteData with $RestoreProfileRoute {
|
||||
final String backupFileUri;
|
||||
|
||||
const RestoreProfileRoute({required this.backupFileUri});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return ProfileRestoreScreen(backupFileUri: Uri.parse(backupFileUri));
|
||||
}
|
||||
}
|
||||
|
||||
class ProfileBackupListRoute extends GoRouteData with $ProfileBackupListRoute {
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const ProfileBackupListScreen();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
part of 'routes.dart';
|
||||
|
||||
@TypedGoRoute<SettingsRoute>(
|
||||
name: 'SettingsRoute',
|
||||
path: '/settings',
|
||||
routes: [
|
||||
TypedGoRoute<GeneralSettingsRoute>(
|
||||
name: 'GeneralSettingsRoute',
|
||||
path: 'general',
|
||||
),
|
||||
TypedGoRoute<BrowsingSettingsRoute>(
|
||||
name: 'BrowsingSettingsRoute',
|
||||
path: 'browsing',
|
||||
),
|
||||
TypedGoRoute<PrivacySecuritySettingsRoute>(
|
||||
name: 'PrivacySecuritySettingsRoute',
|
||||
path: 'privacy_security',
|
||||
),
|
||||
TypedGoRoute<ToolbarLayoutSettingsRoute>(
|
||||
name: 'ToolbarLayoutSettingsRoute',
|
||||
path: 'toolbar_layout',
|
||||
),
|
||||
TypedGoRoute<WebContentSettingsRoute>(
|
||||
name: 'WebContentSettingsRoute',
|
||||
path: 'web_content',
|
||||
),
|
||||
TypedGoRoute<SearchSettingsRoute>(
|
||||
name: 'SearchSettingsRoute',
|
||||
path: 'search',
|
||||
),
|
||||
TypedGoRoute<ExtensionsSettingsRoute>(
|
||||
name: 'ExtensionsSettingsRoute',
|
||||
path: 'extensions',
|
||||
),
|
||||
TypedGoRoute<AdvancedSettingsRoute>(
|
||||
name: 'AdvancedSettingsRoute',
|
||||
path: 'advanced',
|
||||
),
|
||||
TypedGoRoute<ExperimentalSettingsRoute>(
|
||||
name: 'ExperimentalSettingsRoute',
|
||||
path: 'experimental',
|
||||
),
|
||||
TypedGoRoute<BangSettingsRoute>(name: 'BangSettingsRoute', path: 'bang'),
|
||||
TypedGoRoute<WebEngineHardeningRoute>(
|
||||
name: 'WebEngineHardeningRoute',
|
||||
path: 'hardening',
|
||||
routes: [
|
||||
TypedGoRoute<WebEngineHardeningGroupRoute>(
|
||||
name: 'WebEngineHardeningGroupRoute',
|
||||
path: 'group/:group',
|
||||
),
|
||||
],
|
||||
),
|
||||
TypedGoRoute<DohSettingsRoute>(name: 'DohSettingsRoute', path: 'doh'),
|
||||
TypedGoRoute<FingerprintSettingsRoute>(
|
||||
name: 'FingerprintSettingsRoute',
|
||||
path: 'fingerprint',
|
||||
),
|
||||
TypedGoRoute<LocaleSettingsRoute>(
|
||||
name: 'LocaleSettingsRoute',
|
||||
path: 'locales',
|
||||
),
|
||||
TypedGoRoute<AddonCollectionRoute>(
|
||||
name: 'AddonCollectionRoute',
|
||||
path: 'addon_collection',
|
||||
),
|
||||
TypedGoRoute<TrackingProtectionExceptionsRoute>(
|
||||
name: 'TrackingProtectionExceptionsRoute',
|
||||
path: 'tracking_protection_exceptions',
|
||||
),
|
||||
TypedGoRoute<CustomTrackingProtectionRoute>(
|
||||
name: 'CustomTrackingProtectionRoute',
|
||||
path: 'custom_tracking_protection',
|
||||
),
|
||||
TypedGoRoute<ErrorLogsRoute>(name: 'ErrorLogsRoute', path: 'error_logs'),
|
||||
TypedGoRoute<SyncSettingsRoute>(name: 'SyncSettingsRoute', path: 'sync'),
|
||||
TypedGoRoute<UrlCleanerSettingsRoute>(
|
||||
name: 'UrlCleanerSettingsRoute',
|
||||
path: 'url_cleaner',
|
||||
),
|
||||
TypedGoRoute<UnshortenerSettingsRoute>(
|
||||
name: 'UnshortenerSettingsRoute',
|
||||
path: 'unshortener',
|
||||
),
|
||||
TypedGoRoute<ContextualToolbarSettingsRoute>(
|
||||
name: 'ContextualToolbarSettingsRoute',
|
||||
path: 'contextual_toolbar',
|
||||
),
|
||||
],
|
||||
)
|
||||
class SettingsRoute extends GoRouteData with $SettingsRoute {
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const SettingsScreen();
|
||||
}
|
||||
}
|
||||
|
||||
class GeneralSettingsRoute extends GoRouteData with $GeneralSettingsRoute {
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const GeneralSettingsScreen();
|
||||
}
|
||||
}
|
||||
|
||||
class BrowsingSettingsRoute extends GoRouteData with $BrowsingSettingsRoute {
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const BrowsingSettingsScreen();
|
||||
}
|
||||
}
|
||||
|
||||
class PrivacySecuritySettingsRoute extends GoRouteData
|
||||
with $PrivacySecuritySettingsRoute {
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const PrivacySecuritySettingsScreen();
|
||||
}
|
||||
}
|
||||
|
||||
class ToolbarLayoutSettingsRoute extends GoRouteData
|
||||
with $ToolbarLayoutSettingsRoute {
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const ToolbarLayoutSettingsScreen();
|
||||
}
|
||||
}
|
||||
|
||||
class WebContentSettingsRoute extends GoRouteData
|
||||
with $WebContentSettingsRoute {
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const WebContentSettingsScreen();
|
||||
}
|
||||
}
|
||||
|
||||
class SearchSettingsRoute extends GoRouteData with $SearchSettingsRoute {
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const SearchSettingsScreen();
|
||||
}
|
||||
}
|
||||
|
||||
class ExtensionsSettingsRoute extends GoRouteData
|
||||
with $ExtensionsSettingsRoute {
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const ExtensionsSettingsScreen();
|
||||
}
|
||||
}
|
||||
|
||||
class AdvancedSettingsRoute extends GoRouteData with $AdvancedSettingsRoute {
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const AdvancedSettingsScreen();
|
||||
}
|
||||
}
|
||||
|
||||
class ExperimentalSettingsRoute extends GoRouteData
|
||||
with $ExperimentalSettingsRoute {
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const ExperimentalSettingsScreen();
|
||||
}
|
||||
}
|
||||
|
||||
class BangSettingsRoute extends GoRouteData with $BangSettingsRoute {
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const BangSettingsScreen();
|
||||
}
|
||||
}
|
||||
|
||||
class DohSettingsRoute extends GoRouteData with $DohSettingsRoute {
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const DohSettingsScreen();
|
||||
}
|
||||
}
|
||||
|
||||
class FingerprintSettingsRoute extends GoRouteData
|
||||
with $FingerprintSettingsRoute {
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const FingerprintSettingsScreen();
|
||||
}
|
||||
}
|
||||
|
||||
class LocaleSettingsRoute extends GoRouteData with $LocaleSettingsRoute {
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const LocaleSettingsScreen();
|
||||
}
|
||||
}
|
||||
|
||||
class AddonCollectionRoute extends GoRouteData with $AddonCollectionRoute {
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const AddonCollectionScreen();
|
||||
}
|
||||
}
|
||||
|
||||
class WebEngineHardeningRoute extends GoRouteData
|
||||
with $WebEngineHardeningRoute {
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const WebEngineHardeningScreen();
|
||||
}
|
||||
}
|
||||
|
||||
class WebEngineHardeningGroupRoute extends GoRouteData
|
||||
with $WebEngineHardeningGroupRoute {
|
||||
final String group;
|
||||
|
||||
const WebEngineHardeningGroupRoute({required this.group});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return WebEngineHardeningGroupScreen(groupName: group);
|
||||
}
|
||||
}
|
||||
|
||||
class TrackingProtectionExceptionsRoute extends GoRouteData
|
||||
with $TrackingProtectionExceptionsRoute {
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const TrackingProtectionExceptionsScreen();
|
||||
}
|
||||
}
|
||||
|
||||
class ErrorLogsRoute extends GoRouteData with $ErrorLogsRoute {
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const ErrorLogsScreen();
|
||||
}
|
||||
}
|
||||
|
||||
class CustomTrackingProtectionRoute extends GoRouteData
|
||||
with $CustomTrackingProtectionRoute {
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const CustomTrackingProtectionScreen();
|
||||
}
|
||||
}
|
||||
|
||||
class SyncSettingsRoute extends GoRouteData with $SyncSettingsRoute {
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const SyncSettingsScreen();
|
||||
}
|
||||
}
|
||||
|
||||
class UrlCleanerSettingsRoute extends GoRouteData
|
||||
with $UrlCleanerSettingsRoute {
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const UrlCleanerSettingsScreen();
|
||||
}
|
||||
}
|
||||
|
||||
class UnshortenerSettingsRoute extends GoRouteData
|
||||
with $UnshortenerSettingsRoute {
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const UnshortenerSettingsScreen();
|
||||
}
|
||||
}
|
||||
|
||||
class ContextualToolbarSettingsRoute extends GoRouteData
|
||||
with $ContextualToolbarSettingsRoute {
|
||||
const ContextualToolbarSettingsRoute();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const ContextualToolbarSettingsScreen();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
part of 'routes.dart';
|
||||
|
||||
@TypedGoRoute<TorProxyRoute>(
|
||||
name: 'TorProxyRoute',
|
||||
path: '/tor',
|
||||
routes: [
|
||||
TypedGoRoute<TorCountryPickerRoute>(
|
||||
name: 'TorCountryPickerRoute',
|
||||
path: 'country_picker',
|
||||
),
|
||||
],
|
||||
)
|
||||
class TorProxyRoute extends GoRouteData with $TorProxyRoute {
|
||||
const TorProxyRoute();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const TorProxyScreen();
|
||||
}
|
||||
}
|
||||
|
||||
class TorCountryPickerRoute extends GoRouteData with $TorCountryPickerRoute {
|
||||
final String title;
|
||||
final String? $extra;
|
||||
|
||||
const TorCountryPickerRoute({required this.title, this.$extra});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return CountryPickerScreen(title: title, selectedCountryCode: $extra);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// A bottom sheet page with Material entrance and exit animations.
|
||||
/// Similar to DialogPage but displays content as a modal bottom sheet.
|
||||
class BottomSheetPage<T> extends Page<T> {
|
||||
final WidgetBuilder builder;
|
||||
final Color? barrierColor;
|
||||
final bool barrierDismissible;
|
||||
final String? barrierLabel;
|
||||
final bool isScrollControlled;
|
||||
final bool useSafeArea;
|
||||
|
||||
const BottomSheetPage({
|
||||
required this.builder,
|
||||
this.barrierColor,
|
||||
this.barrierDismissible = true,
|
||||
this.barrierLabel,
|
||||
this.isScrollControlled = true,
|
||||
this.useSafeArea = true,
|
||||
super.key,
|
||||
super.name,
|
||||
super.arguments,
|
||||
super.restorationId,
|
||||
});
|
||||
|
||||
@override
|
||||
Route<T> createRoute(BuildContext context) => ModalBottomSheetRoute<T>(
|
||||
settings: this,
|
||||
builder: builder,
|
||||
barrierLabel:
|
||||
barrierLabel ??
|
||||
MaterialLocalizations.of(context).modalBarrierDismissLabel,
|
||||
backgroundColor: Theme.of(context).bottomSheetTheme.modalBackgroundColor,
|
||||
elevation: Theme.of(context).bottomSheetTheme.modalElevation,
|
||||
shape: Theme.of(context).bottomSheetTheme.shape,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
constraints: Theme.of(context).bottomSheetTheme.constraints,
|
||||
modalBarrierColor:
|
||||
barrierColor ?? Theme.of(context).bottomSheetTheme.modalBarrierColor,
|
||||
isScrollControlled: isScrollControlled,
|
||||
isDismissible: barrierDismissible,
|
||||
useSafeArea: useSafeArea,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// A dialog page with Material entrance and exit animations, modal barrier color,
|
||||
/// and modal barrier behavior (dialog is dismissible with a tap on the barrier).
|
||||
class DialogPage<T> extends Page<T> {
|
||||
final Offset? anchorPoint;
|
||||
final Color? barrierColor;
|
||||
final bool barrierDismissible;
|
||||
final String? barrierLabel;
|
||||
final bool useSafeArea;
|
||||
final CapturedThemes? themes;
|
||||
final WidgetBuilder builder;
|
||||
|
||||
const DialogPage({
|
||||
required this.builder,
|
||||
this.anchorPoint,
|
||||
this.barrierColor,
|
||||
this.barrierDismissible = true,
|
||||
this.barrierLabel,
|
||||
this.useSafeArea = true,
|
||||
this.themes,
|
||||
super.key,
|
||||
super.name,
|
||||
super.arguments,
|
||||
super.restorationId,
|
||||
});
|
||||
|
||||
@override
|
||||
Route<T> createRoute(BuildContext context) => DialogRoute<T>(
|
||||
context: context,
|
||||
settings: this,
|
||||
builder: builder,
|
||||
anchorPoint: anchorPoint,
|
||||
barrierColor:
|
||||
barrierColor ??
|
||||
DialogTheme.of(context).barrierColor ??
|
||||
Theme.of(context).dialogTheme.barrierColor ??
|
||||
Colors.black54,
|
||||
barrierDismissible: barrierDismissible,
|
||||
barrierLabel: barrierLabel,
|
||||
useSafeArea: useSafeArea,
|
||||
themes: themes,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/// Reusable sort directions for list items that share common sortable fields
|
||||
/// (title, URL, date).
|
||||
enum SortField { titleAsc, titleDesc, urlAsc, urlDesc, dateAsc, dateDesc }
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* 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:uuid/data.dart';
|
||||
import 'package:uuid/rng.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
const uuid = Uuid(goptions: GlobalOptions(CryptoRNG()));
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* 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:ui';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
class ColorJsonConverter implements JsonConverter<Color, int> {
|
||||
const ColorJsonConverter();
|
||||
|
||||
@override
|
||||
Color fromJson(int from) {
|
||||
return Color(from);
|
||||
}
|
||||
|
||||
@override
|
||||
int toJson(Color value) {
|
||||
return value.toARGB32();
|
||||
}
|
||||
}
|
||||
|
||||
class ColorConverter extends TypeConverter<Color, int> {
|
||||
const ColorConverter();
|
||||
|
||||
@override
|
||||
Color fromSql(int fromDb) {
|
||||
return Color(fromDb);
|
||||
}
|
||||
|
||||
@override
|
||||
int toSql(Color value) {
|
||||
return value.toARGB32();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
class DateTimeRangeConverter
|
||||
implements JsonConverter<DateTimeRange?, Map<String, dynamic>?> {
|
||||
const DateTimeRangeConverter();
|
||||
|
||||
@override
|
||||
DateTimeRange? fromJson(Map<String, dynamic>? json) {
|
||||
if (json == null) return null;
|
||||
|
||||
return DateTimeRange(
|
||||
start: DateTime.parse(json['start'] as String),
|
||||
end: DateTime.parse(json['end'] as String),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic>? toJson(DateTimeRange? dateTimeRange) {
|
||||
if (dateTimeRange == null) return null;
|
||||
|
||||
return {
|
||||
'start': dateTimeRange.start.toIso8601String(),
|
||||
'end': dateTimeRange.end.toIso8601String(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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:convert';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
class IconDataJsonConverter
|
||||
implements JsonConverter<IconData, Map<String, dynamic>> {
|
||||
const IconDataJsonConverter();
|
||||
|
||||
@override
|
||||
IconData fromJson(Map<String, dynamic> json) {
|
||||
return IconData(
|
||||
json['codePoint'] as int,
|
||||
fontFamily: json['fontFamily'] as String,
|
||||
fontPackage: json['fontPackage'] as String,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson(IconData iconData) {
|
||||
return <String, dynamic>{
|
||||
'codePoint': iconData.codePoint,
|
||||
'fontFamily': iconData.fontFamily,
|
||||
'fontPackage': iconData.fontPackage,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class IconDataTypeConverter implements TypeConverter<IconData, String> {
|
||||
const IconDataTypeConverter();
|
||||
|
||||
@override
|
||||
IconData fromSql(String fromDb) {
|
||||
final json = jsonDecode(fromDb) as Map<String, dynamic>;
|
||||
return const IconDataJsonConverter().fromJson(json);
|
||||
}
|
||||
|
||||
@override
|
||||
String toSql(IconData value) {
|
||||
assert(
|
||||
value.fontFamily != null,
|
||||
'Font family must be provided to identify icon',
|
||||
);
|
||||
|
||||
return jsonEncode(const IconDataJsonConverter().toJson(value));
|
||||
}
|
||||
}
|
||||
@@ -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 'dart:convert';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
|
||||
class StringListConverter extends TypeConverter<List<String>, String> {
|
||||
const StringListConverter();
|
||||
|
||||
@override
|
||||
List<String> fromSql(String fromDb) {
|
||||
return (jsonDecode(fromDb) as List).cast<String>();
|
||||
}
|
||||
|
||||
@override
|
||||
String toSql(List<String> value) {
|
||||
return jsonEncode(value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* 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/utils/uri_parser.dart' as uri_parser;
|
||||
|
||||
class UriConverter extends TypeConverter<Uri, String> {
|
||||
const UriConverter();
|
||||
|
||||
@override
|
||||
Uri fromSql(String fromDb) {
|
||||
return uri_parser.tryParseUrl(fromDb, eagerParsing: true) ??
|
||||
Uri.parse(fromDb);
|
||||
}
|
||||
|
||||
@override
|
||||
String toSql(Uri value) {
|
||||
return value.toString();
|
||||
}
|
||||
}
|
||||
|
||||
class UriConverterNullable extends TypeConverter<Uri?, String?> {
|
||||
const UriConverterNullable();
|
||||
|
||||
@override
|
||||
Uri? fromSql(String? fromDb) {
|
||||
return uri_parser.tryParseUrl(fromDb, eagerParsing: true);
|
||||
}
|
||||
|
||||
@override
|
||||
String? toSql(Uri? value) {
|
||||
return value?.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* 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';
|
||||
|
||||
extension TableSize on DatabaseConnectionUser {
|
||||
SingleSelectable<double> tableSize(TableInfo table) {
|
||||
return customSelect(
|
||||
'SELECT SUM(pgsize) /(1024.0 * 1024.0)AS pgsize_mb FROM dbstat WHERE name = ?1',
|
||||
variables: [Variable<String>(table.actualTableName)],
|
||||
readsFrom: {table},
|
||||
).map((QueryRow row) => row.read<double>('pgsize_mb'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* 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:lexo_rank/lexo_rank.dart';
|
||||
import 'package:lexo_rank/lexo_rank/lexo_rank_bucket.dart';
|
||||
import 'package:nullability/nullability.dart';
|
||||
import 'package:sqlite3/common.dart';
|
||||
|
||||
String _nextRankOrMiddle(List<Object?> args) {
|
||||
final parsedBucket = LexoRankBucket.resolve(args[0]! as int);
|
||||
|
||||
if (args[1] != null) {
|
||||
final parsedRank = LexoRank.parse(args[1]! as String);
|
||||
|
||||
return parsedRank.genNext().value;
|
||||
} else {
|
||||
return LexoRank.middle(bucket: parsedBucket).value;
|
||||
}
|
||||
}
|
||||
|
||||
String _previousRankOrMiddle(List<Object?> args) {
|
||||
final parsedBucket = LexoRankBucket.resolve(args[0]! as int);
|
||||
|
||||
if (args[1] != null) {
|
||||
final parsedRank = LexoRank.parse(args[1]! as String);
|
||||
|
||||
return parsedRank.genPrev().value;
|
||||
} else {
|
||||
return LexoRank.middle(bucket: parsedBucket).value;
|
||||
}
|
||||
}
|
||||
|
||||
String _reorderAfter(List<Object?> args) {
|
||||
final first = args[0].mapNotNull((arg) => LexoRank.parse(arg as String));
|
||||
final last = args[1].mapNotNull((arg) => LexoRank.parse(arg as String));
|
||||
|
||||
if (first == null) {
|
||||
throw Exception('Tab not found');
|
||||
} else if (last == null) {
|
||||
return first.genNext().value;
|
||||
} else {
|
||||
return first.genBetween(last).value;
|
||||
}
|
||||
}
|
||||
|
||||
String _reorderBefore(List<Object?> args) {
|
||||
final first = args[0].mapNotNull((arg) => LexoRank.parse(arg as String));
|
||||
final last = args[1].mapNotNull((arg) => LexoRank.parse(arg as String));
|
||||
|
||||
if (first == null) {
|
||||
throw Exception('Tab not found');
|
||||
} else if (last == null) {
|
||||
return first.genPrev().value;
|
||||
} else {
|
||||
return last.genBetween(first).value;
|
||||
}
|
||||
}
|
||||
|
||||
void registerLexorankFunctions(CommonDatabase database) {
|
||||
database.createFunction(
|
||||
functionName: 'lexo_rank_next',
|
||||
argumentCount: const AllowedArgumentCount(2),
|
||||
function: _nextRankOrMiddle,
|
||||
);
|
||||
database.createFunction(
|
||||
functionName: 'lexo_rank_previous',
|
||||
argumentCount: const AllowedArgumentCount(2),
|
||||
function: _previousRankOrMiddle,
|
||||
);
|
||||
database.createFunction(
|
||||
functionName: 'lexo_rank_reorder_after',
|
||||
argumentCount: const AllowedArgumentCount(2),
|
||||
function: _reorderAfter,
|
||||
);
|
||||
database.createFunction(
|
||||
functionName: 'lexo_rank_reorder_before',
|
||||
argumentCount: const AllowedArgumentCount(2),
|
||||
function: _reorderBefore,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'package:fast_equatable/fast_equatable.dart';
|
||||
|
||||
sealed class DropTargetData with FastEquatable {}
|
||||
|
||||
final class ContainerDropData extends DropTargetData {
|
||||
final String tabId;
|
||||
|
||||
ContainerDropData(this.tabId);
|
||||
|
||||
@override
|
||||
List<Object?> get hashParameters => [tabId];
|
||||
}
|
||||
|
||||
final class DeleteDropData extends DropTargetData {
|
||||
final String tabId;
|
||||
|
||||
DeleteDropData(this.tabId);
|
||||
|
||||
@override
|
||||
List<Object?> get hashParameters => [tabId];
|
||||
}
|
||||
|
||||
sealed class DragTargetData with FastEquatable {}
|
||||
|
||||
final class TabDragData extends DragTargetData {
|
||||
final String tabId;
|
||||
|
||||
TabDragData(this.tabId);
|
||||
|
||||
@override
|
||||
List<Object?> get hashParameters => [tabId];
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'package:fast_equatable/fast_equatable.dart';
|
||||
|
||||
class ReceivedIntentParameter with FastEquatable {
|
||||
final String? content;
|
||||
final String? tool;
|
||||
final String? contextId;
|
||||
|
||||
ReceivedIntentParameter(this.content, this.tool, {this.contextId});
|
||||
|
||||
@override
|
||||
List<Object?> get hashParameters => [content, tool, contextId];
|
||||
}
|
||||
@@ -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:copy_with_extension/copy_with_extension.dart';
|
||||
import 'package:fast_equatable/fast_equatable.dart';
|
||||
import 'package:nullability/nullability.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/entities/browser_icon.dart';
|
||||
|
||||
part 'web_page_info.g.dart';
|
||||
|
||||
@CopyWith()
|
||||
class WebPageInfo with FastEquatable {
|
||||
final Uri url;
|
||||
final String? title;
|
||||
final BrowserIcon? favicon;
|
||||
final Set<Uri>? feeds;
|
||||
|
||||
bool get isPageInfoComplete =>
|
||||
title.isNotEmpty && favicon != null && feeds != null;
|
||||
|
||||
WebPageInfo({required this.url, this.title, this.favicon, this.feeds});
|
||||
|
||||
@override
|
||||
List<Object?> get hashParameters => [url, title, favicon, feeds];
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'web_page_info.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// CopyWithGenerator
|
||||
// **************************************************************************
|
||||
|
||||
abstract class _$WebPageInfoCWProxy {
|
||||
WebPageInfo url(Uri url);
|
||||
|
||||
WebPageInfo title(String? title);
|
||||
|
||||
WebPageInfo favicon(BrowserIcon? favicon);
|
||||
|
||||
WebPageInfo feeds(Set<Uri>? feeds);
|
||||
|
||||
/// 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 `WebPageInfo(...).copyWith.fieldName(value)`.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// WebPageInfo(...).copyWith(id: 12, name: "My name")
|
||||
/// ```
|
||||
WebPageInfo call({
|
||||
Uri url,
|
||||
String? title,
|
||||
BrowserIcon? favicon,
|
||||
Set<Uri>? feeds,
|
||||
});
|
||||
}
|
||||
|
||||
/// Callable proxy for `copyWith` functionality.
|
||||
/// Use as `instanceOfWebPageInfo.copyWith(...)` or call `instanceOfWebPageInfo.copyWith.fieldName(value)` for a single field.
|
||||
class _$WebPageInfoCWProxyImpl implements _$WebPageInfoCWProxy {
|
||||
const _$WebPageInfoCWProxyImpl(this._value);
|
||||
|
||||
final WebPageInfo _value;
|
||||
|
||||
@override
|
||||
WebPageInfo url(Uri url) => call(url: url);
|
||||
|
||||
@override
|
||||
WebPageInfo title(String? title) => call(title: title);
|
||||
|
||||
@override
|
||||
WebPageInfo favicon(BrowserIcon? favicon) => call(favicon: favicon);
|
||||
|
||||
@override
|
||||
WebPageInfo feeds(Set<Uri>? feeds) => call(feeds: feeds);
|
||||
|
||||
@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 `WebPageInfo(...).copyWith.fieldName(value)`.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// WebPageInfo(...).copyWith(id: 12, name: "My name")
|
||||
/// ```
|
||||
WebPageInfo call({
|
||||
Object? url = const $CopyWithPlaceholder(),
|
||||
Object? title = const $CopyWithPlaceholder(),
|
||||
Object? favicon = const $CopyWithPlaceholder(),
|
||||
Object? feeds = const $CopyWithPlaceholder(),
|
||||
}) {
|
||||
return WebPageInfo(
|
||||
url: url == const $CopyWithPlaceholder() || url == null
|
||||
? _value.url
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: url as Uri,
|
||||
title: title == const $CopyWithPlaceholder()
|
||||
? _value.title
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: title as String?,
|
||||
favicon: favicon == const $CopyWithPlaceholder()
|
||||
? _value.favicon
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: favicon as BrowserIcon?,
|
||||
feeds: feeds == const $CopyWithPlaceholder()
|
||||
? _value.feeds
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: feeds as Set<Uri>?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension $WebPageInfoCopyWith on WebPageInfo {
|
||||
/// Returns a callable class used to build a new instance with modified fields.
|
||||
/// Example: `instanceOfWebPageInfo.copyWith(...)` or `instanceOfWebPageInfo.copyWith.fieldName(...)`.
|
||||
// ignore: library_private_types_in_public_api
|
||||
_$WebPageInfoCWProxy get copyWith => _$WebPageInfoCWProxyImpl(this);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* 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:ui';
|
||||
|
||||
class EquatableImage {
|
||||
Image? _value;
|
||||
final int _imageHash;
|
||||
bool _isDisposed = false;
|
||||
|
||||
EquatableImage(Image value, {required int hash})
|
||||
: _value = value,
|
||||
_imageHash = hash;
|
||||
|
||||
/// The underlying ui.Image. Returns null if disposed.
|
||||
Image? get value => _isDisposed ? null : _value;
|
||||
|
||||
/// Whether this image has been disposed.
|
||||
bool get isDisposed => _isDisposed;
|
||||
|
||||
/// Disposes the underlying ui.Image to free GPU memory.
|
||||
/// This is safe to call multiple times.
|
||||
void dispose() {
|
||||
if (_isDisposed) return;
|
||||
_isDisposed = true;
|
||||
// Delay disposal to allow widgets to finish rendering
|
||||
Future.delayed(const Duration(seconds: 3), () {
|
||||
_value?.dispose();
|
||||
_value = null;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => _imageHash.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is EquatableImage && other._imageHash == _imageHash;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'package:copy_with_extension/copy_with_extension.dart';
|
||||
import 'package:fast_equatable/fast_equatable.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:uuid/uuid_value.dart';
|
||||
import 'package:weblibre/core/uuid.dart';
|
||||
import 'package:weblibre/features/user/data/models/auth_settings.dart';
|
||||
|
||||
part 'profile.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
@CopyWith()
|
||||
class Profile with FastEquatable {
|
||||
@CopyWithField(immutable: true)
|
||||
final String id;
|
||||
final String name;
|
||||
final AuthSettings authSettings;
|
||||
|
||||
late final uuidValue = UuidValue.fromString(id);
|
||||
|
||||
static String getNewProfileId() => uuid.v7();
|
||||
|
||||
Profile({required this.id, required this.name, AuthSettings? authSettings})
|
||||
: authSettings = authSettings ?? AuthSettings.withDefaults();
|
||||
|
||||
factory Profile.create({required String name, AuthSettings? authSettings}) {
|
||||
return Profile(
|
||||
id: getNewProfileId(),
|
||||
name: name,
|
||||
authSettings: authSettings,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get hashParameters => [id, name, authSettings];
|
||||
|
||||
factory Profile.fromJson(Map<String, dynamic> json) =>
|
||||
_$ProfileFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$ProfileToJson(this);
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'profile.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// CopyWithGenerator
|
||||
// **************************************************************************
|
||||
|
||||
abstract class _$ProfileCWProxy {
|
||||
Profile name(String name);
|
||||
|
||||
Profile authSettings(AuthSettings? authSettings);
|
||||
|
||||
/// 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 `Profile(...).copyWith.fieldName(value)`.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// Profile(...).copyWith(id: 12, name: "My name")
|
||||
/// ```
|
||||
Profile call({String name, AuthSettings? authSettings});
|
||||
}
|
||||
|
||||
/// Callable proxy for `copyWith` functionality.
|
||||
/// Use as `instanceOfProfile.copyWith(...)` or call `instanceOfProfile.copyWith.fieldName(value)` for a single field.
|
||||
class _$ProfileCWProxyImpl implements _$ProfileCWProxy {
|
||||
const _$ProfileCWProxyImpl(this._value);
|
||||
|
||||
final Profile _value;
|
||||
|
||||
@override
|
||||
Profile name(String name) => call(name: name);
|
||||
|
||||
@override
|
||||
Profile authSettings(AuthSettings? authSettings) =>
|
||||
call(authSettings: authSettings);
|
||||
|
||||
@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 `Profile(...).copyWith.fieldName(value)`.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// Profile(...).copyWith(id: 12, name: "My name")
|
||||
/// ```
|
||||
Profile call({
|
||||
Object? name = const $CopyWithPlaceholder(),
|
||||
Object? authSettings = const $CopyWithPlaceholder(),
|
||||
}) {
|
||||
return Profile(
|
||||
id: _value.id,
|
||||
name: name == const $CopyWithPlaceholder() || name == null
|
||||
? _value.name
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: name as String,
|
||||
authSettings: authSettings == const $CopyWithPlaceholder()
|
||||
? _value.authSettings
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: authSettings as AuthSettings?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension $ProfileCopyWith on Profile {
|
||||
/// Returns a callable class used to build a new instance with modified fields.
|
||||
/// Example: `instanceOfProfile.copyWith(...)` or `instanceOfProfile.copyWith.fieldName(...)`.
|
||||
// ignore: library_private_types_in_public_api
|
||||
_$ProfileCWProxy get copyWith => _$ProfileCWProxyImpl(this);
|
||||
}
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
Profile _$ProfileFromJson(Map<String, dynamic> json) => Profile(
|
||||
id: json['id'] as String,
|
||||
name: json['name'] as String,
|
||||
authSettings: json['authSettings'] == null
|
||||
? null
|
||||
: AuthSettings.fromJson(json['authSettings'] as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ProfileToJson(Profile instance) => <String, dynamic>{
|
||||
'id': instance.id,
|
||||
'name': instance.name,
|
||||
'authSettings': instance.authSettings.toJson(),
|
||||
};
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'package:flutter/widgets.dart' hide Locale;
|
||||
import 'package:intl/locale.dart' as intl;
|
||||
import 'package:locale_resolver/locale_resolver.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:weblibre/extensions/locale.dart';
|
||||
|
||||
part 'locale_resolver.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class LocaleResolverRepository extends _$LocaleResolverRepository {
|
||||
final _service = LocaleResolver();
|
||||
final _cache = <intl.Locale, LocalizedResult>{};
|
||||
|
||||
Future<LocalizedResult> resolve(intl.Locale locale) async {
|
||||
final cached = _cache[locale];
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
return _cache[locale] = await _service.resolve(
|
||||
locale.toLanguageTag(),
|
||||
targetLocale.toLanguageTag(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void build(intl.Locale targetLocale) {}
|
||||
}
|
||||
|
||||
@Riverpod()
|
||||
Future<LocalizedResult> resolveLocale(Ref ref, intl.Locale locale) {
|
||||
return ref
|
||||
.read(
|
||||
localeResolverRepositoryProvider(
|
||||
WidgetsBinding.instance.platformDispatcher.locale.toIntlLocale(),
|
||||
).notifier,
|
||||
)
|
||||
.resolve(locale);
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'locale_resolver.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(LocaleResolverRepository)
|
||||
final localeResolverRepositoryProvider = LocaleResolverRepositoryFamily._();
|
||||
|
||||
final class LocaleResolverRepositoryProvider
|
||||
extends $NotifierProvider<LocaleResolverRepository, void> {
|
||||
LocaleResolverRepositoryProvider._({
|
||||
required LocaleResolverRepositoryFamily super.from,
|
||||
required intl.Locale super.argument,
|
||||
}) : super(
|
||||
retry: null,
|
||||
name: r'localeResolverRepositoryProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$localeResolverRepositoryHash();
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return r'localeResolverRepositoryProvider'
|
||||
''
|
||||
'($argument)';
|
||||
}
|
||||
|
||||
@$internal
|
||||
@override
|
||||
LocaleResolverRepository create() => LocaleResolverRepository();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(void value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<void>(value),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is LocaleResolverRepositoryProvider &&
|
||||
other.argument == argument;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return argument.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
String _$localeResolverRepositoryHash() =>
|
||||
r'dc74908d6ac1f5cc851e2a10dcf33b0dc68b5b15';
|
||||
|
||||
final class LocaleResolverRepositoryFamily extends $Family
|
||||
with
|
||||
$ClassFamilyOverride<
|
||||
LocaleResolverRepository,
|
||||
void,
|
||||
void,
|
||||
void,
|
||||
intl.Locale
|
||||
> {
|
||||
LocaleResolverRepositoryFamily._()
|
||||
: super(
|
||||
retry: null,
|
||||
name: r'localeResolverRepositoryProvider',
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
isAutoDispose: false,
|
||||
);
|
||||
|
||||
LocaleResolverRepositoryProvider call(intl.Locale targetLocale) =>
|
||||
LocaleResolverRepositoryProvider._(argument: targetLocale, from: this);
|
||||
|
||||
@override
|
||||
String toString() => r'localeResolverRepositoryProvider';
|
||||
}
|
||||
|
||||
abstract class _$LocaleResolverRepository extends $Notifier<void> {
|
||||
late final _$args = ref.$arg as intl.Locale;
|
||||
intl.Locale get targetLocale => _$args;
|
||||
|
||||
void build(intl.Locale targetLocale);
|
||||
@$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(_$args));
|
||||
}
|
||||
}
|
||||
|
||||
@ProviderFor(resolveLocale)
|
||||
final resolveLocaleProvider = ResolveLocaleFamily._();
|
||||
|
||||
final class ResolveLocaleProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
AsyncValue<LocalizedResult>,
|
||||
LocalizedResult,
|
||||
FutureOr<LocalizedResult>
|
||||
>
|
||||
with $FutureModifier<LocalizedResult>, $FutureProvider<LocalizedResult> {
|
||||
ResolveLocaleProvider._({
|
||||
required ResolveLocaleFamily super.from,
|
||||
required intl.Locale super.argument,
|
||||
}) : super(
|
||||
retry: null,
|
||||
name: r'resolveLocaleProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$resolveLocaleHash();
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return r'resolveLocaleProvider'
|
||||
''
|
||||
'($argument)';
|
||||
}
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$FutureProviderElement<LocalizedResult> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $FutureProviderElement(pointer);
|
||||
|
||||
@override
|
||||
FutureOr<LocalizedResult> create(Ref ref) {
|
||||
final argument = this.argument as intl.Locale;
|
||||
return resolveLocale(ref, argument);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is ResolveLocaleProvider && other.argument == argument;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return argument.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
String _$resolveLocaleHash() => r'94d7a9b307a81de372b8b40b06f046acc76e01c8';
|
||||
|
||||
final class ResolveLocaleFamily extends $Family
|
||||
with $FunctionalFamilyOverride<FutureOr<LocalizedResult>, intl.Locale> {
|
||||
ResolveLocaleFamily._()
|
||||
: super(
|
||||
retry: null,
|
||||
name: r'resolveLocaleProvider',
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
isAutoDispose: true,
|
||||
);
|
||||
|
||||
ResolveLocaleProvider call(intl.Locale locale) =>
|
||||
ResolveLocaleProvider._(argument: locale, from: this);
|
||||
|
||||
@override
|
||||
String toString() => r'resolveLocaleProvider';
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:exceptions/exceptions.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:weblibre/core/providers/format.dart';
|
||||
import 'package:weblibre/features/about/domain/providers.dart';
|
||||
import 'package:weblibre/features/bangs/data/models/bang_group.dart';
|
||||
import 'package:weblibre/features/bangs/domain/repositories/sync.dart';
|
||||
|
||||
part 'app_initialization.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class AppInitializationService extends _$AppInitializationService {
|
||||
/// Will de facto restart the app
|
||||
Future<void> reinitialize() {
|
||||
ref.invalidateSelf();
|
||||
return initialize();
|
||||
}
|
||||
|
||||
Future<void> _initPackageInfo() {
|
||||
//Ensure Package info is loaded
|
||||
state = Result.success((
|
||||
initialized: false,
|
||||
stage: 'Loading Package Info...',
|
||||
errors: List.empty(),
|
||||
));
|
||||
|
||||
return ref.read(packageInfoProvider.future);
|
||||
}
|
||||
|
||||
Future<Map<BangGroup, Result<void>>> _initBangs() {
|
||||
state = Result.success((
|
||||
initialized: false,
|
||||
stage: 'Synchronizing Bangs...',
|
||||
errors: List.empty(),
|
||||
));
|
||||
|
||||
return ref
|
||||
.read(bangSyncRepositoryProvider.notifier)
|
||||
.syncBundledBangGroups();
|
||||
}
|
||||
|
||||
Future<void> initialize() async {
|
||||
state = await Result.fromAsync(() async {
|
||||
final errors = <ErrorMessage>[];
|
||||
|
||||
await ref.read(formatProvider.future);
|
||||
if (!ref.mounted) {
|
||||
return (initialized: false, stage: null, errors: errors);
|
||||
}
|
||||
|
||||
await _initPackageInfo();
|
||||
if (!ref.mounted) {
|
||||
return (initialized: false, stage: null, errors: errors);
|
||||
}
|
||||
|
||||
final bangSyncResults = await _initBangs();
|
||||
for (final MapEntry(value: result) in bangSyncResults.entries) {
|
||||
result.onFailure(errors.add);
|
||||
}
|
||||
|
||||
return (initialized: true, stage: null, errors: errors);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Result<({bool initialized, String? stage, List<ErrorMessage> errors})>
|
||||
build() {
|
||||
return Result.success((
|
||||
initialized: false,
|
||||
stage: null,
|
||||
errors: List.empty(),
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'app_initialization.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(AppInitializationService)
|
||||
final appInitializationServiceProvider = AppInitializationServiceProvider._();
|
||||
|
||||
final class AppInitializationServiceProvider
|
||||
extends
|
||||
$NotifierProvider<
|
||||
AppInitializationService,
|
||||
Result<({List<ErrorMessage> errors, bool initialized, String? stage})>
|
||||
> {
|
||||
AppInitializationServiceProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'appInitializationServiceProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$appInitializationServiceHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
AppInitializationService create() => AppInitializationService();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(
|
||||
Result<({List<ErrorMessage> errors, bool initialized, String? stage})>
|
||||
value,
|
||||
) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride:
|
||||
$SyncValueProvider<
|
||||
Result<
|
||||
({List<ErrorMessage> errors, bool initialized, String? stage})
|
||||
>
|
||||
>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$appInitializationServiceHash() =>
|
||||
r'c26f968d53b4ea18f7be890c9610c9bf9d300322';
|
||||
|
||||
abstract class _$AppInitializationService
|
||||
extends
|
||||
$Notifier<
|
||||
Result<({List<ErrorMessage> errors, bool initialized, String? stage})>
|
||||
> {
|
||||
Result<({List<ErrorMessage> errors, bool initialized, String? stage})>
|
||||
build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final ref =
|
||||
this.ref
|
||||
as $Ref<
|
||||
Result<
|
||||
({List<ErrorMessage> errors, bool initialized, String? stage})
|
||||
>,
|
||||
Result<
|
||||
({List<ErrorMessage> errors, bool initialized, String? stage})
|
||||
>
|
||||
>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<
|
||||
Result<
|
||||
({List<ErrorMessage> errors, bool initialized, String? stage})
|
||||
>,
|
||||
Result<
|
||||
({List<ErrorMessage> errors, bool initialized, String? stage})
|
||||
>
|
||||
>,
|
||||
Result<
|
||||
({List<ErrorMessage> errors, bool initialized, String? stage})
|
||||
>,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleCreate(ref, build);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,407 @@
|
||||
/*
|
||||
* 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 'dart:ui';
|
||||
|
||||
import 'package:exceptions/exceptions.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
||||
import 'package:html/dom.dart';
|
||||
import 'package:html/parser.dart' as html_parser;
|
||||
import 'package:http/io_client.dart';
|
||||
import 'package:nullability/nullability.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:socks5_proxy/socks_client.dart';
|
||||
|
||||
import 'package:weblibre/core/http_error_handler.dart';
|
||||
import 'package:weblibre/data/models/web_page_info.dart';
|
||||
import 'package:weblibre/extensions/http_encoding.dart';
|
||||
import 'package:weblibre/extensions/uri.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/entities/browser_icon.dart';
|
||||
import 'package:weblibre/features/user/domain/repositories/cache.dart';
|
||||
import 'package:weblibre/features/web_feed/utils/feed_finder.dart';
|
||||
import 'package:weblibre/utils/lru_cache.dart';
|
||||
|
||||
part 'generic_website.g.dart';
|
||||
|
||||
const _typeMap = {
|
||||
"manifest": IconType.manifestIcon,
|
||||
"icon": IconType.favicon,
|
||||
"shortcut icon": IconType.favicon,
|
||||
"fluid-icon": IconType.fluidIcon,
|
||||
"apple-touch-icon": IconType.appleTouchIcon,
|
||||
"image_src": IconType.imageSrc,
|
||||
"apple-touch-icon image_src": IconType.appleTouchIcon,
|
||||
"apple-touch-icon-precomposed": IconType.appleTouchIcon,
|
||||
"og:image": IconType.openGraph,
|
||||
"og:image:url": IconType.openGraph,
|
||||
"og:image:secure_url": IconType.openGraph,
|
||||
"twitter:image": IconType.twitter,
|
||||
"msapplication-TileImage": IconType.microsoftTile,
|
||||
};
|
||||
|
||||
final class _InFlightFetch {
|
||||
final Future<Result<WebPageInfo>> future;
|
||||
|
||||
const _InFlightFetch(this.future);
|
||||
}
|
||||
|
||||
Iterable<ResourceSize> sizesToList(String? sizes) sync* {
|
||||
if (sizes != null) {
|
||||
final splitted = sizes
|
||||
.split(' ')
|
||||
.where((size) => size.contains('x'))
|
||||
.toList();
|
||||
|
||||
for (final size in splitted) {
|
||||
final dimensions = size.split('x');
|
||||
if (dimensions.length == 2) {
|
||||
final height = int.tryParse(dimensions[0]);
|
||||
final width = int.tryParse(dimensions[1]);
|
||||
|
||||
if (width != null && height != null) {
|
||||
yield ResourceSize(height: height, width: width);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class GenericWebsiteService extends _$GenericWebsiteService {
|
||||
final GeckoIconService _iconsService;
|
||||
|
||||
late CacheRepository _cacheRepository;
|
||||
late final LRUCache<String, BrowserIcon> _browserIconCache;
|
||||
final _inFlightFetches = <Uri, _InFlightFetch>{};
|
||||
|
||||
GenericWebsiteService()
|
||||
: _iconsService = GeckoIconService(),
|
||||
_browserIconCache = LRUCache(50, onEvict: (icon) => icon.image.dispose());
|
||||
|
||||
@override
|
||||
void build() {
|
||||
_cacheRepository = ref.watch(cacheRepositoryProvider.notifier);
|
||||
}
|
||||
|
||||
static Map<String, dynamic> _serializeResource(Resource resource) {
|
||||
return {
|
||||
'url': resource.url,
|
||||
'type': resource.type,
|
||||
'sizes': resource.sizes.nonNulls.map((s) => [s.height, s.width]).toList(),
|
||||
'mimeType': resource.mimeType,
|
||||
'maskable': resource.maskable,
|
||||
};
|
||||
}
|
||||
|
||||
static Resource _deserializeResource(Map<String, dynamic> resource) {
|
||||
return Resource(
|
||||
url: resource['url'] as String,
|
||||
type: resource['type'] as IconType,
|
||||
mimeType: resource['mimeType'] as String?,
|
||||
sizes: (resource['sizes'] as List<List<int>>)
|
||||
.map((s) => ResourceSize(height: s[0], width: s[1]))
|
||||
.toList(),
|
||||
maskable: resource['maskable'] as bool,
|
||||
);
|
||||
}
|
||||
|
||||
static Uri _resolveRelativeUri(Uri baseUri, Uri uri) {
|
||||
if (!uri.isAbsolute) {
|
||||
return baseUri.resolveUri(uri);
|
||||
}
|
||||
return uri;
|
||||
}
|
||||
|
||||
static bool _isHttpUrl(Uri url) => url.isHttpOrHttps;
|
||||
|
||||
static List<Resource> _extractIcons(Uri baseUrl, Document document) {
|
||||
final List<Resource> icons = [];
|
||||
|
||||
void collectLinkIcons(String rel) {
|
||||
final links = document.querySelectorAll('link[rel="$rel"]');
|
||||
for (final link in links) {
|
||||
final href = link.attributes['href'];
|
||||
final type = _typeMap[rel];
|
||||
final mimeType = link.attributes['type'];
|
||||
if (href != null && type != null) {
|
||||
if (Uri.tryParse(href) case final Uri url) {
|
||||
icons.add(
|
||||
Resource(
|
||||
url: _resolveRelativeUri(baseUrl, url).toString(),
|
||||
type: type,
|
||||
sizes: sizesToList(link.attributes['sizes']).toList(),
|
||||
mimeType: mimeType.isNotEmpty ? mimeType : null,
|
||||
maskable: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void collectMetaPropertyIcons(String property) {
|
||||
final metas = document.querySelectorAll('meta[property="$property"]');
|
||||
for (final meta in metas) {
|
||||
final content = meta.attributes['content'];
|
||||
final type = _typeMap[property];
|
||||
if (content != null && type != null) {
|
||||
if (Uri.tryParse(content) case final Uri url) {
|
||||
icons.add(
|
||||
Resource(
|
||||
type: type,
|
||||
url: _resolveRelativeUri(baseUrl, url).toString(),
|
||||
sizes: [],
|
||||
maskable: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void collectMetaNameIcons(String name) {
|
||||
final metas = document.querySelectorAll('meta[name="$name"]');
|
||||
for (final meta in metas) {
|
||||
final content = meta.attributes['content'];
|
||||
final type = _typeMap[name];
|
||||
if (content != null && type != null) {
|
||||
if (Uri.tryParse(content) case final Uri url) {
|
||||
icons.add(
|
||||
Resource(
|
||||
type: type,
|
||||
url: _resolveRelativeUri(baseUrl, url).toString(),
|
||||
sizes: [],
|
||||
maskable: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
collectLinkIcons("icon");
|
||||
collectLinkIcons("shortcut icon");
|
||||
collectLinkIcons("fluid-icon");
|
||||
collectLinkIcons("apple-touch-icon");
|
||||
collectLinkIcons("image_src");
|
||||
collectLinkIcons("apple-touch-icon image_src");
|
||||
collectLinkIcons("apple-touch-icon-precomposed");
|
||||
|
||||
collectMetaPropertyIcons("og:image");
|
||||
collectMetaPropertyIcons("og:image:url");
|
||||
collectMetaPropertyIcons("og:image:secure_url");
|
||||
|
||||
collectMetaNameIcons("twitter:image");
|
||||
collectMetaNameIcons("msapplication-TileImage");
|
||||
|
||||
return icons;
|
||||
}
|
||||
|
||||
Future<Result<WebPageInfo>> fetchPageInfo({
|
||||
required Uri url,
|
||||
required bool isImageRequest,
|
||||
required int? proxyPort,
|
||||
}) {
|
||||
return Result.fromAsync(() async {
|
||||
final result = await compute((args) async {
|
||||
final [String urlString, bool isImageRequest, int? proxyPort] = args;
|
||||
|
||||
final httpClient = HttpClient();
|
||||
if (proxyPort != null) {
|
||||
SocksTCPClient.assignToHttpClient(httpClient, [
|
||||
ProxySettings(InternetAddress.loopbackIPv4, proxyPort),
|
||||
]);
|
||||
}
|
||||
|
||||
final client = IOClient(httpClient);
|
||||
try {
|
||||
final baseUri = Uri.parse(urlString);
|
||||
final response = await client
|
||||
.get(baseUri)
|
||||
.timeout(const Duration(seconds: 15));
|
||||
|
||||
//When this is a request for an icon and we hit an image, directly return it
|
||||
if (isImageRequest) {
|
||||
final contentType = response.headers['content-type'];
|
||||
if (contentType?.contains('image/') == true) {
|
||||
return {
|
||||
'imageBytes': [response.bodyBytes],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
final document = html_parser.parse(response.bodyUnicodeFallback);
|
||||
|
||||
final title = document.querySelector('title')?.text;
|
||||
final resources = _extractIcons(baseUri, document);
|
||||
final feeds = await FeedFinder(
|
||||
url: baseUri,
|
||||
document: document,
|
||||
).parse();
|
||||
|
||||
return {
|
||||
'title': title,
|
||||
'resources': resources.map(_serializeResource).toList(),
|
||||
'feeds': feeds.map((uri) => uri.toString()).toList(),
|
||||
};
|
||||
} finally {
|
||||
client.close();
|
||||
}
|
||||
}, <dynamic>[url.toString(), isImageRequest, proxyPort]);
|
||||
|
||||
if (result['imageBytes'] case final Uint8List imageBytes) {
|
||||
return WebPageInfo(
|
||||
url: url,
|
||||
favicon: await BrowserIcon.fromBytes(
|
||||
imageBytes,
|
||||
dominantColor: null,
|
||||
source: IconSource.download,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final resources = (result['resources']! as List<Map<String, dynamic>>)
|
||||
.map(_deserializeResource)
|
||||
.toList();
|
||||
|
||||
final favicon =
|
||||
await getCachedIcon(url) ??
|
||||
await loadIcon(url: url, resources: resources);
|
||||
|
||||
return WebPageInfo(
|
||||
url: url,
|
||||
title: (result['title'] as String?)?.trim(),
|
||||
favicon: favicon,
|
||||
feeds: Set.from(
|
||||
(result['feeds']! as List<String>).map((url) => Uri.tryParse(url)),
|
||||
),
|
||||
);
|
||||
}, exceptionHandler: handleHttpError);
|
||||
}
|
||||
|
||||
Future<BrowserIcon?> getCachedIcon(Uri url) async {
|
||||
if (_isHttpUrl(url)) {
|
||||
final cachedBrowserIcon = _browserIconCache.get(url.origin);
|
||||
if (cachedBrowserIcon?.image.value != null) {
|
||||
return cachedBrowserIcon;
|
||||
} else if (cachedBrowserIcon != null) {
|
||||
_browserIconCache.remove(url.origin);
|
||||
}
|
||||
|
||||
final cachedIcon = await _cacheRepository.getCachedIcon(url.origin);
|
||||
if (cachedIcon != null) {
|
||||
return _browserIconCache.set(
|
||||
url.origin,
|
||||
await BrowserIcon.fromBytes(
|
||||
cachedIcon,
|
||||
dominantColor: null,
|
||||
source: IconSource.disk,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<BrowserIcon> loadIcon({
|
||||
required Uri url,
|
||||
required List<Resource> resources,
|
||||
bool isPrivate = false,
|
||||
bool waitOnNetworkLoad = true,
|
||||
}) async {
|
||||
final result = await _iconsService.loadIcon(
|
||||
url: url,
|
||||
resources: resources,
|
||||
isPrivate: isPrivate,
|
||||
waitOnNetworkLoad: waitOnNetworkLoad,
|
||||
);
|
||||
|
||||
if (result.source != IconSource.generator &&
|
||||
result.source != IconSource.memory) {
|
||||
await _cacheRepository.cacheIcon(url, result.image);
|
||||
}
|
||||
|
||||
return _browserIconCache.set(
|
||||
url.origin,
|
||||
await BrowserIcon.fromBytes(
|
||||
result.image,
|
||||
dominantColor: result.color.mapNotNull((color) => Color(color)),
|
||||
source: result.source,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<Result<WebPageInfo>> _deduplicatedFetchPageInfo(Uri url) {
|
||||
final existing = _inFlightFetches[url];
|
||||
if (existing != null) {
|
||||
return existing.future;
|
||||
}
|
||||
|
||||
late final Future<Result<WebPageInfo>> inFlightFetch;
|
||||
inFlightFetch = fetchPageInfo(url: url, isImageRequest: true, proxyPort: null)
|
||||
.timeout(
|
||||
const Duration(seconds: 20),
|
||||
onTimeout: () => Result.failure(
|
||||
const ErrorMessage(source: 'icon', message: 'Icon fetch timeout'),
|
||||
),
|
||||
)
|
||||
.whenComplete(() {
|
||||
// Only clear this entry if it is still the active in-flight request.
|
||||
if (identical(_inFlightFetches[url]?.future, inFlightFetch)) {
|
||||
_inFlightFetches.remove(url);
|
||||
}
|
||||
});
|
||||
|
||||
_inFlightFetches[url] = _InFlightFetch(inFlightFetch);
|
||||
return inFlightFetch;
|
||||
}
|
||||
|
||||
Future<BrowserIcon?> getUrlIcon(List<Uri> urlList) async {
|
||||
for (final url in urlList) {
|
||||
if (!_isHttpUrl(url)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final cachedIcon = await getCachedIcon(url);
|
||||
|
||||
if (cachedIcon != null) {
|
||||
return cachedIcon;
|
||||
}
|
||||
|
||||
if (ref.mounted) {
|
||||
final result = await _deduplicatedFetchPageInfo(url);
|
||||
|
||||
if (result.isSuccess) {
|
||||
if (result.value.favicon case final BrowserIcon favicon) {
|
||||
if (!_browserIconCache.contains(url.origin)) {
|
||||
_browserIconCache.set(url.origin, favicon);
|
||||
}
|
||||
|
||||
return favicon;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'generic_website.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(GenericWebsiteService)
|
||||
final genericWebsiteServiceProvider = GenericWebsiteServiceProvider._();
|
||||
|
||||
final class GenericWebsiteServiceProvider
|
||||
extends $NotifierProvider<GenericWebsiteService, void> {
|
||||
GenericWebsiteServiceProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'genericWebsiteServiceProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$genericWebsiteServiceHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
GenericWebsiteService create() => GenericWebsiteService();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(void value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<void>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$genericWebsiteServiceHash() =>
|
||||
r'a2bd892f0ca07467eaa906648e57f232a7e50155';
|
||||
|
||||
abstract class _$GenericWebsiteService 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
extension DateTimeFormat on DateTime {
|
||||
String _twoDigits(int n) => n.toString().padLeft(2, '0');
|
||||
|
||||
String formatWithMinutePrecision() {
|
||||
final year = this.year.toString();
|
||||
final month = _twoDigits(this.month);
|
||||
final day = _twoDigits(this.day);
|
||||
final hour = _twoDigits(this.hour);
|
||||
final minute = _twoDigits(this.minute);
|
||||
|
||||
return '$year-$month-$day $hour:$minute';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
extension DurationX on Duration {
|
||||
String formatTimeZoneOffset() {
|
||||
final hours = inHours.abs().toString().padLeft(2, '0');
|
||||
final minutes = (inMinutes.abs() % 60).toString().padLeft(2, '0');
|
||||
final sign = isNegative ? '-' : '+';
|
||||
|
||||
return '$sign$hours$minutes';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:http/http.dart';
|
||||
|
||||
/// This is modified from default latin1 fallback to use utf8
|
||||
Encoding _encodingForHeaders(Map<String, String> headers) =>
|
||||
_encodingForContentTypeHeader(_contentTypeForHeaders(headers), utf8);
|
||||
|
||||
/// Returns the [MediaType] object for the given headers' content-type.
|
||||
///
|
||||
/// Defaults to `application/octet-stream`.
|
||||
MediaType _contentTypeForHeaders(Map<String, String> headers) {
|
||||
final contentType = headers['content-type'];
|
||||
if (contentType != null) return MediaType.parse(contentType);
|
||||
return MediaType('application', 'octet-stream');
|
||||
}
|
||||
|
||||
Encoding _encodingForContentTypeHeader(
|
||||
MediaType contentTypeHeader, [
|
||||
Encoding fallback = latin1,
|
||||
]) {
|
||||
final charset = contentTypeHeader.parameters['charset'];
|
||||
|
||||
// Default to utf8 for application/json when charset is unspecified.
|
||||
if (contentTypeHeader.type == 'application' &&
|
||||
contentTypeHeader.subtype == 'json' &&
|
||||
charset == null) {
|
||||
return utf8;
|
||||
}
|
||||
|
||||
// Attempt to find the encoding or fall back to the default.
|
||||
return charset != null ? Encoding.getByName(charset) ?? fallback : fallback;
|
||||
}
|
||||
|
||||
extension ResponesEncoding on Response {
|
||||
String get bodyUnicodeFallback =>
|
||||
_encodingForHeaders(headers).decode(bodyBytes);
|
||||
}
|
||||
@@ -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 'dart:ui';
|
||||
|
||||
import 'package:fast_equatable/hash.dart';
|
||||
|
||||
extension ImageHash on Image {
|
||||
Future<int?> calculateHash() async {
|
||||
final byteData = await toByteData();
|
||||
if (byteData != null) {
|
||||
final digest = secureHash(byteData);
|
||||
return digest;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Future<EquatableImage> toEquatable() {
|
||||
// return EquatableImage.calculate(this);
|
||||
// }
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
extension UniqueItems<T> on Iterable<T> {
|
||||
Iterable<T> findDuplicates() sync* {
|
||||
final seen = <T>{};
|
||||
|
||||
for (final item in this) {
|
||||
if (seen.contains(item)) {
|
||||
yield item;
|
||||
} else {
|
||||
seen.add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* 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:ui' as ui;
|
||||
|
||||
import 'package:intl/locale.dart' as intl;
|
||||
|
||||
extension LocaleFormat on intl.Locale {
|
||||
String rawToString(String separator) {
|
||||
final StringBuffer out = StringBuffer(languageCode);
|
||||
if (scriptCode != null && scriptCode!.isNotEmpty) {
|
||||
out.write('$separator$scriptCode');
|
||||
}
|
||||
final String? countryCode = this.countryCode;
|
||||
if (countryCode != null && countryCode.isNotEmpty) {
|
||||
out.write('$separator${this.countryCode}');
|
||||
}
|
||||
return out.toString();
|
||||
}
|
||||
}
|
||||
|
||||
extension LocaleConverter on ui.Locale {
|
||||
intl.Locale toIntlLocale() {
|
||||
return intl.Locale.fromSubtags(
|
||||
languageCode: languageCode,
|
||||
countryCode: countryCode,
|
||||
scriptCode: scriptCode,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
extension RelativeSafeArea on MediaQueryData {
|
||||
double relativeSafeArea() {
|
||||
return 1.0 - (padding.top / size.height);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:hooks_riverpod/misc.dart';
|
||||
import 'package:riverpod/riverpod.dart';
|
||||
|
||||
extension CacheForExtension on Ref {
|
||||
/// Keeps the provider alive for [duration] after the last listener is removed.
|
||||
KeepAliveLink cacheFor(Duration duration) {
|
||||
Timer? timer;
|
||||
final link = keepAlive();
|
||||
|
||||
onCancel(() {
|
||||
// All listeners are gone — start the dispose timer.
|
||||
timer = Timer(duration, link.close);
|
||||
});
|
||||
|
||||
onResume(() {
|
||||
// A new listener was added — cancel the timer.
|
||||
timer?.cancel();
|
||||
});
|
||||
|
||||
onDispose(() {
|
||||
// Provider is being fully disposed — clean up.
|
||||
timer?.cancel();
|
||||
});
|
||||
|
||||
return link;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
extension StringExtension on String {
|
||||
String toCapitalized() =>
|
||||
isEmpty ? this : '${this[0].toUpperCase()}${substring(1)}';
|
||||
|
||||
bool startsWithIgnoreCase(String prefix) =>
|
||||
toLowerCase().startsWith(prefix.toLowerCase());
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'package:weblibre/utils/uri_policy.dart';
|
||||
|
||||
extension UriX on Uri {
|
||||
Uri get base => Uri.parse('$scheme://$authority');
|
||||
|
||||
bool get hasSupportedScheme =>
|
||||
allSupportedSchemes.any((s) => s.name == scheme);
|
||||
|
||||
bool get isHttp => isScheme('http');
|
||||
bool get isHttps => isScheme('https');
|
||||
bool get isHttpOrHttps => isHttp || isHttps;
|
||||
|
||||
bool get isLocalhost => host == 'localhost' || host == '127.0.0.1';
|
||||
|
||||
/// Removes a bare root path (`/`) when there is no query or fragment, so
|
||||
/// that `https://example.com/` and `https://example.com` are treated as
|
||||
/// equivalent.
|
||||
Uri get normalized {
|
||||
if (path == '/' && !hasQuery && !hasFragment) {
|
||||
return replace(path: '');
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'providers.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
Future<PackageInfo> packageInfo(Ref ref) async {
|
||||
return await PackageInfo.fromPlatform();
|
||||
}
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
Future<String> geckoVersion(Ref ref) async {
|
||||
return await GeckoBrowserService().getGeckoVersion();
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
// 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(packageInfo)
|
||||
final packageInfoProvider = PackageInfoProvider._();
|
||||
|
||||
final class PackageInfoProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
AsyncValue<PackageInfo>,
|
||||
PackageInfo,
|
||||
FutureOr<PackageInfo>
|
||||
>
|
||||
with $FutureModifier<PackageInfo>, $FutureProvider<PackageInfo> {
|
||||
PackageInfoProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'packageInfoProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$packageInfoHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$FutureProviderElement<PackageInfo> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $FutureProviderElement(pointer);
|
||||
|
||||
@override
|
||||
FutureOr<PackageInfo> create(Ref ref) {
|
||||
return packageInfo(ref);
|
||||
}
|
||||
}
|
||||
|
||||
String _$packageInfoHash() => r'44d37547139567a5f03c1942c1d62ff1abb07248';
|
||||
|
||||
@ProviderFor(geckoVersion)
|
||||
final geckoVersionProvider = GeckoVersionProvider._();
|
||||
|
||||
final class GeckoVersionProvider
|
||||
extends $FunctionalProvider<AsyncValue<String>, String, FutureOr<String>>
|
||||
with $FutureModifier<String>, $FutureProvider<String> {
|
||||
GeckoVersionProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'geckoVersionProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$geckoVersionHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$FutureProviderElement<String> $createElement($ProviderPointer pointer) =>
|
||||
$FutureProviderElement(pointer);
|
||||
|
||||
@override
|
||||
FutureOr<String> create(Ref ref) {
|
||||
return geckoVersion(ref);
|
||||
}
|
||||
}
|
||||
|
||||
String _$geckoVersionHash() => r'1cdaaebc674da967a2765c5bf85a7442425c47ce';
|
||||
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:weblibre/core/providers/defaults.dart';
|
||||
import 'package:weblibre/core/routing/routes.dart';
|
||||
import 'package:weblibre/features/about/domain/providers.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/repositories/tab.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_mode.dart';
|
||||
|
||||
class AboutDialogScreen extends HookConsumerWidget {
|
||||
const AboutDialogScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final packageInfo = ref.watch(
|
||||
packageInfoProvider.select(
|
||||
//During startup we make sure
|
||||
(value) => value.value!,
|
||||
),
|
||||
);
|
||||
|
||||
return AboutDialog(
|
||||
applicationIcon: SizedBox.square(
|
||||
dimension: IconTheme.of(context).size,
|
||||
child: SvgPicture.asset('assets/icon/icon.svg'),
|
||||
),
|
||||
applicationName: packageInfo.appName,
|
||||
applicationVersion: packageInfo.version,
|
||||
applicationLegalese: 'Copyright © Fabian Freund, 2024-2026',
|
||||
children: [
|
||||
ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: const Text('Gecko Version'),
|
||||
subtitle: Consumer(
|
||||
builder: (context, ref, child) {
|
||||
final geckoVersion = ref.watch(geckoVersionProvider);
|
||||
|
||||
return Text(geckoVersion.value ?? 'N/A');
|
||||
},
|
||||
),
|
||||
),
|
||||
const Divider(),
|
||||
ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: const Icon(MdiIcons.charity),
|
||||
title: const Text('Feedback'),
|
||||
onTap: () async {
|
||||
await ref
|
||||
.read(tabRepositoryProvider.notifier)
|
||||
.addTab(
|
||||
url: Uri.https('feedback.weblibre.eu'),
|
||||
tabMode: TabMode.regular,
|
||||
selectTab: true,
|
||||
);
|
||||
|
||||
if (context.mounted) {
|
||||
const BrowserRoute().go(context);
|
||||
}
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: const Icon(MdiIcons.handHeart),
|
||||
title: const Text('Donate'),
|
||||
onTap: () async {
|
||||
await ref
|
||||
.read(tabRepositoryProvider.notifier)
|
||||
.addTab(
|
||||
url: Uri.https('github.com').replace(path: 'FaFre/WebLibre'),
|
||||
tabMode: TabMode.regular,
|
||||
selectTab: true,
|
||||
);
|
||||
|
||||
if (context.mounted) {
|
||||
const BrowserRoute().go(context);
|
||||
}
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
// ignore: deprecated_member_use
|
||||
leading: const Icon(Icons.book),
|
||||
title: const Text('Documentation'),
|
||||
onTap: () async {
|
||||
await ref
|
||||
.read(tabRepositoryProvider.notifier)
|
||||
.addTab(
|
||||
url: ref.read(docsUriProvider),
|
||||
tabMode: TabMode.regular,
|
||||
selectTab: true,
|
||||
);
|
||||
|
||||
if (context.mounted) {
|
||||
const BrowserRoute().go(context);
|
||||
}
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
// ignore: deprecated_member_use
|
||||
leading: const Icon(MdiIcons.github),
|
||||
title: const Text('Github'),
|
||||
onTap: () async {
|
||||
await ref
|
||||
.read(tabRepositoryProvider.notifier)
|
||||
.addTab(
|
||||
url: Uri.https('github.com').replace(path: 'FaFre/WebLibre'),
|
||||
tabMode: TabMode.regular,
|
||||
selectTab: true,
|
||||
);
|
||||
|
||||
if (context.mounted) {
|
||||
const BrowserRoute().go(context);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* 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:home_widget/home_widget.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:rxdart/rxdart.dart';
|
||||
import 'package:weblibre/data/models/received_intent_parameter.dart';
|
||||
|
||||
part 'home_widget.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
Future<bool> widgetPinnable(Ref ref) async {
|
||||
return await HomeWidget.isRequestPinWidgetSupported() ?? false;
|
||||
}
|
||||
|
||||
@Riverpod()
|
||||
Raw<Stream<ReceivedIntentParameter>> appWidgetLaunchStream(Ref ref) {
|
||||
final initialStream = HomeWidget.initiallyLaunchedFromHomeWidget().asStream();
|
||||
|
||||
return ConcatStream([
|
||||
initialStream,
|
||||
HomeWidget.widgetClicked,
|
||||
]).whereNotNull().map((uri) => ReceivedIntentParameter(null, uri.host));
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'home_widget.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(widgetPinnable)
|
||||
final widgetPinnableProvider = WidgetPinnableProvider._();
|
||||
|
||||
final class WidgetPinnableProvider
|
||||
extends $FunctionalProvider<AsyncValue<bool>, bool, FutureOr<bool>>
|
||||
with $FutureModifier<bool>, $FutureProvider<bool> {
|
||||
WidgetPinnableProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'widgetPinnableProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$widgetPinnableHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$FutureProviderElement<bool> $createElement($ProviderPointer pointer) =>
|
||||
$FutureProviderElement(pointer);
|
||||
|
||||
@override
|
||||
FutureOr<bool> create(Ref ref) {
|
||||
return widgetPinnable(ref);
|
||||
}
|
||||
}
|
||||
|
||||
String _$widgetPinnableHash() => r'3181e5e3e69e7e796e6429ca7507bbbc239f6c21';
|
||||
|
||||
@ProviderFor(appWidgetLaunchStream)
|
||||
final appWidgetLaunchStreamProvider = AppWidgetLaunchStreamProvider._();
|
||||
|
||||
final class AppWidgetLaunchStreamProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
Raw<Stream<ReceivedIntentParameter>>,
|
||||
Raw<Stream<ReceivedIntentParameter>>,
|
||||
Raw<Stream<ReceivedIntentParameter>>
|
||||
>
|
||||
with $Provider<Raw<Stream<ReceivedIntentParameter>>> {
|
||||
AppWidgetLaunchStreamProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'appWidgetLaunchStreamProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$appWidgetLaunchStreamHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<Raw<Stream<ReceivedIntentParameter>>> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
Raw<Stream<ReceivedIntentParameter>> create(Ref ref) {
|
||||
return appWidgetLaunchStream(ref);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(Raw<Stream<ReceivedIntentParameter>> value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride:
|
||||
$SyncValueProvider<Raw<Stream<ReceivedIntentParameter>>>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$appWidgetLaunchStreamHash() =>
|
||||
r'ed042d1391ae3dbc26f806d6fa2e9a4a2464ae44';
|
||||
@@ -0,0 +1,169 @@
|
||||
/*
|
||||
* 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:nullability/nullability.dart';
|
||||
import 'package:weblibre/features/bangs/data/database/daos/bang.drift.dart';
|
||||
import 'package:weblibre/features/bangs/data/database/database.dart';
|
||||
import 'package:weblibre/features/bangs/data/database/definitions.drift.dart';
|
||||
import 'package:weblibre/features/bangs/data/models/bang.dart';
|
||||
import 'package:weblibre/features/bangs/data/models/bang_data.dart';
|
||||
import 'package:weblibre/features/bangs/data/models/bang_group.dart';
|
||||
import 'package:weblibre/features/bangs/data/models/bang_key.dart';
|
||||
|
||||
@DriftAccessor()
|
||||
class BangDao extends DatabaseAccessor<BangDatabase> with $BangDaoMixin {
|
||||
BangDao(super.db);
|
||||
|
||||
Selectable<Bang> getBangList({Iterable<BangGroup>? groups}) {
|
||||
final selectable = select(db.bang);
|
||||
if (groups != null) {
|
||||
selectable.where((t) => t.group.isInValues(groups));
|
||||
}
|
||||
|
||||
return selectable;
|
||||
}
|
||||
|
||||
SingleSelectable<int> getBangCount({Iterable<BangGroup>? groups}) {
|
||||
return db.bang.count(
|
||||
where: groups.mapNotNull(
|
||||
(groups) =>
|
||||
(t) => t.group.isInValues(groups),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<int> upsertBang(Bang bang) {
|
||||
return db.bang.insertOne(bang, mode: InsertMode.insertOrReplace);
|
||||
}
|
||||
|
||||
SingleOrNullSelectable<BangData> getBangData(
|
||||
BangGroup group,
|
||||
String trigger,
|
||||
) {
|
||||
return select(db.bangDataView)
|
||||
..where((t) => t.group.equalsValue(group) & t.trigger.equals(trigger));
|
||||
}
|
||||
|
||||
Selectable<BangData> getBangDataList({
|
||||
Iterable<String>? triggers,
|
||||
Iterable<BangGroup>? groups,
|
||||
String? domain,
|
||||
String? category,
|
||||
String? subCategory,
|
||||
bool? orderMostFrequentFirst,
|
||||
}) {
|
||||
final selectable = select(db.bangDataView);
|
||||
if (triggers != null) {
|
||||
selectable.where((t) => t.trigger.isIn(triggers));
|
||||
}
|
||||
if (groups != null) {
|
||||
selectable.where((t) => t.group.isInValues(groups));
|
||||
}
|
||||
if (domain != null) {
|
||||
selectable.where((t) => t.domain.equals(domain));
|
||||
}
|
||||
if (category != null) {
|
||||
selectable.where((t) => t.category.equals(category));
|
||||
|
||||
if (subCategory != null) {
|
||||
selectable.where((t) => t.subCategory.equals(subCategory));
|
||||
}
|
||||
}
|
||||
|
||||
selectable.orderBy([
|
||||
if (orderMostFrequentFirst == true) (t) => OrderingTerm.desc(t.frequency),
|
||||
(t) => OrderingTerm.asc(t.websiteName),
|
||||
]);
|
||||
|
||||
return selectable;
|
||||
}
|
||||
|
||||
Selectable<BangData> getFrequentBangDataList({Iterable<BangGroup>? groups}) {
|
||||
final selectable = select(db.bangDataView)
|
||||
..where((t) => t.frequency.isBiggerThanValue(0));
|
||||
|
||||
if (groups != null) {
|
||||
selectable.where((t) => t.group.isInValues(groups));
|
||||
}
|
||||
|
||||
selectable.orderBy([
|
||||
(t) => OrderingTerm.desc(t.frequency),
|
||||
(t) => OrderingTerm.desc(t.lastUsed),
|
||||
]);
|
||||
|
||||
return selectable;
|
||||
}
|
||||
|
||||
Future<int> increaseBangFrequency(BangKey key) {
|
||||
return db.bangFrequency.insertOne(
|
||||
BangFrequencyCompanion.insert(
|
||||
trigger: key.trigger,
|
||||
group: key.group,
|
||||
frequency: 1,
|
||||
lastUsed: DateTime.now(),
|
||||
),
|
||||
onConflict: DoUpdate(
|
||||
(old) => BangFrequencyCompanion.custom(
|
||||
frequency: old.frequency + const Constant(1),
|
||||
lastUsed: Variable(DateTime.now()),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Selectable<BangData> queryBangs(String searchString) {
|
||||
final ftsQuery = db.buildFtsQuery(searchString);
|
||||
|
||||
if (ftsQuery.isNotEmpty) {
|
||||
return db.definitionsDrift.queryBangs(query: ftsQuery);
|
||||
} else {
|
||||
return db.definitionsDrift.queryBangsBasic(
|
||||
query: db.buildLikeQuery(searchString),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<int> addSearchEntry(
|
||||
BangGroup group,
|
||||
String trigger,
|
||||
String searchQuery,
|
||||
) {
|
||||
return db.bangHistory.insertOne(
|
||||
BangHistoryCompanion.insert(
|
||||
searchQuery: searchQuery,
|
||||
trigger: trigger,
|
||||
group: group,
|
||||
searchDate: DateTime.now(),
|
||||
),
|
||||
onConflict: DoUpdate(
|
||||
target: [db.bangHistory.searchQuery],
|
||||
(old) => BangHistoryCompanion(
|
||||
trigger: Value(trigger),
|
||||
group: Value(group),
|
||||
searchDate: Value(DateTime.now()),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<int> removeSearchEntry(String searchQuery) {
|
||||
return db.bangHistory.deleteWhere((t) => t.searchQuery.equals(searchQuery));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// dart format width=80
|
||||
// ignore_for_file: type=lint
|
||||
import 'package:drift/drift.dart' as i0;
|
||||
import 'package:weblibre/features/bangs/data/database/database.dart' as i1;
|
||||
|
||||
mixin $BangDaoMixin on i0.DatabaseAccessor<i1.BangDatabase> {
|
||||
BangDaoManager get managers => BangDaoManager(this);
|
||||
}
|
||||
|
||||
class BangDaoManager {
|
||||
final $BangDaoMixin _db;
|
||||
BangDaoManager(this._db);
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* 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/bangs/data/database/daos/sync.drift.dart';
|
||||
import 'package:weblibre/features/bangs/data/database/database.dart';
|
||||
import 'package:weblibre/features/bangs/data/database/definitions.drift.dart';
|
||||
import 'package:weblibre/features/bangs/data/models/bang.dart';
|
||||
import 'package:weblibre/features/bangs/data/models/bang_group.dart';
|
||||
|
||||
@DriftAccessor()
|
||||
class SyncDao extends DatabaseAccessor<BangDatabase> with $SyncDaoMixin {
|
||||
SyncDao(super.db);
|
||||
|
||||
SingleOrNullSelectable<DateTime?> getLastSyncOfGroup(BangGroup group) {
|
||||
final query = selectOnly(db.bangSync)
|
||||
..addColumns([db.bangSync.lastSync])
|
||||
..where(db.bangSync.group.equalsValue(group));
|
||||
|
||||
return query.map((row) => row.read(db.bangSync.lastSync));
|
||||
}
|
||||
|
||||
Future<void> upsertLastSyncOfGroup(BangGroup group, DateTime lastSync) {
|
||||
return db.bangSync.insertOne(
|
||||
BangSyncCompanion.insert(group: Value(group), lastSync: lastSync),
|
||||
onConflict: DoUpdate(
|
||||
(old) => BangSyncCompanion(lastSync: Value(lastSync)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> insertBangs(Iterable<Bang> bangs) {
|
||||
return db.bang.insertAll(bangs);
|
||||
}
|
||||
|
||||
Future<void> replaceBangs(Iterable<Bang> bangs) {
|
||||
return batch((batch) {
|
||||
batch.replaceAll(db.bang, bangs);
|
||||
});
|
||||
}
|
||||
|
||||
Future<int> deleteBangs(BangGroup group, Iterable<String> triggers) {
|
||||
final statement = delete(db.bang)
|
||||
..where((t) => t.group.equalsValue(group) & t.trigger.isIn(triggers));
|
||||
return statement.go();
|
||||
}
|
||||
|
||||
Future<void> syncBangs({
|
||||
required BangGroup group,
|
||||
required Iterable<Bang> remoteBangs,
|
||||
required DateTime syncTime,
|
||||
}) async {
|
||||
final remoteBangMap = Map.fromEntries(
|
||||
remoteBangs.map((e) => MapEntry(e.trigger, e)),
|
||||
);
|
||||
final localBangMap = await db.bangDao
|
||||
.getBangList(groups: [group])
|
||||
.get()
|
||||
.then(
|
||||
(bangs) => Map.fromEntries(bangs.map((e) => MapEntry(e.trigger, e))),
|
||||
);
|
||||
|
||||
final remoteBangTriggers = remoteBangMap.keys.toSet();
|
||||
final localBangTriggers = localBangMap.keys.toSet();
|
||||
|
||||
final removedBangs = localBangTriggers.difference(remoteBangTriggers);
|
||||
final addedBangs = remoteBangTriggers
|
||||
.difference(localBangTriggers)
|
||||
.map((e) => remoteBangMap[e]!);
|
||||
|
||||
final changedBangs = remoteBangTriggers
|
||||
.intersection(localBangTriggers)
|
||||
.where((e) => remoteBangMap[e] != localBangMap[e])
|
||||
.map((e) => remoteBangMap[e]!);
|
||||
|
||||
await db.transaction(() async {
|
||||
await deleteBangs(group, removedBangs);
|
||||
await insertBangs(addedBangs);
|
||||
await replaceBangs(changedBangs);
|
||||
await upsertLastSyncOfGroup(group, syncTime);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// dart format width=80
|
||||
// ignore_for_file: type=lint
|
||||
import 'package:drift/drift.dart' as i0;
|
||||
import 'package:weblibre/features/bangs/data/database/database.dart' as i1;
|
||||
|
||||
mixin $SyncDaoMixin on i0.DatabaseAccessor<i1.BangDatabase> {
|
||||
SyncDaoManager get managers => SyncDaoManager(this);
|
||||
}
|
||||
|
||||
class SyncDaoManager {
|
||||
final $SyncDaoMixin _db;
|
||||
SyncDaoManager(this._db);
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
/*
|
||||
* 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/internal/versioned_schema.dart';
|
||||
import 'package:drift_dev/api/migrations_native.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:weblibre/features/bangs/data/database/daos/bang.dart';
|
||||
import 'package:weblibre/features/bangs/data/database/daos/sync.dart';
|
||||
import 'package:weblibre/features/bangs/data/database/database.drift.dart';
|
||||
import 'package:weblibre/features/bangs/data/database/database.steps.dart';
|
||||
import 'package:weblibre/features/bangs/data/database/definitions.drift.dart';
|
||||
import 'package:weblibre/features/search/domain/fts_tokenizer.dart';
|
||||
|
||||
@DriftDatabase(include: {'definitions.drift'}, daos: [BangDao, SyncDao])
|
||||
class BangDatabase extends $BangDatabase with PrefixQueryBuilderMixin {
|
||||
@override
|
||||
final int schemaVersion = 5;
|
||||
|
||||
@override
|
||||
final int ftsTokenLimit = 6;
|
||||
@override
|
||||
final int ftsMinTokenLength = 2;
|
||||
|
||||
@override
|
||||
MigrationStrategy get migration => MigrationStrategy(
|
||||
onUpgrade: (m, from, to) async {
|
||||
// Following the advice from https://drift.simonbinder.eu/Migrations/api/#general-tips
|
||||
await customStatement('PRAGMA foreign_keys = OFF');
|
||||
|
||||
await transaction(
|
||||
() => VersionedSchema.runMigrationSteps(
|
||||
migrator: m,
|
||||
from: from,
|
||||
to: to,
|
||||
steps: _upgrade,
|
||||
),
|
||||
);
|
||||
|
||||
if (kDebugMode) {
|
||||
final wrongForeignKeys = await customSelect(
|
||||
'PRAGMA foreign_key_check',
|
||||
).get();
|
||||
assert(
|
||||
wrongForeignKeys.isEmpty,
|
||||
'${wrongForeignKeys.map((e) => e.data)}',
|
||||
);
|
||||
}
|
||||
|
||||
await customStatement('PRAGMA foreign_keys = ON');
|
||||
},
|
||||
beforeOpen: (details) async {
|
||||
if (kDebugMode) {
|
||||
// This check pulls in a fair amount of code that's not needed
|
||||
// anywhere else, so we recommend only doing it in debug builds.
|
||||
await validateDatabaseSchema();
|
||||
}
|
||||
|
||||
if (details.hadUpgrade && details.versionBefore != null) {
|
||||
await customStatement('PRAGMA foreign_keys = OFF');
|
||||
|
||||
if (details.versionBefore! < 3) {
|
||||
await bang.deleteWhere((t) => t.group.equals(3));
|
||||
await bangTriggers.deleteWhere((t) => t.group.equals(3));
|
||||
await bangSync.deleteWhere((t) => t.group.equals(3));
|
||||
await bangFrequency.deleteWhere((t) => t.group.equals(3));
|
||||
await bangHistory.deleteWhere((t) => t.group.equals(3));
|
||||
} else if (details.versionBefore! < 5) {
|
||||
await bang.deleteWhere((t) => t.group.equals(1));
|
||||
await bangTriggers.deleteWhere((t) => t.group.equals(1));
|
||||
await bangSync.deleteWhere((t) => t.group.equals(1));
|
||||
await bangFrequency.deleteWhere((t) => t.group.equals(1));
|
||||
await bangHistory.deleteWhere((t) => t.group.equals(1));
|
||||
|
||||
await (bang.update()..where((t) => t.group.isBiggerThanValue(0)))
|
||||
.write(
|
||||
BangCompanion.custom(group: bang.group - const Constant(1)),
|
||||
);
|
||||
await (bangTriggers.update()
|
||||
..where((t) => t.group.isBiggerThanValue(0)))
|
||||
.write(
|
||||
BangTriggersCompanion.custom(
|
||||
group: bangTriggers.group - const Constant(1),
|
||||
),
|
||||
);
|
||||
await (bangSync.update()..where((t) => t.group.isBiggerThanValue(0)))
|
||||
.write(
|
||||
BangSyncCompanion.custom(
|
||||
group: bangSync.group - const Constant(1),
|
||||
),
|
||||
);
|
||||
await (bangFrequency.update()
|
||||
..where((t) => t.group.isBiggerThanValue(0)))
|
||||
.write(
|
||||
BangFrequencyCompanion.custom(
|
||||
group: bangFrequency.group - const Constant(1),
|
||||
),
|
||||
);
|
||||
await (bangHistory.update()
|
||||
..where((t) => t.group.isBiggerThanValue(0)))
|
||||
.write(
|
||||
BangHistoryCompanion.custom(
|
||||
group: bangHistory.group - const Constant(1),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await customStatement('PRAGMA foreign_keys = ON');
|
||||
},
|
||||
);
|
||||
|
||||
BangDatabase(super.e);
|
||||
|
||||
static final _upgrade = migrationSteps(
|
||||
from1To2: (m, schema) async {
|
||||
//Too many changes, we switch to a new database
|
||||
},
|
||||
from2To3: (m, schema) async {
|
||||
await m.addColumn(schema.bang, schema.bang.searxngApi);
|
||||
},
|
||||
from3To4: (m, schema) async {
|
||||
await m.alterTable(TableMigration(schema.bangHistory));
|
||||
},
|
||||
from4To5: (m, schema) async {
|
||||
await m.addColumn(schema.bang, schema.bang.snapDomain);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
// dart format width=80
|
||||
// ignore_for_file: type=lint
|
||||
import 'package:drift/drift.dart' as i0;
|
||||
import 'package:weblibre/features/bangs/data/database/definitions.drift.dart'
|
||||
as i1;
|
||||
import 'package:weblibre/features/bangs/data/database/daos/bang.dart' as i2;
|
||||
import 'package:weblibre/features/bangs/data/database/database.dart' as i3;
|
||||
import 'package:weblibre/features/bangs/data/database/daos/sync.dart' as i4;
|
||||
import 'package:drift/internal/modular.dart' as i5;
|
||||
import 'package:sqlite3/common.dart' as i6;
|
||||
|
||||
abstract class $BangDatabase extends i0.GeneratedDatabase {
|
||||
$BangDatabase(i0.QueryExecutor e) : super(e);
|
||||
$BangDatabaseManager get managers => $BangDatabaseManager(this);
|
||||
late final i1.BangTable bang = i1.BangTable(this);
|
||||
late final i1.BangTriggers bangTriggers = i1.BangTriggers(this);
|
||||
late final i1.BangSync bangSync = i1.BangSync(this);
|
||||
late final i1.BangFrequency bangFrequency = i1.BangFrequency(this);
|
||||
late final i1.BangHistory bangHistory = i1.BangHistory(this);
|
||||
late final i1.BangFts bangFts = i1.BangFts(this);
|
||||
late final i1.BangTriggersFts bangTriggersFts = i1.BangTriggersFts(this);
|
||||
late final i1.BangDataView bangDataView = i1.BangDataView(this);
|
||||
late final i2.BangDao bangDao = i2.BangDao(this as i3.BangDatabase);
|
||||
late final i4.SyncDao syncDao = i4.SyncDao(this as i3.BangDatabase);
|
||||
i1.DefinitionsDrift get definitionsDrift => i5.ReadDatabaseContainer(
|
||||
this,
|
||||
).accessor<i1.DefinitionsDrift>(i1.DefinitionsDrift.new);
|
||||
@override
|
||||
Iterable<i0.TableInfo<i0.Table, Object?>> get allTables =>
|
||||
allSchemaEntities.whereType<i0.TableInfo<i0.Table, Object?>>();
|
||||
@override
|
||||
List<i0.DatabaseSchemaEntity> get allSchemaEntities => [
|
||||
bang,
|
||||
bangTriggers,
|
||||
i1.idxBangTriggersLookup,
|
||||
i1.bangTriggersAfterInsert,
|
||||
i1.bangTriggersAfterUpdate,
|
||||
bangSync,
|
||||
bangFrequency,
|
||||
bangHistory,
|
||||
bangFts,
|
||||
bangTriggersFts,
|
||||
bangDataView,
|
||||
i1.bangAfterInsert,
|
||||
i1.bangAfterDelete,
|
||||
i1.bangAfterUpdate,
|
||||
i1.bangTriggersAfterInsertFts,
|
||||
i1.bangTriggersAfterDeleteFts,
|
||||
i1.bangTriggersAfterUpdateFts,
|
||||
];
|
||||
@override
|
||||
i0.StreamQueryUpdateRules
|
||||
get streamUpdateRules => const i0.StreamQueryUpdateRules([
|
||||
i0.WritePropagation(
|
||||
on: i0.TableUpdateQuery.onTableName(
|
||||
'bang',
|
||||
limitUpdateKind: i0.UpdateKind.delete,
|
||||
),
|
||||
result: [i0.TableUpdate('bang_triggers', kind: i0.UpdateKind.delete)],
|
||||
),
|
||||
i0.WritePropagation(
|
||||
on: i0.TableUpdateQuery.onTableName(
|
||||
'bang',
|
||||
limitUpdateKind: i0.UpdateKind.insert,
|
||||
),
|
||||
result: [i0.TableUpdate('bang_triggers', kind: i0.UpdateKind.insert)],
|
||||
),
|
||||
i0.WritePropagation(
|
||||
on: i0.TableUpdateQuery.onTableName(
|
||||
'bang',
|
||||
limitUpdateKind: i0.UpdateKind.update,
|
||||
),
|
||||
result: [
|
||||
i0.TableUpdate('bang_triggers', kind: i0.UpdateKind.delete),
|
||||
i0.TableUpdate('bang_triggers', kind: i0.UpdateKind.insert),
|
||||
],
|
||||
),
|
||||
i0.WritePropagation(
|
||||
on: i0.TableUpdateQuery.onTableName(
|
||||
'bang',
|
||||
limitUpdateKind: i0.UpdateKind.delete,
|
||||
),
|
||||
result: [i0.TableUpdate('bang_frequency', kind: i0.UpdateKind.delete)],
|
||||
),
|
||||
i0.WritePropagation(
|
||||
on: i0.TableUpdateQuery.onTableName(
|
||||
'bang',
|
||||
limitUpdateKind: i0.UpdateKind.delete,
|
||||
),
|
||||
result: [i0.TableUpdate('bang_history', kind: i0.UpdateKind.delete)],
|
||||
),
|
||||
i0.WritePropagation(
|
||||
on: i0.TableUpdateQuery.onTableName(
|
||||
'bang',
|
||||
limitUpdateKind: i0.UpdateKind.insert,
|
||||
),
|
||||
result: [i0.TableUpdate('bang_fts', kind: i0.UpdateKind.insert)],
|
||||
),
|
||||
i0.WritePropagation(
|
||||
on: i0.TableUpdateQuery.onTableName(
|
||||
'bang',
|
||||
limitUpdateKind: i0.UpdateKind.delete,
|
||||
),
|
||||
result: [i0.TableUpdate('bang_fts', kind: i0.UpdateKind.insert)],
|
||||
),
|
||||
i0.WritePropagation(
|
||||
on: i0.TableUpdateQuery.onTableName(
|
||||
'bang',
|
||||
limitUpdateKind: i0.UpdateKind.update,
|
||||
),
|
||||
result: [i0.TableUpdate('bang_fts', kind: i0.UpdateKind.insert)],
|
||||
),
|
||||
i0.WritePropagation(
|
||||
on: i0.TableUpdateQuery.onTableName(
|
||||
'bang_triggers',
|
||||
limitUpdateKind: i0.UpdateKind.insert,
|
||||
),
|
||||
result: [i0.TableUpdate('bang_triggers_fts', kind: i0.UpdateKind.insert)],
|
||||
),
|
||||
i0.WritePropagation(
|
||||
on: i0.TableUpdateQuery.onTableName(
|
||||
'bang_triggers',
|
||||
limitUpdateKind: i0.UpdateKind.delete,
|
||||
),
|
||||
result: [i0.TableUpdate('bang_triggers_fts', kind: i0.UpdateKind.insert)],
|
||||
),
|
||||
i0.WritePropagation(
|
||||
on: i0.TableUpdateQuery.onTableName(
|
||||
'bang_triggers',
|
||||
limitUpdateKind: i0.UpdateKind.update,
|
||||
),
|
||||
result: [i0.TableUpdate('bang_triggers_fts', kind: i0.UpdateKind.insert)],
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
class $BangDatabaseManager {
|
||||
final $BangDatabase _db;
|
||||
$BangDatabaseManager(this._db);
|
||||
i1.$BangTableTableManager get bang =>
|
||||
i1.$BangTableTableManager(_db, _db.bang);
|
||||
i1.$BangTriggersTableManager get bangTriggers =>
|
||||
i1.$BangTriggersTableManager(_db, _db.bangTriggers);
|
||||
i1.$BangSyncTableManager get bangSync =>
|
||||
i1.$BangSyncTableManager(_db, _db.bangSync);
|
||||
i1.$BangFrequencyTableManager get bangFrequency =>
|
||||
i1.$BangFrequencyTableManager(_db, _db.bangFrequency);
|
||||
i1.$BangHistoryTableManager get bangHistory =>
|
||||
i1.$BangHistoryTableManager(_db, _db.bangHistory);
|
||||
i1.$BangFtsTableManager get bangFts =>
|
||||
i1.$BangFtsTableManager(_db, _db.bangFts);
|
||||
i1.$BangTriggersFtsTableManager get bangTriggersFts =>
|
||||
i1.$BangTriggersFtsTableManager(_db, _db.bangTriggersFts);
|
||||
}
|
||||
|
||||
extension DefineFunctions on i6.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 i6.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 i6.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 i6.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 i6.AllowedArgumentCount(2),
|
||||
function: (args) {
|
||||
final arg0 = args[0] as String?;
|
||||
final arg1 = args[1] as String?;
|
||||
return lexoRankReorderBefore(arg0, arg1);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,295 @@
|
||||
import 'package:weblibre/features/bangs/data/database/drift/converters/bang_format.dart';
|
||||
import 'package:weblibre/features/bangs/data/database/drift/converters/trigger_list.dart';
|
||||
import 'package:weblibre/features/bangs/data/models/bang_data.dart';
|
||||
import 'package:weblibre/features/bangs/data/models/bang_group.dart';
|
||||
import 'package:weblibre/features/bangs/data/models/bang.dart';
|
||||
import 'package:weblibre/features/bangs/data/models/search_history_entry.dart';
|
||||
|
||||
CREATE TABLE bang (
|
||||
"trigger" TEXT NOT NULL,
|
||||
"group" ENUM(BangGroup) NOT NULL,
|
||||
website_name TEXT NOT NULL,
|
||||
domain TEXT NOT NULL,
|
||||
url_template TEXT NOT NULL,
|
||||
category TEXT,
|
||||
sub_category TEXT,
|
||||
format TEXT MAPPED BY `const BangFormatConverter()`,
|
||||
additional_triggers TEXT MAPPED BY `const TriggerListConverter()`,
|
||||
searxng_api BOOL NOT NULL DEFAULT FALSE,
|
||||
snap_domain TEXT,
|
||||
PRIMARY KEY ("trigger", "group")
|
||||
) WITH Bang;
|
||||
|
||||
CREATE TABLE bang_triggers (
|
||||
"trigger" TEXT NOT NULL,
|
||||
"group" ENUM(BangGroup) NOT NULL,
|
||||
additional_trigger TEXT NOT NULL,
|
||||
PRIMARY KEY ("trigger", "group", additional_trigger),
|
||||
FOREIGN KEY ("trigger", "group") REFERENCES bang ("trigger", "group") ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX idx_bang_triggers_lookup ON bang_triggers (additional_trigger, "group");
|
||||
|
||||
-- Trigger to populate bang_triggers when inserting a new bang
|
||||
CREATE TRIGGER bang_triggers_after_insert AFTER INSERT ON bang
|
||||
WHEN new.additional_triggers IS NOT NULL
|
||||
BEGIN
|
||||
INSERT INTO bang_triggers("trigger", "group", additional_trigger)
|
||||
SELECT
|
||||
new."trigger",
|
||||
new."group",
|
||||
json_each.value
|
||||
FROM json_each(new.additional_triggers);
|
||||
END;
|
||||
|
||||
-- Trigger to update bang_triggers when updating a bang
|
||||
CREATE TRIGGER bang_triggers_after_update AFTER UPDATE ON bang BEGIN
|
||||
-- Delete old additional triggers
|
||||
DELETE FROM bang_triggers
|
||||
WHERE "trigger" = old."trigger" AND "group" = old."group";
|
||||
|
||||
-- Insert new additional triggers if they exist
|
||||
INSERT INTO bang_triggers("trigger", "group", additional_trigger)
|
||||
SELECT
|
||||
new."trigger",
|
||||
new."group",
|
||||
json_each.value
|
||||
FROM json_each(new.additional_triggers)
|
||||
WHERE new.additional_triggers IS NOT NULL;
|
||||
END;
|
||||
|
||||
CREATE TABLE bang_sync (
|
||||
"group" ENUM(BangGroup) PRIMARY KEY NOT NULL,
|
||||
last_sync DATETIME NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE bang_frequency (
|
||||
"trigger" TEXT NOT NULL,
|
||||
"group" ENUM(BangGroup) NOT NULL,
|
||||
frequency INTEGER NOT NULL,
|
||||
last_used DATETIME NOT NULL,
|
||||
PRIMARY KEY ("trigger", "group"),
|
||||
FOREIGN KEY ("trigger", "group") REFERENCES bang ("trigger", "group") ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE bang_history (
|
||||
search_query TEXT UNIQUE NOT NULL,
|
||||
"trigger" TEXT NOT NULL,
|
||||
"group" ENUM(BangGroup) NOT NULL,
|
||||
search_date DATETIME NOT NULL,
|
||||
FOREIGN KEY ("trigger", "group") REFERENCES bang ("trigger", "group") ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE VIRTUAL TABLE bang_fts
|
||||
USING fts5(
|
||||
trigger,
|
||||
website_name,
|
||||
content=bang,
|
||||
prefix='2 3'
|
||||
);
|
||||
|
||||
CREATE VIRTUAL TABLE bang_triggers_fts
|
||||
USING fts5(
|
||||
additional_trigger,
|
||||
content=bang_triggers,
|
||||
prefix='2 3'
|
||||
);
|
||||
|
||||
CREATE VIEW bang_data_view WITH BangData AS
|
||||
SELECT
|
||||
b.*,
|
||||
bf.frequency,
|
||||
bf.last_used
|
||||
FROM
|
||||
bang b
|
||||
LEFT JOIN
|
||||
bang_frequency bf ON b."trigger" = bf."trigger" AND b."group" = bf."group";
|
||||
|
||||
-- Triggers to keep the FTS index up to date.
|
||||
CREATE TRIGGER bang_after_insert AFTER INSERT ON bang BEGIN
|
||||
INSERT INTO
|
||||
bang_fts(rowid, "trigger", website_name)
|
||||
VALUES (new.rowid, new."trigger", new.website_name);
|
||||
END;
|
||||
CREATE TRIGGER bang_after_delete AFTER DELETE ON bang BEGIN
|
||||
INSERT INTO
|
||||
bang_fts(bang_fts, rowid, "trigger", website_name)
|
||||
VALUES('delete', old.rowid, old."trigger", old.website_name);
|
||||
END;
|
||||
CREATE TRIGGER bang_after_update AFTER UPDATE ON bang BEGIN
|
||||
INSERT INTO
|
||||
bang_fts(bang_fts, rowid, "trigger", website_name)
|
||||
VALUES('delete', old.rowid, old."trigger", old.website_name);
|
||||
INSERT INTO
|
||||
bang_fts(rowid, "trigger", website_name)
|
||||
VALUES (new.rowid, new."trigger", new.website_name);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER bang_triggers_after_insert_fts AFTER INSERT ON bang_triggers BEGIN
|
||||
INSERT INTO
|
||||
bang_triggers_fts(rowid, additional_trigger)
|
||||
VALUES (new.rowid, new.additional_trigger);
|
||||
END;
|
||||
CREATE TRIGGER bang_triggers_after_delete_fts AFTER DELETE ON bang_triggers BEGIN
|
||||
INSERT INTO
|
||||
bang_triggers_fts(bang_triggers_fts, rowid, additional_trigger)
|
||||
VALUES('delete', old.rowid, old.additional_trigger);
|
||||
END;
|
||||
CREATE TRIGGER bang_triggers_after_update_fts AFTER UPDATE ON bang_triggers BEGIN
|
||||
INSERT INTO
|
||||
bang_triggers_fts(bang_triggers_fts, rowid, additional_trigger)
|
||||
VALUES('delete', old.rowid, old.additional_trigger);
|
||||
INSERT INTO
|
||||
bang_triggers_fts(rowid, additional_trigger)
|
||||
VALUES (new.rowid, new.additional_trigger);
|
||||
END;
|
||||
|
||||
optimizeBangFtsIndex:
|
||||
INSERT INTO bang_fts(bang_fts) VALUES ('optimize');
|
||||
|
||||
optimizeTriggerFtsIndex:
|
||||
INSERT INTO bang_triggers_fts(bang_triggers_fts) VALUES ('optimize');
|
||||
|
||||
queryBangs WITH BangData:
|
||||
WITH weights AS (
|
||||
SELECT
|
||||
10.0 AS "trigger",
|
||||
8.0 AS additional_trigger,
|
||||
5.0 AS website_name
|
||||
),
|
||||
bang_results AS (
|
||||
SELECT
|
||||
b.*,
|
||||
bf.frequency,
|
||||
bf.last_used,
|
||||
bm25(bang_fts, weights."trigger", weights.website_name) AS weighted_rank
|
||||
FROM
|
||||
bang_fts(:query) fts
|
||||
INNER JOIN
|
||||
bang b ON b.rowid = fts.rowid
|
||||
LEFT JOIN
|
||||
bang_frequency bf ON b."trigger" = bf."trigger" AND b."group" = bf."group"
|
||||
CROSS JOIN weights
|
||||
),
|
||||
trigger_results AS (
|
||||
SELECT
|
||||
b.*,
|
||||
bf.frequency,
|
||||
bf.last_used,
|
||||
bm25(bang_triggers_fts, weights.additional_trigger) AS weighted_rank
|
||||
FROM
|
||||
bang_triggers_fts(:query) tfts
|
||||
INNER JOIN
|
||||
bang_triggers bt ON bt.rowid = tfts.rowid
|
||||
INNER JOIN
|
||||
bang b ON b."trigger" = bt."trigger" AND b."group" = bt."group"
|
||||
LEFT JOIN
|
||||
bang_frequency bf ON b."trigger" = bf."trigger" AND b."group" = bf."group"
|
||||
CROSS JOIN weights
|
||||
),
|
||||
combined_results AS (
|
||||
SELECT * FROM bang_results
|
||||
UNION ALL
|
||||
SELECT * FROM trigger_results
|
||||
)
|
||||
SELECT
|
||||
*,
|
||||
MIN(weighted_rank) AS weighted_rank
|
||||
FROM combined_results
|
||||
GROUP BY "trigger", "group"
|
||||
ORDER BY
|
||||
weighted_rank ASC,
|
||||
frequency NULLS LAST;
|
||||
|
||||
queryBangsBasic WITH BangData:
|
||||
WITH weights AS (
|
||||
SELECT
|
||||
10.0 AS "trigger",
|
||||
8.0 AS additional_trigger,
|
||||
5.0 AS website_name
|
||||
),
|
||||
bang_results AS (
|
||||
SELECT
|
||||
b.*,
|
||||
bf.frequency,
|
||||
bf.last_used,
|
||||
bm25(bang_fts, weights."trigger", weights.website_name) AS weighted_rank
|
||||
FROM
|
||||
bang_fts fts
|
||||
INNER JOIN
|
||||
bang b ON b.rowid = fts.rowid
|
||||
LEFT JOIN
|
||||
bang_frequency bf ON b."trigger" = bf."trigger" AND b."group" = bf."group"
|
||||
CROSS JOIN weights
|
||||
WHERE
|
||||
fts."trigger" LIKE :query OR
|
||||
fts.website_name LIKE :query
|
||||
),
|
||||
trigger_results AS (
|
||||
SELECT
|
||||
b.*,
|
||||
bf.frequency,
|
||||
bf.last_used,
|
||||
bm25(bang_triggers_fts, weights.additional_trigger) AS weighted_rank
|
||||
FROM
|
||||
bang_triggers_fts tfts
|
||||
INNER JOIN
|
||||
bang_triggers bt ON bt.rowid = tfts.rowid
|
||||
INNER JOIN
|
||||
bang b ON b."trigger" = bt."trigger" AND b."group" = bt."group"
|
||||
LEFT JOIN
|
||||
bang_frequency bf ON b."trigger" = bf."trigger" AND b."group" = bf."group"
|
||||
CROSS JOIN weights
|
||||
WHERE
|
||||
tfts.additional_trigger LIKE :query
|
||||
),
|
||||
combined_results AS (
|
||||
SELECT * FROM bang_results
|
||||
UNION ALL
|
||||
SELECT * FROM trigger_results
|
||||
)
|
||||
SELECT
|
||||
*,
|
||||
MIN(weighted_rank) AS weighted_rank
|
||||
FROM combined_results
|
||||
GROUP BY "trigger", "group"
|
||||
ORDER BY
|
||||
weighted_rank ASC,
|
||||
frequency NULLS LAST;
|
||||
|
||||
categoriesJson:
|
||||
WITH categories AS (
|
||||
SELECT
|
||||
b.category,
|
||||
json_group_array(
|
||||
DISTINCT b.sub_category
|
||||
ORDER BY b.sub_category
|
||||
) AS sub_categories
|
||||
FROM
|
||||
bang b
|
||||
WHERE
|
||||
b.category IS NOT NULL AND
|
||||
b.sub_category IS NOT NULL
|
||||
GROUP BY b.category
|
||||
ORDER BY b.category
|
||||
)
|
||||
SELECT
|
||||
json_group_object(
|
||||
c.category,
|
||||
json(c.sub_categories)
|
||||
) AS categories_json
|
||||
FROM categories c;
|
||||
|
||||
searchHistoryEntries WITH SearchHistoryEntry:
|
||||
SELECT *
|
||||
FROM bang_history
|
||||
ORDER BY search_date DESC
|
||||
LIMIT :limit;
|
||||
|
||||
evictHistoryEntries:
|
||||
DELETE FROM bang_history
|
||||
WHERE rowid IN (
|
||||
SELECT rowid
|
||||
FROM bang_history
|
||||
ORDER BY search_date DESC
|
||||
LIMIT -1 OFFSET :limit
|
||||
);
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* 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:convert';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:weblibre/features/bangs/data/models/bang.dart';
|
||||
|
||||
class BangFormatConverter extends TypeConverter<Set<BangFormat>?, String?> {
|
||||
const BangFormatConverter();
|
||||
|
||||
@override
|
||||
Set<BangFormat>? fromSql(String? fromDb) {
|
||||
if (fromDb == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Bang.decodeFormat(jsonDecode(fromDb) as List);
|
||||
}
|
||||
|
||||
@override
|
||||
String? toSql(Set<BangFormat>? value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return jsonEncode(Bang.encodeFormat(value));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* 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:convert';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:nullability/nullability.dart';
|
||||
|
||||
class TriggerListConverter extends TypeConverter<Set<String>?, String?> {
|
||||
const TriggerListConverter();
|
||||
|
||||
@override
|
||||
Set<String>? fromSql(String? fromDb) {
|
||||
return fromDb.mapNotNull(
|
||||
(value) => (jsonDecode(value) as List).cast<String>().toSet(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String? toSql(Set<String>? value) {
|
||||
return value.mapNotNull((value) => jsonEncode(value.toList()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'package:copy_with_extension/copy_with_extension.dart';
|
||||
import 'package:drift/drift.dart' show Expression, Insertable, Value;
|
||||
import 'package:fast_equatable/fast_equatable.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:nullability/nullability.dart';
|
||||
import 'package:weblibre/extensions/uri.dart';
|
||||
import 'package:weblibre/features/bangs/data/database/definitions.drift.dart';
|
||||
import 'package:weblibre/features/bangs/data/models/bang_group.dart';
|
||||
|
||||
part 'bang.g.dart';
|
||||
|
||||
enum BangFormat {
|
||||
///When the bang is invoked with no query, opens the base path of the URL (/)
|
||||
///instead of any path given in the template (g., /search)
|
||||
@JsonValue('open_base_path')
|
||||
openBasePath,
|
||||
|
||||
///URL encode the search terms. Some sites do not work with this, so it can
|
||||
///be disabled by omitting this.
|
||||
@JsonValue('url_encode_placeholder')
|
||||
urlEncodePlaceholder,
|
||||
|
||||
///URL encodes spaces as +, instead of %20. Some sites only work correctly
|
||||
///with one or the other.
|
||||
@JsonValue('url_encode_space_to_plus')
|
||||
urlEncodeSpaceToPlus,
|
||||
|
||||
///When the bang is invoked with no query, open the snap domain (ad) instead of any path given in the template
|
||||
@JsonValue('open_snap_domain')
|
||||
openSnapDomain,
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
@CopyWith()
|
||||
class Bang with FastEquatable implements Insertable<Bang> {
|
||||
static const _templateQueryPlaceholder = '{{{s}}}';
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
final BangGroup? group;
|
||||
|
||||
///The name of the website associated with the bang.
|
||||
@JsonKey(name: 's')
|
||||
final String websiteName;
|
||||
|
||||
///The domain name of the websit
|
||||
@JsonKey(name: 'd')
|
||||
final String domain;
|
||||
|
||||
///The specific trigger word or phrase used to invoke the bang.
|
||||
@JsonKey(name: 't')
|
||||
final String trigger;
|
||||
|
||||
///The URL template to use when the bang is invoked, where `{{{s}}}` is replaced by the user's query.
|
||||
@JsonKey(name: 'u')
|
||||
final String urlTemplate;
|
||||
|
||||
///The category of the website, if applicable
|
||||
@JsonKey(name: 'c')
|
||||
final String? category;
|
||||
|
||||
///The subcategory of the website, if applicable
|
||||
@JsonKey(name: 'sc')
|
||||
final String? subCategory;
|
||||
|
||||
///The format flags indicating how the query should be processed.
|
||||
@JsonKey(name: 'fmt')
|
||||
final Set<BangFormat>? format;
|
||||
|
||||
///Additional triggers that invoke this bang
|
||||
@JsonKey(name: 'ts')
|
||||
final Set<String>? additionalTriggers;
|
||||
|
||||
///Additional triggers that invoke this bang
|
||||
@JsonKey(name: 'ad')
|
||||
final String? snapDomain;
|
||||
|
||||
@JsonKey(defaultValue: false)
|
||||
final bool searxngApi;
|
||||
|
||||
String formatQuery(String input) {
|
||||
return (format == null ||
|
||||
format!.contains(BangFormat.urlEncodePlaceholder) == true)
|
||||
? (format == null ||
|
||||
format?.contains(BangFormat.urlEncodeSpaceToPlus) == true)
|
||||
? Uri.encodeQueryComponent(input)
|
||||
: Uri.encodeComponent(input)
|
||||
: input;
|
||||
}
|
||||
|
||||
Uri getDefaultUrl() {
|
||||
return getTemplateUrl('');
|
||||
}
|
||||
|
||||
Uri getTemplateUrl(String? query) {
|
||||
final queryEmpty = query.isEmpty;
|
||||
|
||||
if (queryEmpty && format?.contains(BangFormat.openSnapDomain) == true) {
|
||||
if (snapDomain.isNotEmpty) {
|
||||
return Uri.parse(snapDomain!);
|
||||
}
|
||||
}
|
||||
|
||||
final url = (!queryEmpty)
|
||||
? urlTemplate.replaceAll(_templateQueryPlaceholder, formatQuery(query!))
|
||||
: urlTemplate;
|
||||
|
||||
var template = Uri.parse(url);
|
||||
if (!template.hasScheme || template.origin.isEmpty) {
|
||||
template = Uri.https(
|
||||
domain,
|
||||
).replace(path: template.path, query: template.query);
|
||||
}
|
||||
|
||||
if (queryEmpty && format?.contains(BangFormat.openBasePath) == true) {
|
||||
template = template.base;
|
||||
}
|
||||
|
||||
return template;
|
||||
}
|
||||
|
||||
static Set<BangFormat> decodeFormat(Iterable input) {
|
||||
return input.map((e) => $enumDecode(_$BangFormatEnumMap, e)).toSet();
|
||||
}
|
||||
|
||||
static List<String> encodeFormat(Iterable<BangFormat> format) {
|
||||
return format.map((e) => _$BangFormatEnumMap[e]!).toList();
|
||||
}
|
||||
|
||||
Bang({
|
||||
required this.websiteName,
|
||||
required this.domain,
|
||||
required this.trigger,
|
||||
required this.urlTemplate,
|
||||
required this.searxngApi,
|
||||
this.group,
|
||||
this.category,
|
||||
this.subCategory,
|
||||
this.format,
|
||||
this.additionalTriggers,
|
||||
this.snapDomain,
|
||||
});
|
||||
|
||||
factory Bang.fromJson(Map<String, dynamic> json) => _$BangFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$BangToJson(this);
|
||||
|
||||
@override
|
||||
List<Object?> get hashParameters => [
|
||||
group,
|
||||
websiteName,
|
||||
domain,
|
||||
trigger,
|
||||
urlTemplate,
|
||||
category,
|
||||
subCategory,
|
||||
format,
|
||||
additionalTriggers,
|
||||
snapDomain,
|
||||
searxngApi,
|
||||
];
|
||||
|
||||
@override
|
||||
Map<String, Expression<Object>> toColumns(bool nullToAbsent) {
|
||||
return BangCompanion(
|
||||
trigger: Value(trigger),
|
||||
websiteName: Value(websiteName),
|
||||
domain: Value(domain),
|
||||
urlTemplate: Value(urlTemplate),
|
||||
group: Value.absentIfNull(group),
|
||||
category: Value.absentIfNull(category),
|
||||
subCategory: Value.absentIfNull(subCategory),
|
||||
format: Value.absentIfNull(format),
|
||||
additionalTriggers: Value.absentIfNull(additionalTriggers),
|
||||
snapDomain: Value.absentIfNull(snapDomain),
|
||||
).toColumns(nullToAbsent);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'bang.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// CopyWithGenerator
|
||||
// **************************************************************************
|
||||
|
||||
abstract class _$BangCWProxy {
|
||||
Bang websiteName(String websiteName);
|
||||
|
||||
Bang domain(String domain);
|
||||
|
||||
Bang trigger(String trigger);
|
||||
|
||||
Bang urlTemplate(String urlTemplate);
|
||||
|
||||
Bang searxngApi(bool searxngApi);
|
||||
|
||||
Bang group(BangGroup? group);
|
||||
|
||||
Bang category(String? category);
|
||||
|
||||
Bang subCategory(String? subCategory);
|
||||
|
||||
Bang format(Set<BangFormat>? format);
|
||||
|
||||
Bang additionalTriggers(Set<String>? additionalTriggers);
|
||||
|
||||
Bang snapDomain(String? snapDomain);
|
||||
|
||||
/// 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 `Bang(...).copyWith.fieldName(value)`.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// Bang(...).copyWith(id: 12, name: "My name")
|
||||
/// ```
|
||||
Bang call({
|
||||
String websiteName,
|
||||
String domain,
|
||||
String trigger,
|
||||
String urlTemplate,
|
||||
bool searxngApi,
|
||||
BangGroup? group,
|
||||
String? category,
|
||||
String? subCategory,
|
||||
Set<BangFormat>? format,
|
||||
Set<String>? additionalTriggers,
|
||||
String? snapDomain,
|
||||
});
|
||||
}
|
||||
|
||||
/// Callable proxy for `copyWith` functionality.
|
||||
/// Use as `instanceOfBang.copyWith(...)` or call `instanceOfBang.copyWith.fieldName(value)` for a single field.
|
||||
class _$BangCWProxyImpl implements _$BangCWProxy {
|
||||
const _$BangCWProxyImpl(this._value);
|
||||
|
||||
final Bang _value;
|
||||
|
||||
@override
|
||||
Bang websiteName(String websiteName) => call(websiteName: websiteName);
|
||||
|
||||
@override
|
||||
Bang domain(String domain) => call(domain: domain);
|
||||
|
||||
@override
|
||||
Bang trigger(String trigger) => call(trigger: trigger);
|
||||
|
||||
@override
|
||||
Bang urlTemplate(String urlTemplate) => call(urlTemplate: urlTemplate);
|
||||
|
||||
@override
|
||||
Bang searxngApi(bool searxngApi) => call(searxngApi: searxngApi);
|
||||
|
||||
@override
|
||||
Bang group(BangGroup? group) => call(group: group);
|
||||
|
||||
@override
|
||||
Bang category(String? category) => call(category: category);
|
||||
|
||||
@override
|
||||
Bang subCategory(String? subCategory) => call(subCategory: subCategory);
|
||||
|
||||
@override
|
||||
Bang format(Set<BangFormat>? format) => call(format: format);
|
||||
|
||||
@override
|
||||
Bang additionalTriggers(Set<String>? additionalTriggers) =>
|
||||
call(additionalTriggers: additionalTriggers);
|
||||
|
||||
@override
|
||||
Bang snapDomain(String? snapDomain) => call(snapDomain: snapDomain);
|
||||
|
||||
@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 `Bang(...).copyWith.fieldName(value)`.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// Bang(...).copyWith(id: 12, name: "My name")
|
||||
/// ```
|
||||
Bang call({
|
||||
Object? websiteName = const $CopyWithPlaceholder(),
|
||||
Object? domain = const $CopyWithPlaceholder(),
|
||||
Object? trigger = const $CopyWithPlaceholder(),
|
||||
Object? urlTemplate = const $CopyWithPlaceholder(),
|
||||
Object? searxngApi = const $CopyWithPlaceholder(),
|
||||
Object? group = const $CopyWithPlaceholder(),
|
||||
Object? category = const $CopyWithPlaceholder(),
|
||||
Object? subCategory = const $CopyWithPlaceholder(),
|
||||
Object? format = const $CopyWithPlaceholder(),
|
||||
Object? additionalTriggers = const $CopyWithPlaceholder(),
|
||||
Object? snapDomain = const $CopyWithPlaceholder(),
|
||||
}) {
|
||||
return Bang(
|
||||
websiteName:
|
||||
websiteName == const $CopyWithPlaceholder() || websiteName == null
|
||||
? _value.websiteName
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: websiteName as String,
|
||||
domain: domain == const $CopyWithPlaceholder() || domain == null
|
||||
? _value.domain
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: domain as String,
|
||||
trigger: trigger == const $CopyWithPlaceholder() || trigger == null
|
||||
? _value.trigger
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: trigger as String,
|
||||
urlTemplate:
|
||||
urlTemplate == const $CopyWithPlaceholder() || urlTemplate == null
|
||||
? _value.urlTemplate
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: urlTemplate as String,
|
||||
searxngApi:
|
||||
searxngApi == const $CopyWithPlaceholder() || searxngApi == null
|
||||
? _value.searxngApi
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: searxngApi as bool,
|
||||
group: group == const $CopyWithPlaceholder()
|
||||
? _value.group
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: group as BangGroup?,
|
||||
category: category == const $CopyWithPlaceholder()
|
||||
? _value.category
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: category as String?,
|
||||
subCategory: subCategory == const $CopyWithPlaceholder()
|
||||
? _value.subCategory
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: subCategory as String?,
|
||||
format: format == const $CopyWithPlaceholder()
|
||||
? _value.format
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: format as Set<BangFormat>?,
|
||||
additionalTriggers: additionalTriggers == const $CopyWithPlaceholder()
|
||||
? _value.additionalTriggers
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: additionalTriggers as Set<String>?,
|
||||
snapDomain: snapDomain == const $CopyWithPlaceholder()
|
||||
? _value.snapDomain
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: snapDomain as String?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension $BangCopyWith on Bang {
|
||||
/// Returns a callable class used to build a new instance with modified fields.
|
||||
/// Example: `instanceOfBang.copyWith(...)` or `instanceOfBang.copyWith.fieldName(...)`.
|
||||
// ignore: library_private_types_in_public_api
|
||||
_$BangCWProxy get copyWith => _$BangCWProxyImpl(this);
|
||||
}
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
Bang _$BangFromJson(Map<String, dynamic> json) => Bang(
|
||||
websiteName: json['s'] as String,
|
||||
domain: json['d'] as String,
|
||||
trigger: json['t'] as String,
|
||||
urlTemplate: json['u'] as String,
|
||||
searxngApi: json['searxngApi'] as bool? ?? false,
|
||||
category: json['c'] as String?,
|
||||
subCategory: json['sc'] as String?,
|
||||
format: (json['fmt'] as List<dynamic>?)
|
||||
?.map((e) => $enumDecode(_$BangFormatEnumMap, e))
|
||||
.toSet(),
|
||||
additionalTriggers: (json['ts'] as List<dynamic>?)
|
||||
?.map((e) => e as String)
|
||||
.toSet(),
|
||||
snapDomain: json['ad'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$BangToJson(Bang instance) => <String, dynamic>{
|
||||
's': instance.websiteName,
|
||||
'd': instance.domain,
|
||||
't': instance.trigger,
|
||||
'u': instance.urlTemplate,
|
||||
'c': instance.category,
|
||||
'sc': instance.subCategory,
|
||||
'fmt': instance.format?.map((e) => _$BangFormatEnumMap[e]!).toList(),
|
||||
'ts': instance.additionalTriggers?.toList(),
|
||||
'ad': instance.snapDomain,
|
||||
'searxngApi': instance.searxngApi,
|
||||
};
|
||||
|
||||
const _$BangFormatEnumMap = {
|
||||
BangFormat.openBasePath: 'open_base_path',
|
||||
BangFormat.urlEncodePlaceholder: 'url_encode_placeholder',
|
||||
BangFormat.urlEncodeSpaceToPlus: 'url_encode_space_to_plus',
|
||||
BangFormat.openSnapDomain: 'open_snap_domain',
|
||||
};
|
||||
@@ -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 'package:copy_with_extension/copy_with_extension.dart';
|
||||
import 'package:weblibre/features/bangs/data/models/bang.dart';
|
||||
import 'package:weblibre/features/bangs/data/models/bang_group.dart';
|
||||
import 'package:weblibre/features/bangs/data/models/bang_key.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/entities/browser_icon.dart';
|
||||
|
||||
part 'bang_data.g.dart';
|
||||
|
||||
@CopyWith(constructor: '_copyWith')
|
||||
class BangData extends Bang {
|
||||
final int frequency;
|
||||
final DateTime? lastUsed;
|
||||
|
||||
final BrowserIcon? icon;
|
||||
|
||||
@override
|
||||
BangGroup get group => super.group!;
|
||||
|
||||
BangData({
|
||||
required super.websiteName,
|
||||
required super.domain,
|
||||
required super.trigger,
|
||||
required super.urlTemplate,
|
||||
required super.group,
|
||||
required super.searxngApi,
|
||||
super.category,
|
||||
super.subCategory,
|
||||
super.format,
|
||||
super.additionalTriggers,
|
||||
super.snapDomain,
|
||||
int? frequency,
|
||||
this.lastUsed,
|
||||
this.icon,
|
||||
}) : frequency = frequency ?? 0;
|
||||
|
||||
//For some reasons including super.group breaks generation of copywith, so we have this one for now
|
||||
BangData._copyWith({
|
||||
required super.websiteName,
|
||||
required super.domain,
|
||||
required super.trigger,
|
||||
required super.urlTemplate,
|
||||
required super.searxngApi,
|
||||
super.category,
|
||||
super.subCategory,
|
||||
super.format,
|
||||
super.additionalTriggers,
|
||||
super.snapDomain,
|
||||
int? frequency,
|
||||
this.lastUsed,
|
||||
this.icon,
|
||||
}) : frequency = frequency ?? 0;
|
||||
|
||||
BangKey toKey() => BangKey(group: group, trigger: trigger);
|
||||
|
||||
@override
|
||||
List<Object?> get hashParameters => [
|
||||
...super.hashParameters,
|
||||
frequency,
|
||||
lastUsed,
|
||||
icon,
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'bang_data.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// CopyWithGenerator
|
||||
// **************************************************************************
|
||||
|
||||
abstract class _$BangDataCWProxy {
|
||||
BangData websiteName(String websiteName);
|
||||
|
||||
BangData domain(String domain);
|
||||
|
||||
BangData trigger(String trigger);
|
||||
|
||||
BangData urlTemplate(String urlTemplate);
|
||||
|
||||
BangData searxngApi(bool searxngApi);
|
||||
|
||||
BangData category(String? category);
|
||||
|
||||
BangData subCategory(String? subCategory);
|
||||
|
||||
BangData format(Set<BangFormat>? format);
|
||||
|
||||
BangData additionalTriggers(Set<String>? additionalTriggers);
|
||||
|
||||
BangData snapDomain(String? snapDomain);
|
||||
|
||||
BangData frequency(int? frequency);
|
||||
|
||||
BangData lastUsed(DateTime? lastUsed);
|
||||
|
||||
BangData icon(BrowserIcon? icon);
|
||||
|
||||
/// 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 `BangData(...).copyWith.fieldName(value)`.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// BangData(...).copyWith(id: 12, name: "My name")
|
||||
/// ```
|
||||
BangData call({
|
||||
String websiteName,
|
||||
String domain,
|
||||
String trigger,
|
||||
String urlTemplate,
|
||||
bool searxngApi,
|
||||
String? category,
|
||||
String? subCategory,
|
||||
Set<BangFormat>? format,
|
||||
Set<String>? additionalTriggers,
|
||||
String? snapDomain,
|
||||
int? frequency,
|
||||
DateTime? lastUsed,
|
||||
BrowserIcon? icon,
|
||||
});
|
||||
}
|
||||
|
||||
/// Callable proxy for `copyWith` functionality.
|
||||
/// Use as `instanceOfBangData.copyWith(...)` or call `instanceOfBangData.copyWith.fieldName(value)` for a single field.
|
||||
class _$BangDataCWProxyImpl implements _$BangDataCWProxy {
|
||||
const _$BangDataCWProxyImpl(this._value);
|
||||
|
||||
final BangData _value;
|
||||
|
||||
@override
|
||||
BangData websiteName(String websiteName) => call(websiteName: websiteName);
|
||||
|
||||
@override
|
||||
BangData domain(String domain) => call(domain: domain);
|
||||
|
||||
@override
|
||||
BangData trigger(String trigger) => call(trigger: trigger);
|
||||
|
||||
@override
|
||||
BangData urlTemplate(String urlTemplate) => call(urlTemplate: urlTemplate);
|
||||
|
||||
@override
|
||||
BangData searxngApi(bool searxngApi) => call(searxngApi: searxngApi);
|
||||
|
||||
@override
|
||||
BangData category(String? category) => call(category: category);
|
||||
|
||||
@override
|
||||
BangData subCategory(String? subCategory) => call(subCategory: subCategory);
|
||||
|
||||
@override
|
||||
BangData format(Set<BangFormat>? format) => call(format: format);
|
||||
|
||||
@override
|
||||
BangData additionalTriggers(Set<String>? additionalTriggers) =>
|
||||
call(additionalTriggers: additionalTriggers);
|
||||
|
||||
@override
|
||||
BangData snapDomain(String? snapDomain) => call(snapDomain: snapDomain);
|
||||
|
||||
@override
|
||||
BangData frequency(int? frequency) => call(frequency: frequency);
|
||||
|
||||
@override
|
||||
BangData lastUsed(DateTime? lastUsed) => call(lastUsed: lastUsed);
|
||||
|
||||
@override
|
||||
BangData icon(BrowserIcon? icon) => call(icon: icon);
|
||||
|
||||
@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 `BangData(...).copyWith.fieldName(value)`.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// BangData(...).copyWith(id: 12, name: "My name")
|
||||
/// ```
|
||||
BangData call({
|
||||
Object? websiteName = const $CopyWithPlaceholder(),
|
||||
Object? domain = const $CopyWithPlaceholder(),
|
||||
Object? trigger = const $CopyWithPlaceholder(),
|
||||
Object? urlTemplate = const $CopyWithPlaceholder(),
|
||||
Object? searxngApi = const $CopyWithPlaceholder(),
|
||||
Object? category = const $CopyWithPlaceholder(),
|
||||
Object? subCategory = const $CopyWithPlaceholder(),
|
||||
Object? format = const $CopyWithPlaceholder(),
|
||||
Object? additionalTriggers = const $CopyWithPlaceholder(),
|
||||
Object? snapDomain = const $CopyWithPlaceholder(),
|
||||
Object? frequency = const $CopyWithPlaceholder(),
|
||||
Object? lastUsed = const $CopyWithPlaceholder(),
|
||||
Object? icon = const $CopyWithPlaceholder(),
|
||||
}) {
|
||||
return BangData._copyWith(
|
||||
websiteName:
|
||||
websiteName == const $CopyWithPlaceholder() || websiteName == null
|
||||
? _value.websiteName
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: websiteName as String,
|
||||
domain: domain == const $CopyWithPlaceholder() || domain == null
|
||||
? _value.domain
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: domain as String,
|
||||
trigger: trigger == const $CopyWithPlaceholder() || trigger == null
|
||||
? _value.trigger
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: trigger as String,
|
||||
urlTemplate:
|
||||
urlTemplate == const $CopyWithPlaceholder() || urlTemplate == null
|
||||
? _value.urlTemplate
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: urlTemplate as String,
|
||||
searxngApi:
|
||||
searxngApi == const $CopyWithPlaceholder() || searxngApi == null
|
||||
? _value.searxngApi
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: searxngApi as bool,
|
||||
category: category == const $CopyWithPlaceholder()
|
||||
? _value.category
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: category as String?,
|
||||
subCategory: subCategory == const $CopyWithPlaceholder()
|
||||
? _value.subCategory
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: subCategory as String?,
|
||||
format: format == const $CopyWithPlaceholder()
|
||||
? _value.format
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: format as Set<BangFormat>?,
|
||||
additionalTriggers: additionalTriggers == const $CopyWithPlaceholder()
|
||||
? _value.additionalTriggers
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: additionalTriggers as Set<String>?,
|
||||
snapDomain: snapDomain == const $CopyWithPlaceholder()
|
||||
? _value.snapDomain
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: snapDomain as String?,
|
||||
frequency: frequency == const $CopyWithPlaceholder()
|
||||
? _value.frequency
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: frequency as int?,
|
||||
lastUsed: lastUsed == const $CopyWithPlaceholder()
|
||||
? _value.lastUsed
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: lastUsed as DateTime?,
|
||||
icon: icon == const $CopyWithPlaceholder()
|
||||
? _value.icon
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: icon as BrowserIcon?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension $BangDataCopyWith on BangData {
|
||||
/// Returns a callable class used to build a new instance with modified fields.
|
||||
/// Example: `instanceOfBangData.copyWith(...)` or `instanceOfBangData.copyWith.fieldName(...)`.
|
||||
// ignore: library_private_types_in_public_api
|
||||
_$BangDataCWProxy get copyWith => _$BangDataCWProxyImpl(this);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
enum BangGroup {
|
||||
general(
|
||||
remote:
|
||||
'https://raw.githubusercontent.com/FaFre/bangs/main/data/bangs.json',
|
||||
bundled: 'assets/bangs/bangs.json',
|
||||
),
|
||||
kagi(
|
||||
remote:
|
||||
'https://raw.githubusercontent.com/FaFre/bangs/main/data/kagi_bangs.json',
|
||||
bundled: 'assets/bangs/kagi_bangs.json',
|
||||
),
|
||||
user(remote: null, bundled: null);
|
||||
|
||||
final String? bundled;
|
||||
final String? remote;
|
||||
|
||||
const BangGroup({required this.bundled, required this.remote});
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* 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:json_annotation/json_annotation.dart';
|
||||
import 'package:nullability/nullability.dart';
|
||||
import 'package:weblibre/core/logger.dart';
|
||||
import 'package:weblibre/features/bangs/data/models/bang_group.dart';
|
||||
|
||||
class BangKey {
|
||||
final String trigger;
|
||||
final BangGroup group;
|
||||
|
||||
const BangKey({required this.group, required this.trigger});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '${group.name}::$trigger';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is BangKey &&
|
||||
runtimeType == other.runtimeType &&
|
||||
trigger == other.trigger &&
|
||||
group == other.group;
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(trigger, group);
|
||||
|
||||
static BangKey? tryFromString(String key) {
|
||||
try {
|
||||
var [group, trigger] = key.split('::');
|
||||
|
||||
//Migrate to schema v5
|
||||
if (group == 'assistant') {
|
||||
group = BangGroup.kagi.name;
|
||||
}
|
||||
|
||||
return BangKey(
|
||||
group: BangGroup.values.firstWhere((g) => g.name == group),
|
||||
trigger: trigger,
|
||||
);
|
||||
} catch (e, s) {
|
||||
logger.w(
|
||||
'Failed to parse BangKey from string: "$key"',
|
||||
error: e,
|
||||
stackTrace: s,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class BangKeyConverter implements JsonConverter<BangKey?, String?> {
|
||||
const BangKeyConverter();
|
||||
|
||||
@override
|
||||
BangKey? fromJson(String? json) {
|
||||
return json.mapNotNull((json) => BangKey.tryFromString(json));
|
||||
}
|
||||
|
||||
@override
|
||||
String? toJson(BangKey? object) {
|
||||
return object?.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'package:fast_equatable/fast_equatable.dart';
|
||||
|
||||
class SearchHistoryEntry with FastEquatable {
|
||||
final String searchQuery;
|
||||
final String trigger;
|
||||
final DateTime searchDate;
|
||||
|
||||
SearchHistoryEntry({
|
||||
required this.searchQuery,
|
||||
required this.trigger,
|
||||
required this.searchDate,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get hashParameters => [searchQuery, trigger, searchDate];
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* 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: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/database_registry.dart';
|
||||
import 'package:weblibre/core/filesystem.dart';
|
||||
import 'package:weblibre/features/bangs/data/database/database.dart';
|
||||
|
||||
part 'providers.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
BangDatabase bangDatabase(Ref ref) {
|
||||
final db = BangDatabase(
|
||||
LazyDatabase(() async {
|
||||
final file = File(p.join(filesystem.profileDatabasesDir.path, 'bang.db'));
|
||||
|
||||
// Also work around limitations on old Android versions
|
||||
if (Platform.isAndroid) {
|
||||
await applyWorkaroundToOpenSqlite3OnOldAndroidVersions();
|
||||
}
|
||||
|
||||
return NativeDatabase.createInBackground(file);
|
||||
}),
|
||||
);
|
||||
|
||||
DatabaseRegistry.instance.register('bang', db);
|
||||
|
||||
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(bangDatabase)
|
||||
final bangDatabaseProvider = BangDatabaseProvider._();
|
||||
|
||||
final class BangDatabaseProvider
|
||||
extends $FunctionalProvider<BangDatabase, BangDatabase, BangDatabase>
|
||||
with $Provider<BangDatabase> {
|
||||
BangDatabaseProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'bangDatabaseProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$bangDatabaseHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<BangDatabase> $createElement($ProviderPointer pointer) =>
|
||||
$ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
BangDatabase create(Ref ref) {
|
||||
return bangDatabase(ref);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(BangDatabase value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<BangDatabase>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$bangDatabaseHash() => r'0369d508def140a32c08c0551cefed57b1ca4b26';
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:exceptions/exceptions.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart' show rootBundle;
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:weblibre/core/http_error_handler.dart';
|
||||
import 'package:weblibre/features/bangs/data/models/bang.dart';
|
||||
import 'package:weblibre/features/bangs/data/models/bang_group.dart';
|
||||
|
||||
part 'data_source.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class BangDataSourceService extends _$BangDataSourceService {
|
||||
@override
|
||||
void build() {}
|
||||
|
||||
Future<Result<List<Bang>>> fetchRemoteBangs(Uri url, BangGroup group) {
|
||||
return Result.fromAsync(() async {
|
||||
return await compute((args) async {
|
||||
final client = http.Client();
|
||||
try {
|
||||
final url = Uri.parse(args[0]);
|
||||
final response = await client
|
||||
.get(url)
|
||||
.timeout(const Duration(seconds: 30));
|
||||
|
||||
return jsonDecode(utf8.decode(response.bodyBytes)) as List;
|
||||
} finally {
|
||||
client.close();
|
||||
}
|
||||
}, [url.toString()]).then(
|
||||
(json) => json.map((e) {
|
||||
final bang = Bang.fromJson(e as Map<String, dynamic>);
|
||||
return bang.copyWith.group(group);
|
||||
}).toList(),
|
||||
);
|
||||
}, exceptionHandler: handleHttpError);
|
||||
}
|
||||
|
||||
Future<DateTime> getBundledBangDate(String path) async {
|
||||
final content = await rootBundle.loadString(path);
|
||||
return DateTime.parse(content.trim()).toLocal();
|
||||
}
|
||||
|
||||
Future<Result<List<Bang>>> getBundledBangs(String path, BangGroup? group) {
|
||||
return Result.fromAsync(() async {
|
||||
final content = await rootBundle.loadString(path);
|
||||
final json = jsonDecode(content) as List;
|
||||
|
||||
return json.map((e) {
|
||||
var bang = Bang.fromJson(e as Map<String, dynamic>);
|
||||
|
||||
if (group != null) {
|
||||
bang = bang.copyWith.group(group);
|
||||
}
|
||||
|
||||
return bang;
|
||||
}).toList();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'data_source.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(BangDataSourceService)
|
||||
final bangDataSourceServiceProvider = BangDataSourceServiceProvider._();
|
||||
|
||||
final class BangDataSourceServiceProvider
|
||||
extends $NotifierProvider<BangDataSourceService, void> {
|
||||
BangDataSourceServiceProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'bangDataSourceServiceProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$bangDataSourceServiceHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
BangDataSourceService create() => BangDataSourceService();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(void value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<void>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$bangDataSourceServiceHash() =>
|
||||
r'b1bd96bbd834de0f71af86d7f791d367d0a837be';
|
||||
|
||||
abstract class _$BangDataSourceService 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* 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/riverpod.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:weblibre/features/bangs/data/models/bang_data.dart';
|
||||
import 'package:weblibre/features/bangs/data/models/bang_group.dart';
|
||||
import 'package:weblibre/features/bangs/data/models/bang_key.dart';
|
||||
import 'package:weblibre/features/bangs/data/models/search_history_entry.dart';
|
||||
import 'package:weblibre/features/bangs/domain/repositories/data.dart';
|
||||
import 'package:weblibre/features/bangs/domain/repositories/sync.dart';
|
||||
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
||||
|
||||
part 'bangs.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
Stream<BangData?> defaultSearchBangData(Ref ref) {
|
||||
final key = ref.watch(
|
||||
generalSettingsWithDefaultsProvider.select(
|
||||
(value) => value.defaultSearchProvider,
|
||||
),
|
||||
);
|
||||
|
||||
final repository = ref.watch(bangDataRepositoryProvider.notifier);
|
||||
return repository.watchBang(key);
|
||||
}
|
||||
|
||||
@Riverpod()
|
||||
Stream<BangData?> bangData(Ref ref, BangKey key) {
|
||||
final repository = ref.watch(bangDataRepositoryProvider.notifier);
|
||||
return repository.watchBang(key);
|
||||
}
|
||||
|
||||
@Riverpod()
|
||||
Stream<Map<String, List<String>>> bangCategories(Ref ref) {
|
||||
final repository = ref.watch(bangDataRepositoryProvider.notifier);
|
||||
return repository.watchCategories();
|
||||
}
|
||||
|
||||
@Riverpod()
|
||||
Stream<List<BangData>> bangList(
|
||||
Ref ref, {
|
||||
List<String>? triggers,
|
||||
List<BangGroup>? groups,
|
||||
String? domain,
|
||||
({String category, String? subCategory})? categoryFilter,
|
||||
bool? orderMostFrequentFirst,
|
||||
}) {
|
||||
final repository = ref.watch(bangDataRepositoryProvider.notifier);
|
||||
return repository.watchBangs(
|
||||
triggers: triggers,
|
||||
groups: groups,
|
||||
domain: domain,
|
||||
categoryFilter: categoryFilter,
|
||||
orderMostFrequentFirst: orderMostFrequentFirst,
|
||||
);
|
||||
}
|
||||
|
||||
@Riverpod()
|
||||
Stream<List<BangData>> frequentBangList(Ref ref) {
|
||||
final repository = ref.watch(bangDataRepositoryProvider.notifier);
|
||||
return repository.watchFrequentBangs();
|
||||
}
|
||||
|
||||
@Riverpod()
|
||||
Stream<List<SearchHistoryEntry>> searchHistory(Ref ref) {
|
||||
final repository = ref.watch(bangDataRepositoryProvider.notifier);
|
||||
final maxSearchHistoryEntries = ref.watch(
|
||||
generalSettingsWithDefaultsProvider.select(
|
||||
(s) => s.maxSearchHistoryEntries,
|
||||
),
|
||||
);
|
||||
return repository.watchSearchHistory(limit: maxSearchHistoryEntries);
|
||||
}
|
||||
|
||||
@Riverpod()
|
||||
Stream<DateTime?> lastSyncOfGroup(Ref ref, BangGroup group) {
|
||||
final repository = ref.watch(bangSyncRepositoryProvider.notifier);
|
||||
return repository.watchLastSyncOfGroup(group);
|
||||
}
|
||||
|
||||
@Riverpod()
|
||||
Stream<int> bangCountOfGroup(Ref ref, BangGroup group) {
|
||||
final repository = ref.watch(bangDataRepositoryProvider.notifier);
|
||||
return repository.watchBangCount(group);
|
||||
}
|
||||
@@ -0,0 +1,496 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'bangs.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(defaultSearchBangData)
|
||||
final defaultSearchBangDataProvider = DefaultSearchBangDataProvider._();
|
||||
|
||||
final class DefaultSearchBangDataProvider
|
||||
extends
|
||||
$FunctionalProvider<AsyncValue<BangData?>, BangData?, Stream<BangData?>>
|
||||
with $FutureModifier<BangData?>, $StreamProvider<BangData?> {
|
||||
DefaultSearchBangDataProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'defaultSearchBangDataProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$defaultSearchBangDataHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$StreamProviderElement<BangData?> $createElement($ProviderPointer pointer) =>
|
||||
$StreamProviderElement(pointer);
|
||||
|
||||
@override
|
||||
Stream<BangData?> create(Ref ref) {
|
||||
return defaultSearchBangData(ref);
|
||||
}
|
||||
}
|
||||
|
||||
String _$defaultSearchBangDataHash() =>
|
||||
r'5f43b8989219cf3cb2f5ca65df351b6cb100427f';
|
||||
|
||||
@ProviderFor(bangData)
|
||||
final bangDataProvider = BangDataFamily._();
|
||||
|
||||
final class BangDataProvider
|
||||
extends
|
||||
$FunctionalProvider<AsyncValue<BangData?>, BangData?, Stream<BangData?>>
|
||||
with $FutureModifier<BangData?>, $StreamProvider<BangData?> {
|
||||
BangDataProvider._({
|
||||
required BangDataFamily super.from,
|
||||
required BangKey super.argument,
|
||||
}) : super(
|
||||
retry: null,
|
||||
name: r'bangDataProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$bangDataHash();
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return r'bangDataProvider'
|
||||
''
|
||||
'($argument)';
|
||||
}
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$StreamProviderElement<BangData?> $createElement($ProviderPointer pointer) =>
|
||||
$StreamProviderElement(pointer);
|
||||
|
||||
@override
|
||||
Stream<BangData?> create(Ref ref) {
|
||||
final argument = this.argument as BangKey;
|
||||
return bangData(ref, argument);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is BangDataProvider && other.argument == argument;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return argument.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
String _$bangDataHash() => r'bd9f5ec8b29aab74620a9b5a4246cb7e3b2fd377';
|
||||
|
||||
final class BangDataFamily extends $Family
|
||||
with $FunctionalFamilyOverride<Stream<BangData?>, BangKey> {
|
||||
BangDataFamily._()
|
||||
: super(
|
||||
retry: null,
|
||||
name: r'bangDataProvider',
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
isAutoDispose: true,
|
||||
);
|
||||
|
||||
BangDataProvider call(BangKey key) =>
|
||||
BangDataProvider._(argument: key, from: this);
|
||||
|
||||
@override
|
||||
String toString() => r'bangDataProvider';
|
||||
}
|
||||
|
||||
@ProviderFor(bangCategories)
|
||||
final bangCategoriesProvider = BangCategoriesProvider._();
|
||||
|
||||
final class BangCategoriesProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
AsyncValue<Map<String, List<String>>>,
|
||||
Map<String, List<String>>,
|
||||
Stream<Map<String, List<String>>>
|
||||
>
|
||||
with
|
||||
$FutureModifier<Map<String, List<String>>>,
|
||||
$StreamProvider<Map<String, List<String>>> {
|
||||
BangCategoriesProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'bangCategoriesProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$bangCategoriesHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$StreamProviderElement<Map<String, List<String>>> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $StreamProviderElement(pointer);
|
||||
|
||||
@override
|
||||
Stream<Map<String, List<String>>> create(Ref ref) {
|
||||
return bangCategories(ref);
|
||||
}
|
||||
}
|
||||
|
||||
String _$bangCategoriesHash() => r'947fcfd2dffcc7f585c6ed7379d319f4fe72293a';
|
||||
|
||||
@ProviderFor(bangList)
|
||||
final bangListProvider = BangListFamily._();
|
||||
|
||||
final class BangListProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
AsyncValue<List<BangData>>,
|
||||
List<BangData>,
|
||||
Stream<List<BangData>>
|
||||
>
|
||||
with $FutureModifier<List<BangData>>, $StreamProvider<List<BangData>> {
|
||||
BangListProvider._({
|
||||
required BangListFamily super.from,
|
||||
required ({
|
||||
List<String>? triggers,
|
||||
List<BangGroup>? groups,
|
||||
String? domain,
|
||||
({String category, String? subCategory})? categoryFilter,
|
||||
bool? orderMostFrequentFirst,
|
||||
})
|
||||
super.argument,
|
||||
}) : super(
|
||||
retry: null,
|
||||
name: r'bangListProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$bangListHash();
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return r'bangListProvider'
|
||||
''
|
||||
'$argument';
|
||||
}
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$StreamProviderElement<List<BangData>> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $StreamProviderElement(pointer);
|
||||
|
||||
@override
|
||||
Stream<List<BangData>> create(Ref ref) {
|
||||
final argument =
|
||||
this.argument
|
||||
as ({
|
||||
List<String>? triggers,
|
||||
List<BangGroup>? groups,
|
||||
String? domain,
|
||||
({String category, String? subCategory})? categoryFilter,
|
||||
bool? orderMostFrequentFirst,
|
||||
});
|
||||
return bangList(
|
||||
ref,
|
||||
triggers: argument.triggers,
|
||||
groups: argument.groups,
|
||||
domain: argument.domain,
|
||||
categoryFilter: argument.categoryFilter,
|
||||
orderMostFrequentFirst: argument.orderMostFrequentFirst,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is BangListProvider && other.argument == argument;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return argument.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
String _$bangListHash() => r'd1e0bb9fa4f523ce516e075c0d149bf7803ebb2b';
|
||||
|
||||
final class BangListFamily extends $Family
|
||||
with
|
||||
$FunctionalFamilyOverride<
|
||||
Stream<List<BangData>>,
|
||||
({
|
||||
List<String>? triggers,
|
||||
List<BangGroup>? groups,
|
||||
String? domain,
|
||||
({String category, String? subCategory})? categoryFilter,
|
||||
bool? orderMostFrequentFirst,
|
||||
})
|
||||
> {
|
||||
BangListFamily._()
|
||||
: super(
|
||||
retry: null,
|
||||
name: r'bangListProvider',
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
isAutoDispose: true,
|
||||
);
|
||||
|
||||
BangListProvider call({
|
||||
List<String>? triggers,
|
||||
List<BangGroup>? groups,
|
||||
String? domain,
|
||||
({String category, String? subCategory})? categoryFilter,
|
||||
bool? orderMostFrequentFirst,
|
||||
}) => BangListProvider._(
|
||||
argument: (
|
||||
triggers: triggers,
|
||||
groups: groups,
|
||||
domain: domain,
|
||||
categoryFilter: categoryFilter,
|
||||
orderMostFrequentFirst: orderMostFrequentFirst,
|
||||
),
|
||||
from: this,
|
||||
);
|
||||
|
||||
@override
|
||||
String toString() => r'bangListProvider';
|
||||
}
|
||||
|
||||
@ProviderFor(frequentBangList)
|
||||
final frequentBangListProvider = FrequentBangListProvider._();
|
||||
|
||||
final class FrequentBangListProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
AsyncValue<List<BangData>>,
|
||||
List<BangData>,
|
||||
Stream<List<BangData>>
|
||||
>
|
||||
with $FutureModifier<List<BangData>>, $StreamProvider<List<BangData>> {
|
||||
FrequentBangListProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'frequentBangListProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$frequentBangListHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$StreamProviderElement<List<BangData>> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $StreamProviderElement(pointer);
|
||||
|
||||
@override
|
||||
Stream<List<BangData>> create(Ref ref) {
|
||||
return frequentBangList(ref);
|
||||
}
|
||||
}
|
||||
|
||||
String _$frequentBangListHash() => r'2c1ecb7e9416772fc1c32d01d767e1eb4f865975';
|
||||
|
||||
@ProviderFor(searchHistory)
|
||||
final searchHistoryProvider = SearchHistoryProvider._();
|
||||
|
||||
final class SearchHistoryProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
AsyncValue<List<SearchHistoryEntry>>,
|
||||
List<SearchHistoryEntry>,
|
||||
Stream<List<SearchHistoryEntry>>
|
||||
>
|
||||
with
|
||||
$FutureModifier<List<SearchHistoryEntry>>,
|
||||
$StreamProvider<List<SearchHistoryEntry>> {
|
||||
SearchHistoryProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'searchHistoryProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$searchHistoryHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$StreamProviderElement<List<SearchHistoryEntry>> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $StreamProviderElement(pointer);
|
||||
|
||||
@override
|
||||
Stream<List<SearchHistoryEntry>> create(Ref ref) {
|
||||
return searchHistory(ref);
|
||||
}
|
||||
}
|
||||
|
||||
String _$searchHistoryHash() => r'5f9508a6b286bfcd1b641bd429de46ad052a3dfe';
|
||||
|
||||
@ProviderFor(lastSyncOfGroup)
|
||||
final lastSyncOfGroupProvider = LastSyncOfGroupFamily._();
|
||||
|
||||
final class LastSyncOfGroupProvider
|
||||
extends
|
||||
$FunctionalProvider<AsyncValue<DateTime?>, DateTime?, Stream<DateTime?>>
|
||||
with $FutureModifier<DateTime?>, $StreamProvider<DateTime?> {
|
||||
LastSyncOfGroupProvider._({
|
||||
required LastSyncOfGroupFamily super.from,
|
||||
required BangGroup super.argument,
|
||||
}) : super(
|
||||
retry: null,
|
||||
name: r'lastSyncOfGroupProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$lastSyncOfGroupHash();
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return r'lastSyncOfGroupProvider'
|
||||
''
|
||||
'($argument)';
|
||||
}
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$StreamProviderElement<DateTime?> $createElement($ProviderPointer pointer) =>
|
||||
$StreamProviderElement(pointer);
|
||||
|
||||
@override
|
||||
Stream<DateTime?> create(Ref ref) {
|
||||
final argument = this.argument as BangGroup;
|
||||
return lastSyncOfGroup(ref, argument);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is LastSyncOfGroupProvider && other.argument == argument;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return argument.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
String _$lastSyncOfGroupHash() => r'23d07f3132ba9bb35a31f74e3a69698d31d4c569';
|
||||
|
||||
final class LastSyncOfGroupFamily extends $Family
|
||||
with $FunctionalFamilyOverride<Stream<DateTime?>, BangGroup> {
|
||||
LastSyncOfGroupFamily._()
|
||||
: super(
|
||||
retry: null,
|
||||
name: r'lastSyncOfGroupProvider',
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
isAutoDispose: true,
|
||||
);
|
||||
|
||||
LastSyncOfGroupProvider call(BangGroup group) =>
|
||||
LastSyncOfGroupProvider._(argument: group, from: this);
|
||||
|
||||
@override
|
||||
String toString() => r'lastSyncOfGroupProvider';
|
||||
}
|
||||
|
||||
@ProviderFor(bangCountOfGroup)
|
||||
final bangCountOfGroupProvider = BangCountOfGroupFamily._();
|
||||
|
||||
final class BangCountOfGroupProvider
|
||||
extends $FunctionalProvider<AsyncValue<int>, int, Stream<int>>
|
||||
with $FutureModifier<int>, $StreamProvider<int> {
|
||||
BangCountOfGroupProvider._({
|
||||
required BangCountOfGroupFamily super.from,
|
||||
required BangGroup super.argument,
|
||||
}) : super(
|
||||
retry: null,
|
||||
name: r'bangCountOfGroupProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$bangCountOfGroupHash();
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return r'bangCountOfGroupProvider'
|
||||
''
|
||||
'($argument)';
|
||||
}
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$StreamProviderElement<int> $createElement($ProviderPointer pointer) =>
|
||||
$StreamProviderElement(pointer);
|
||||
|
||||
@override
|
||||
Stream<int> create(Ref ref) {
|
||||
final argument = this.argument as BangGroup;
|
||||
return bangCountOfGroup(ref, argument);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is BangCountOfGroupProvider && other.argument == argument;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return argument.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
String _$bangCountOfGroupHash() => r'211ffcd7f49b637a7953f913dc5eafda344423f3';
|
||||
|
||||
final class BangCountOfGroupFamily extends $Family
|
||||
with $FunctionalFamilyOverride<Stream<int>, BangGroup> {
|
||||
BangCountOfGroupFamily._()
|
||||
: super(
|
||||
retry: null,
|
||||
name: r'bangCountOfGroupProvider',
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
isAutoDispose: true,
|
||||
);
|
||||
|
||||
BangCountOfGroupProvider call(BangGroup group) =>
|
||||
BangCountOfGroupProvider._(argument: group, from: this);
|
||||
|
||||
@override
|
||||
String toString() => r'bangCountOfGroupProvider';
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:weblibre/features/bangs/data/models/bang_data.dart';
|
||||
import 'package:weblibre/features/bangs/data/providers.dart';
|
||||
import 'package:weblibre/features/bangs/domain/providers/bangs.dart';
|
||||
import 'package:weblibre/features/bangs/domain/repositories/data.dart';
|
||||
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
||||
|
||||
part 'search.g.dart';
|
||||
|
||||
@Riverpod()
|
||||
class BangSearch extends _$BangSearch {
|
||||
late StreamController<List<BangData>> _streamController;
|
||||
|
||||
Future<Uri> triggerBangSearch(BangData bang, String searchQuery) async {
|
||||
final bangDataNotifier = ref.read(bangDataRepositoryProvider.notifier);
|
||||
final settings = ref.read(generalSettingsWithDefaultsProvider);
|
||||
|
||||
await bangDataNotifier.increaseFrequency(bang.toKey());
|
||||
await bangDataNotifier.addSearchEntry(
|
||||
bang.group,
|
||||
bang.trigger,
|
||||
searchQuery,
|
||||
maxEntryCount: settings.maxSearchHistoryEntries,
|
||||
);
|
||||
|
||||
return bang.getTemplateUrl(searchQuery);
|
||||
}
|
||||
|
||||
Future<void> search(String input) async {
|
||||
if (input.isNotEmpty) {
|
||||
await ref.read(bangDatabaseProvider).bangDao.queryBangs(input).get().then(
|
||||
(value) {
|
||||
if (!_streamController.isClosed) {
|
||||
_streamController.add(value);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<List<BangData>> build() {
|
||||
_streamController = StreamController();
|
||||
|
||||
// Emit initial empty list so UI doesn't show loading state
|
||||
_streamController.add([]);
|
||||
|
||||
ref.onDispose(() async {
|
||||
await _streamController.close();
|
||||
});
|
||||
|
||||
return _streamController.stream;
|
||||
}
|
||||
}
|
||||
|
||||
@Riverpod()
|
||||
class SeamlessBang extends _$SeamlessBang {
|
||||
bool _hasSearch = false;
|
||||
|
||||
void search(String input) {
|
||||
if (input.isNotEmpty) {
|
||||
if (!_hasSearch) {
|
||||
_hasSearch = true;
|
||||
ref.invalidateSelf();
|
||||
}
|
||||
|
||||
//Don't block
|
||||
unawaited(ref.read(bangSearchProvider.notifier).search(input));
|
||||
} else if (_hasSearch) {
|
||||
_hasSearch = false;
|
||||
ref.invalidateSelf();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
AsyncValue<List<BangData>> build() {
|
||||
return _hasSearch
|
||||
? ref.watch(bangSearchProvider)
|
||||
: ref.watch(frequentBangListProvider);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'search.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(BangSearch)
|
||||
final bangSearchProvider = BangSearchProvider._();
|
||||
|
||||
final class BangSearchProvider
|
||||
extends $StreamNotifierProvider<BangSearch, List<BangData>> {
|
||||
BangSearchProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'bangSearchProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$bangSearchHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
BangSearch create() => BangSearch();
|
||||
}
|
||||
|
||||
String _$bangSearchHash() => r'feed24edfe703b0697f4a855be9c7359c456b0f2';
|
||||
|
||||
abstract class _$BangSearch extends $StreamNotifier<List<BangData>> {
|
||||
Stream<List<BangData>> build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final ref = this.ref as $Ref<AsyncValue<List<BangData>>, List<BangData>>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<AsyncValue<List<BangData>>, List<BangData>>,
|
||||
AsyncValue<List<BangData>>,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleCreate(ref, build);
|
||||
}
|
||||
}
|
||||
|
||||
@ProviderFor(SeamlessBang)
|
||||
final seamlessBangProvider = SeamlessBangProvider._();
|
||||
|
||||
final class SeamlessBangProvider
|
||||
extends $NotifierProvider<SeamlessBang, AsyncValue<List<BangData>>> {
|
||||
SeamlessBangProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'seamlessBangProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$seamlessBangHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
SeamlessBang create() => SeamlessBang();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(AsyncValue<List<BangData>> value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<AsyncValue<List<BangData>>>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$seamlessBangHash() => r'8bd7a2cbe4c302ae08f85167290666a7437f8b9b';
|
||||
|
||||
abstract class _$SeamlessBang extends $Notifier<AsyncValue<List<BangData>>> {
|
||||
AsyncValue<List<BangData>> build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final ref =
|
||||
this.ref
|
||||
as $Ref<AsyncValue<List<BangData>>, AsyncValue<List<BangData>>>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<
|
||||
AsyncValue<List<BangData>>,
|
||||
AsyncValue<List<BangData>>
|
||||
>,
|
||||
AsyncValue<List<BangData>>,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleCreate(ref, build);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
/*
|
||||
* 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:convert';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:weblibre/features/bangs/data/models/bang.dart';
|
||||
import 'package:weblibre/features/bangs/data/models/bang_data.dart';
|
||||
import 'package:weblibre/features/bangs/data/models/bang_group.dart';
|
||||
import 'package:weblibre/features/bangs/data/models/bang_key.dart';
|
||||
import 'package:weblibre/features/bangs/data/models/search_history_entry.dart';
|
||||
import 'package:weblibre/features/bangs/data/providers.dart';
|
||||
|
||||
part 'data.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class BangDataRepository extends _$BangDataRepository {
|
||||
@override
|
||||
void build() {}
|
||||
|
||||
Stream<BangData?> watchBang(BangKey? key) {
|
||||
if (key != null) {
|
||||
return ref
|
||||
.read(bangDatabaseProvider)
|
||||
.bangDao
|
||||
.getBangData(key.group, key.trigger)
|
||||
.watchSingleOrNull();
|
||||
} else {
|
||||
return Stream.value(null);
|
||||
}
|
||||
}
|
||||
|
||||
Stream<Map<String, List<String>>> watchCategories() {
|
||||
return ref
|
||||
.read(bangDatabaseProvider)
|
||||
.definitionsDrift
|
||||
.categoriesJson()
|
||||
.watchSingle()
|
||||
.map((json) {
|
||||
final decoded = jsonDecode(json) as Map<String, dynamic>;
|
||||
return decoded.map(
|
||||
(key, value) => MapEntry(key, (value as List<dynamic>).cast()),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Stream<int> watchBangCount(BangGroup group) {
|
||||
return ref
|
||||
.read(bangDatabaseProvider)
|
||||
.bangDao
|
||||
.getBangCount(groups: [group])
|
||||
.watchSingle();
|
||||
}
|
||||
|
||||
Stream<List<BangData>> watchBangs({
|
||||
Iterable<String>? triggers,
|
||||
Iterable<BangGroup>? groups,
|
||||
String? domain,
|
||||
({String category, String? subCategory})? categoryFilter,
|
||||
bool? orderMostFrequentFirst,
|
||||
}) {
|
||||
return ref
|
||||
.read(bangDatabaseProvider)
|
||||
.bangDao
|
||||
.getBangDataList(
|
||||
triggers: triggers,
|
||||
groups: groups,
|
||||
domain: domain,
|
||||
category: categoryFilter?.category,
|
||||
subCategory: categoryFilter?.subCategory,
|
||||
orderMostFrequentFirst: orderMostFrequentFirst,
|
||||
)
|
||||
.watch();
|
||||
}
|
||||
|
||||
Stream<List<BangData>> watchFrequentBangs({Iterable<BangGroup>? groups}) {
|
||||
return ref
|
||||
.read(bangDatabaseProvider)
|
||||
.bangDao
|
||||
.getFrequentBangDataList(groups: groups)
|
||||
.watch();
|
||||
}
|
||||
|
||||
Stream<List<SearchHistoryEntry>> watchSearchHistory({required int limit}) {
|
||||
return ref
|
||||
.read(bangDatabaseProvider)
|
||||
.definitionsDrift
|
||||
.searchHistoryEntries(limit: limit)
|
||||
.watch();
|
||||
}
|
||||
|
||||
Future<void> increaseFrequency(BangKey key) {
|
||||
return ref.read(bangDatabaseProvider).bangDao.increaseBangFrequency(key);
|
||||
}
|
||||
|
||||
Future<void> addSearchEntry(
|
||||
BangGroup group,
|
||||
String trigger,
|
||||
String searchQuery, {
|
||||
required int maxEntryCount,
|
||||
}) async {
|
||||
// Skip capturing history if maxEntryCount is 0
|
||||
if (maxEntryCount <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
final db = ref.read(bangDatabaseProvider);
|
||||
//Pack in a transaction to bundle rebuilds of watch() queries
|
||||
return db.transaction(() async {
|
||||
await db.bangDao.addSearchEntry(group, trigger, searchQuery);
|
||||
await db.definitionsDrift.evictHistoryEntries(limit: maxEntryCount);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> removeSearchEntry(String searchQuery) {
|
||||
return ref
|
||||
.read(bangDatabaseProvider)
|
||||
.bangDao
|
||||
.removeSearchEntry(searchQuery);
|
||||
}
|
||||
|
||||
Future<int> resetFrequencies() {
|
||||
return ref.read(bangDatabaseProvider).bangFrequency.deleteAll();
|
||||
}
|
||||
|
||||
Future<int> resetFrequency(String trigger) {
|
||||
return ref
|
||||
.read(bangDatabaseProvider)
|
||||
.bangFrequency
|
||||
.deleteWhere((t) => t.trigger.equals(trigger));
|
||||
}
|
||||
|
||||
Future<BangData?> getBang(BangKey key) {
|
||||
return ref
|
||||
.read(bangDatabaseProvider)
|
||||
.bangDao
|
||||
.getBangData(key.group, key.trigger)
|
||||
.getSingleOrNull();
|
||||
}
|
||||
|
||||
Future<void> upsertBang(Bang bang) {
|
||||
return ref.read(bangDatabaseProvider).bangDao.upsertBang(bang);
|
||||
}
|
||||
|
||||
Future<void> deleteBang(BangKey key) {
|
||||
return ref.read(bangDatabaseProvider).syncDao.deleteBangs(key.group, [
|
||||
key.trigger,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'data.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(BangDataRepository)
|
||||
final bangDataRepositoryProvider = BangDataRepositoryProvider._();
|
||||
|
||||
final class BangDataRepositoryProvider
|
||||
extends $NotifierProvider<BangDataRepository, void> {
|
||||
BangDataRepositoryProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'bangDataRepositoryProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$bangDataRepositoryHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
BangDataRepository create() => BangDataRepository();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(void value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<void>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$bangDataRepositoryHash() =>
|
||||
r'c562ef10d75ca6dee13805f84d2491cf2a89aaac';
|
||||
|
||||
abstract class _$BangDataRepository 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
/*
|
||||
* 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:exceptions/exceptions.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:weblibre/features/bangs/data/database/database.dart';
|
||||
import 'package:weblibre/features/bangs/data/models/bang_group.dart';
|
||||
import 'package:weblibre/features/bangs/data/providers.dart';
|
||||
import 'package:weblibre/features/bangs/data/services/data_source.dart';
|
||||
|
||||
part 'sync.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class BangSyncRepository extends _$BangSyncRepository {
|
||||
static Future<Result<void>> _fetchAndSyncRemote({
|
||||
required BangDataSourceService sourceService,
|
||||
required BangDatabase db,
|
||||
required Uri url,
|
||||
required BangGroup group,
|
||||
required Duration? syncInterval,
|
||||
}) async {
|
||||
if (syncInterval != null) {
|
||||
final lastSync = await db.syncDao
|
||||
.getLastSyncOfGroup(group)
|
||||
.getSingleOrNull();
|
||||
|
||||
if (lastSync != null &&
|
||||
DateTime.now().difference(lastSync) < syncInterval) {
|
||||
return Result.success(null);
|
||||
}
|
||||
}
|
||||
|
||||
final result = await sourceService.fetchRemoteBangs(url, group);
|
||||
return result.flatMapAsync((remoteBangs) async {
|
||||
await db.syncDao.syncBangs(
|
||||
group: group,
|
||||
remoteBangs: remoteBangs,
|
||||
syncTime: DateTime.now(),
|
||||
);
|
||||
await db.definitionsDrift.optimizeBangFtsIndex();
|
||||
await db.definitionsDrift.optimizeTriggerFtsIndex();
|
||||
});
|
||||
}
|
||||
|
||||
static Future<Result<void>> _fetchAndSyncBundled({
|
||||
required BangDataSourceService sourceService,
|
||||
required BangDatabase db,
|
||||
required BangGroup group,
|
||||
}) async {
|
||||
if (group.bundled == null) {
|
||||
return Result.failure(
|
||||
const ErrorMessage(source: 'BangSync', message: 'Not bundled'),
|
||||
);
|
||||
}
|
||||
|
||||
if (group.remote == null) {
|
||||
return Result.failure(
|
||||
const ErrorMessage(source: 'BangSync', message: 'No remote source'),
|
||||
);
|
||||
}
|
||||
|
||||
final lastSync = await db.syncDao
|
||||
.getLastSyncOfGroup(group)
|
||||
.getSingleOrNull();
|
||||
|
||||
final sourceDate = await sourceService.getBundledBangDate(
|
||||
'assets/bangs/last_sync.txt',
|
||||
);
|
||||
|
||||
if (lastSync != null &&
|
||||
(sourceDate == lastSync ||
|
||||
sourceDate.difference(lastSync).isNegative)) {
|
||||
return Result.success(null);
|
||||
}
|
||||
|
||||
final result = await sourceService.getBundledBangs(group.bundled!, group);
|
||||
return result.flatMapAsync((remoteBangs) async {
|
||||
await db.syncDao.syncBangs(
|
||||
group: group,
|
||||
remoteBangs: remoteBangs,
|
||||
syncTime: sourceDate,
|
||||
);
|
||||
await db.definitionsDrift.optimizeBangFtsIndex();
|
||||
await db.definitionsDrift.optimizeTriggerFtsIndex();
|
||||
});
|
||||
}
|
||||
|
||||
Future<Result<void>> syncRemoteBangGroup(
|
||||
BangGroup group,
|
||||
Duration? syncInterval,
|
||||
) async {
|
||||
try {
|
||||
return Result.success(
|
||||
await ref
|
||||
.read(bangDatabaseProvider)
|
||||
.computeWithDatabase(
|
||||
connect: BangDatabase.new,
|
||||
computation: (db) async {
|
||||
final ref = ProviderContainer();
|
||||
final result = await _fetchAndSyncRemote(
|
||||
sourceService: ref.read(
|
||||
bangDataSourceServiceProvider.notifier,
|
||||
),
|
||||
db: db,
|
||||
url: Uri.parse(group.remote!),
|
||||
group: group,
|
||||
syncInterval: syncInterval,
|
||||
);
|
||||
|
||||
//Throw if necessary
|
||||
return result.value;
|
||||
},
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
return Result.failure(
|
||||
ErrorMessage(
|
||||
message: "Failed to sync Bangs (${group.name})",
|
||||
source: 'BangSync',
|
||||
details: e,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<Result<void>> syncBundledBangGroup(BangGroup group) async {
|
||||
try {
|
||||
final db = ref.read(bangDatabaseProvider);
|
||||
|
||||
final result = await _fetchAndSyncBundled(
|
||||
sourceService: ref.read(bangDataSourceServiceProvider.notifier),
|
||||
db: db,
|
||||
group: group,
|
||||
);
|
||||
|
||||
//Throw if necessary
|
||||
return result;
|
||||
} catch (e) {
|
||||
return Result.failure(
|
||||
ErrorMessage(
|
||||
message: "Failed to sync Bangs (${group.name})",
|
||||
source: 'BangSync',
|
||||
details: e,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Stream<DateTime?> watchLastSyncOfGroup(BangGroup group) {
|
||||
return ref
|
||||
.read(bangDatabaseProvider)
|
||||
.syncDao
|
||||
.getLastSyncOfGroup(group)
|
||||
.watchSingleOrNull();
|
||||
}
|
||||
|
||||
Future<Map<BangGroup, Result<void>>> syncBundledBangGroups({
|
||||
Set<BangGroup>? groups,
|
||||
}) async {
|
||||
//Default to all sources
|
||||
groups ??= BangGroup.values.where((e) => e.bundled != null).toSet();
|
||||
|
||||
//Run isolated operations
|
||||
final futures = groups.map(
|
||||
(source) => syncBundledBangGroup(
|
||||
source,
|
||||
).then((result) => MapEntry(source, result)),
|
||||
);
|
||||
|
||||
return Map.fromEntries(await Future.wait(futures));
|
||||
}
|
||||
|
||||
@override
|
||||
void build() {}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'sync.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(BangSyncRepository)
|
||||
final bangSyncRepositoryProvider = BangSyncRepositoryProvider._();
|
||||
|
||||
final class BangSyncRepositoryProvider
|
||||
extends $NotifierProvider<BangSyncRepository, void> {
|
||||
BangSyncRepositoryProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'bangSyncRepositoryProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$bangSyncRepositoryHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
BangSyncRepository create() => BangSyncRepository();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(void value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<void>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$bangSyncRepositoryHash() =>
|
||||
r'1347dcbd03f6a1a4fa3bbbae5d9302098d260c50';
|
||||
|
||||
abstract class _$BangSyncRepository 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* 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/riverpod.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:weblibre/features/bangs/data/providers.dart';
|
||||
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
||||
|
||||
part 'search_history_cleanup.g.dart';
|
||||
|
||||
/// Service that listens to maxSearchHistoryEntries setting changes
|
||||
/// and cleans up search history when the limit is reduced.
|
||||
@Riverpod(keepAlive: true)
|
||||
class SearchHistoryCleanupService extends _$SearchHistoryCleanupService {
|
||||
@override
|
||||
void build() {
|
||||
ref.listen(
|
||||
generalSettingsWithDefaultsProvider.select(
|
||||
(settings) => settings.maxSearchHistoryEntries,
|
||||
),
|
||||
(previous, next) async {
|
||||
// Only cleanup when limit is reduced (including to 0)
|
||||
if (previous != null && next < previous) {
|
||||
final db = ref.read(bangDatabaseProvider);
|
||||
await db.definitionsDrift.evictHistoryEntries(limit: next);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user