prepare for multiple apps

This commit is contained in:
Fabian Freund
2026-04-06 12:23:11 +02:00
parent bd1600e8dc
commit 5afc323f04
904 changed files with 29 additions and 29 deletions
@@ -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');
}
}
+429
View File
@@ -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),
};
}
+38
View File
@@ -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());
}
}
+151
View File
@@ -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,
);
}
+23
View File
@@ -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 }
+24
View File
@@ -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()));