prepare for multiple apps
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:weblibre/core/logger.dart';
|
||||
|
||||
class DatabaseRegistry {
|
||||
DatabaseRegistry._();
|
||||
static final instance = DatabaseRegistry._();
|
||||
|
||||
final _databases = <String, GeneratedDatabase>{};
|
||||
|
||||
void register(String name, GeneratedDatabase db) {
|
||||
_databases[name] = db;
|
||||
}
|
||||
|
||||
Future<void> closeAll() async {
|
||||
for (final entry in _databases.entries) {
|
||||
try {
|
||||
await entry.value.close();
|
||||
logger.i('${entry.key} database closed');
|
||||
} catch (e, st) {
|
||||
logger.e(
|
||||
'Failed to close ${entry.key} database',
|
||||
error: e,
|
||||
stackTrace: st,
|
||||
);
|
||||
}
|
||||
}
|
||||
_databases.clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
@immutable
|
||||
class AppColors extends ThemeExtension<AppColors> {
|
||||
const AppColors._({
|
||||
required this.seedColor,
|
||||
required this.privateTabPurple,
|
||||
required this.privateTabBackground,
|
||||
required this.privateTabForeground,
|
||||
required this.privateSelectionOverlay,
|
||||
required this.isolatedTabTeal,
|
||||
required this.isolatedTabBackground,
|
||||
required this.isolatedTabForeground,
|
||||
required this.isolatedSelectionOverlay,
|
||||
required this.torPurple,
|
||||
required this.torActiveGreen,
|
||||
required this.torBackgroundGrey,
|
||||
required this.warningAmber,
|
||||
required this.auraPurple,
|
||||
required this.auraGold,
|
||||
required this.auraShadow,
|
||||
required this.auraShadowHighlight,
|
||||
required this.auraTint,
|
||||
required this.brandLink,
|
||||
});
|
||||
|
||||
final Color seedColor;
|
||||
final Color privateTabPurple;
|
||||
final Color privateTabBackground;
|
||||
final Color privateTabForeground;
|
||||
final Color privateSelectionOverlay;
|
||||
final Color isolatedTabTeal;
|
||||
final Color isolatedTabBackground;
|
||||
final Color isolatedTabForeground;
|
||||
final Color isolatedSelectionOverlay;
|
||||
final Color torPurple;
|
||||
final Color torActiveGreen;
|
||||
final Color torBackgroundGrey;
|
||||
final Color warningAmber;
|
||||
final Color auraPurple;
|
||||
final Color auraGold;
|
||||
final Color auraShadow;
|
||||
final Color auraShadowHighlight;
|
||||
final Color auraTint;
|
||||
final Color brandLink;
|
||||
|
||||
/// Brand colors derived from the logo (constant across themes).
|
||||
static const brandPurple = Color(0xFF9C83F8);
|
||||
static const brandYellow = Color(0xFFFBDC6B);
|
||||
static const brandGrey = Color(0xFFA7A7A7);
|
||||
|
||||
static const light = AppColors._(
|
||||
seedColor: Color(0xFF167C80),
|
||||
privateTabPurple: Color(0xFF8000D7),
|
||||
privateTabBackground: Color(0xFFF3E5F5),
|
||||
privateTabForeground: Color(0xFF4A0072),
|
||||
privateSelectionOverlay: Color(0x648000D7),
|
||||
isolatedTabTeal: Color(0xFF00897B),
|
||||
isolatedTabBackground: Color(0xFFE0F2F1),
|
||||
isolatedTabForeground: Color(0xFF004D40),
|
||||
isolatedSelectionOverlay: Color(0x6400897B),
|
||||
torPurple: Color(0xFF7D4698),
|
||||
torActiveGreen: Color(0xFF68B030),
|
||||
torBackgroundGrey: Color(0xFFECEFF1),
|
||||
warningAmber: Color(0xFFFFA000),
|
||||
// Aura: brand colors lightened toward white
|
||||
auraPurple: Color(0xFFE0D6FC),
|
||||
auraGold: Color(0xFFFDF1D6),
|
||||
auraShadow: Color(0xFFEDEDF0),
|
||||
auraShadowHighlight: Color(0xFFE2E2E7),
|
||||
auraTint: Color(0xFFFFFFFF),
|
||||
brandLink: Color(0xFF7A5AF0),
|
||||
);
|
||||
|
||||
static const dark = AppColors._(
|
||||
seedColor: Color(0xFF167C80),
|
||||
privateTabPurple: Color(0xFF8000D7),
|
||||
privateTabBackground: Color(0xFF25003E),
|
||||
privateTabForeground: Color(0xFFFFFFFF),
|
||||
privateSelectionOverlay: Color(0x648000D7),
|
||||
isolatedTabTeal: Color(0xFF00897B),
|
||||
isolatedTabBackground: Color(0xFF003D36),
|
||||
isolatedTabForeground: Color(0xFFFFFFFF),
|
||||
isolatedSelectionOverlay: Color(0x6400897B),
|
||||
torPurple: Color(0xFF7D4698),
|
||||
torActiveGreen: Color(0xFF68B030),
|
||||
torBackgroundGrey: Color(0xFF333A41),
|
||||
warningAmber: Color(0xFFFFA000),
|
||||
// Aura: brand colors darkened toward black
|
||||
auraPurple: Color(0xFF2C2543),
|
||||
auraGold: Color(0xFF3C3827),
|
||||
auraShadow: Color(0xFF222224),
|
||||
auraShadowHighlight: Color(0xFF2D2D31),
|
||||
auraTint: Color(0xFF000000),
|
||||
brandLink: Color(0xFFFBDC6B),
|
||||
);
|
||||
|
||||
@override
|
||||
AppColors copyWith({
|
||||
Color? seedColor,
|
||||
Color? privateTabPurple,
|
||||
Color? privateTabBackground,
|
||||
Color? privateTabForeground,
|
||||
Color? privateSelectionOverlay,
|
||||
Color? isolatedTabTeal,
|
||||
Color? isolatedTabBackground,
|
||||
Color? isolatedTabForeground,
|
||||
Color? isolatedSelectionOverlay,
|
||||
Color? torPurple,
|
||||
Color? torActiveGreen,
|
||||
Color? torBackgroundGrey,
|
||||
Color? warningAmber,
|
||||
Color? auraPurple,
|
||||
Color? auraGold,
|
||||
Color? auraShadow,
|
||||
Color? auraShadowHighlight,
|
||||
Color? auraTint,
|
||||
Color? brandLink,
|
||||
}) {
|
||||
return AppColors._(
|
||||
seedColor: seedColor ?? this.seedColor,
|
||||
privateTabPurple: privateTabPurple ?? this.privateTabPurple,
|
||||
privateTabBackground: privateTabBackground ?? this.privateTabBackground,
|
||||
privateTabForeground: privateTabForeground ?? this.privateTabForeground,
|
||||
privateSelectionOverlay:
|
||||
privateSelectionOverlay ?? this.privateSelectionOverlay,
|
||||
isolatedTabTeal: isolatedTabTeal ?? this.isolatedTabTeal,
|
||||
isolatedTabBackground:
|
||||
isolatedTabBackground ?? this.isolatedTabBackground,
|
||||
isolatedTabForeground:
|
||||
isolatedTabForeground ?? this.isolatedTabForeground,
|
||||
isolatedSelectionOverlay:
|
||||
isolatedSelectionOverlay ?? this.isolatedSelectionOverlay,
|
||||
torPurple: torPurple ?? this.torPurple,
|
||||
torActiveGreen: torActiveGreen ?? this.torActiveGreen,
|
||||
torBackgroundGrey: torBackgroundGrey ?? this.torBackgroundGrey,
|
||||
warningAmber: warningAmber ?? this.warningAmber,
|
||||
auraPurple: auraPurple ?? this.auraPurple,
|
||||
auraGold: auraGold ?? this.auraGold,
|
||||
auraShadow: auraShadow ?? this.auraShadow,
|
||||
auraShadowHighlight: auraShadowHighlight ?? this.auraShadowHighlight,
|
||||
auraTint: auraTint ?? this.auraTint,
|
||||
brandLink: brandLink ?? this.brandLink,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
AppColors lerp(covariant ThemeExtension<AppColors>? other, double t) {
|
||||
if (other is! AppColors) {
|
||||
return this;
|
||||
}
|
||||
|
||||
return AppColors._(
|
||||
seedColor: Color.lerp(seedColor, other.seedColor, t)!,
|
||||
privateTabPurple: Color.lerp(
|
||||
privateTabPurple,
|
||||
other.privateTabPurple,
|
||||
t,
|
||||
)!,
|
||||
privateTabBackground: Color.lerp(
|
||||
privateTabBackground,
|
||||
other.privateTabBackground,
|
||||
t,
|
||||
)!,
|
||||
privateTabForeground: Color.lerp(
|
||||
privateTabForeground,
|
||||
other.privateTabForeground,
|
||||
t,
|
||||
)!,
|
||||
privateSelectionOverlay: Color.lerp(
|
||||
privateSelectionOverlay,
|
||||
other.privateSelectionOverlay,
|
||||
t,
|
||||
)!,
|
||||
isolatedTabTeal: Color.lerp(isolatedTabTeal, other.isolatedTabTeal, t)!,
|
||||
isolatedTabBackground: Color.lerp(
|
||||
isolatedTabBackground,
|
||||
other.isolatedTabBackground,
|
||||
t,
|
||||
)!,
|
||||
isolatedTabForeground: Color.lerp(
|
||||
isolatedTabForeground,
|
||||
other.isolatedTabForeground,
|
||||
t,
|
||||
)!,
|
||||
isolatedSelectionOverlay: Color.lerp(
|
||||
isolatedSelectionOverlay,
|
||||
other.isolatedSelectionOverlay,
|
||||
t,
|
||||
)!,
|
||||
torPurple: Color.lerp(torPurple, other.torPurple, t)!,
|
||||
torActiveGreen: Color.lerp(torActiveGreen, other.torActiveGreen, t)!,
|
||||
torBackgroundGrey: Color.lerp(
|
||||
torBackgroundGrey,
|
||||
other.torBackgroundGrey,
|
||||
t,
|
||||
)!,
|
||||
warningAmber: Color.lerp(warningAmber, other.warningAmber, t)!,
|
||||
auraPurple: Color.lerp(auraPurple, other.auraPurple, t)!,
|
||||
auraGold: Color.lerp(auraGold, other.auraGold, t)!,
|
||||
auraShadow: Color.lerp(auraShadow, other.auraShadow, t)!,
|
||||
auraShadowHighlight: Color.lerp(
|
||||
auraShadowHighlight,
|
||||
other.auraShadowHighlight,
|
||||
t,
|
||||
)!,
|
||||
auraTint: Color.lerp(auraTint, other.auraTint, t)!,
|
||||
brandLink: Color.lerp(brandLink, other.brandLink, t)!,
|
||||
);
|
||||
}
|
||||
|
||||
/// Get AppColors from the current theme
|
||||
static AppColors of(BuildContext context) {
|
||||
return Theme.of(context).extension<AppColors>() ??
|
||||
switch (Theme.of(context).brightness) {
|
||||
Brightness.dark => dark,
|
||||
Brightness.light => light,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'package:riverpod/riverpod.dart';
|
||||
import 'package:weblibre/core/logger.dart';
|
||||
|
||||
final class ErrorObserver extends ProviderObserver {
|
||||
const ErrorObserver();
|
||||
|
||||
@override
|
||||
void providerDidFail(
|
||||
ProviderObserverContext context,
|
||||
Object error,
|
||||
StackTrace stackTrace,
|
||||
) {
|
||||
logger.e('Provider ${context.provider} threw $error at $stackTrace');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,429 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart' as path_provider;
|
||||
import 'package:sqlite3/sqlite3.dart';
|
||||
|
||||
import 'package:uuid/uuid.dart';
|
||||
import 'package:weblibre/core/logger.dart';
|
||||
import 'package:weblibre/domain/entities/profile.dart';
|
||||
import 'package:weblibre/utils/filesystem.dart' as fs;
|
||||
|
||||
final filesystem = _Filesystem();
|
||||
|
||||
class _Filesystem {
|
||||
late final Directory dataDir;
|
||||
late final Directory profilesDir;
|
||||
|
||||
late final UuidValue selectedProfile;
|
||||
late final Directory selectedProfileDir;
|
||||
late final Directory profileDatabasesDir;
|
||||
|
||||
late final String relativeProfilePath;
|
||||
|
||||
Future<List<Directory>> getAvailableProfileDirectories() {
|
||||
return profilesDir.list().transform(fs.profileTransformer).toList();
|
||||
}
|
||||
|
||||
Future<Profile?> readProfileMetadata(Directory profileDir) {
|
||||
return fs.readProfileMetadata(profileDir);
|
||||
}
|
||||
|
||||
Directory getProfileDir(UuidValue uuid) {
|
||||
return fs.getProfileDir(profilesDir, uuid);
|
||||
}
|
||||
|
||||
Future<bool> createNewProfile(Profile profile) {
|
||||
return fs.createNewProfile(profilesDir, profile);
|
||||
}
|
||||
|
||||
Future<void> updateProfileMetadata(Profile profile) {
|
||||
return fs.writeProfileMetadata(getProfileDir(profile.uuidValue), profile);
|
||||
}
|
||||
|
||||
Future<void> setStartupProfile(UuidValue profile) {
|
||||
return fs.writeStartupProfile(profilesDir, profile, flush: true);
|
||||
}
|
||||
|
||||
Future<void> clearMozillaProfileCache(String profileId) {
|
||||
return fs.clearMozillaProfileCache(selectedProfileDir, profileId);
|
||||
}
|
||||
|
||||
List<String> getMozillaProfileIds(UuidValue uuid) {
|
||||
return fs.getMozillaProfileIds(getProfileDir(uuid));
|
||||
}
|
||||
|
||||
/// If the old canonical location `{profileDir}/mozilla/` exists as a real
|
||||
/// directory and the new location `{profileDir}/files/mozilla/` does not,
|
||||
/// rename the former to the latter.
|
||||
/// Returns `true` if a migration was performed.
|
||||
static Future<bool> _migrateMozillaDirToFiles(Directory profileDir) async {
|
||||
final oldDir = Directory(p.join(profileDir.path, 'mozilla'));
|
||||
final newDir = Directory(p.join(profileDir.path, 'files', 'mozilla'));
|
||||
|
||||
final oldType = await FileSystemEntity.type(
|
||||
oldDir.path,
|
||||
followLinks: false,
|
||||
);
|
||||
|
||||
if (oldType == FileSystemEntityType.directory && !await newDir.exists()) {
|
||||
await Directory(p.join(profileDir.path, 'files')).create(recursive: true);
|
||||
await oldDir.rename(newDir.path);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<void> _migrateGeckoCache(Directory profileDir) async {
|
||||
final profileIds = fs.getMozillaProfileIds(profileDir);
|
||||
final globalCacheDir = Directory(p.join(dataDir.path, 'cache'));
|
||||
|
||||
for (final profileId in profileIds) {
|
||||
final oldCache = Directory(p.join(globalCacheDir.path, profileId));
|
||||
final newCache = Directory(p.join(profileDir.path, 'cache', profileId));
|
||||
|
||||
if (await oldCache.exists() && !await newCache.exists()) {
|
||||
try {
|
||||
await oldCache.rename(newCache.path);
|
||||
} catch (e, s) {
|
||||
logger.w(
|
||||
'Failed to migrate Gecko cache for $profileId',
|
||||
error: e,
|
||||
stackTrace: s,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Ensure the top-level `{filesDir}/mozilla` symlink points to the given
|
||||
/// profile's `files/mozilla/` directory. Old versions created this symlink
|
||||
/// targeting `{profile}/mozilla` which no longer exists after the migration
|
||||
/// moved it to `{profile}/files/mozilla`. GeckoView's `extensions.json`
|
||||
/// stores absolute paths through this symlink, so it must stay valid.
|
||||
static Future<void> _linkMozillaDir(
|
||||
Directory filesDir,
|
||||
Directory profileDir,
|
||||
) async {
|
||||
final mozillaDir = Directory(p.join(profileDir.path, 'files', 'mozilla'));
|
||||
await mozillaDir.create(recursive: true);
|
||||
|
||||
final mozillaPath = p.join(filesDir.path, 'mozilla');
|
||||
|
||||
final currentType = await FileSystemEntity.type(
|
||||
mozillaPath,
|
||||
followLinks: false,
|
||||
);
|
||||
|
||||
switch (currentType) {
|
||||
case FileSystemEntityType.notFound:
|
||||
break;
|
||||
case FileSystemEntityType.link:
|
||||
final link = Link(mozillaPath);
|
||||
try {
|
||||
if (await link.target() == mozillaDir.path) {
|
||||
return;
|
||||
}
|
||||
} on FileSystemException {
|
||||
// Replace unreadable or broken links.
|
||||
}
|
||||
await link.delete();
|
||||
default:
|
||||
// Move aside any non-link entity (directory, file, etc.)
|
||||
final backupPath = p.join(
|
||||
filesDir.path,
|
||||
'mozilla.backup.${DateTime.now().millisecondsSinceEpoch}',
|
||||
);
|
||||
if (currentType == FileSystemEntityType.directory) {
|
||||
await Directory(mozillaPath).rename(backupPath);
|
||||
} else {
|
||||
await File(mozillaPath).rename(backupPath);
|
||||
}
|
||||
}
|
||||
|
||||
await Link(mozillaPath).create(mozillaDir.path);
|
||||
}
|
||||
|
||||
/// Remove path-sensitive Gecko caches that may contain stale absolute paths.
|
||||
/// Gecko regenerates these on next startup.
|
||||
static Future<void> _healGeckoStartupCaches(Directory profileDir) async {
|
||||
final profileIds = fs.getMozillaProfileIds(profileDir);
|
||||
|
||||
for (final profileId in profileIds) {
|
||||
final mozProfileDir = Directory(
|
||||
p.join(profileDir.path, 'files', 'mozilla', profileId),
|
||||
);
|
||||
if (!await mozProfileDir.exists()) continue;
|
||||
|
||||
// addonStartup.json.lz4 caches absolute addon paths
|
||||
final addonStartup = File(
|
||||
p.join(mozProfileDir.path, 'addonStartup.json.lz4'),
|
||||
);
|
||||
if (await addonStartup.exists()) {
|
||||
try {
|
||||
await addonStartup.delete();
|
||||
logger.i('Cleared addonStartup.json.lz4 for $profileId');
|
||||
} catch (e, s) {
|
||||
logger.w(
|
||||
'Failed to clear addonStartup.json.lz4 for $profileId',
|
||||
error: e,
|
||||
stackTrace: s,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// startupCache may contain stale path references
|
||||
final startupCache = Directory(
|
||||
p.join(mozProfileDir.path, 'startupCache'),
|
||||
);
|
||||
if (await startupCache.exists()) {
|
||||
try {
|
||||
await startupCache.delete(recursive: true);
|
||||
logger.i('Cleared startupCache for $profileId');
|
||||
} catch (e, s) {
|
||||
logger.w(
|
||||
'Failed to clear startupCache for $profileId',
|
||||
error: e,
|
||||
stackTrace: s,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Rewrite absolute paths in `extensions.json` that still reference the
|
||||
/// old pre-migration layout (`{filesDir}/mozilla/…`). After migration the
|
||||
/// real location is `{filesDir}/weblibre_profiles/profile-…/files/mozilla/…`
|
||||
/// and the top-level symlink bridges the two, but fixing the stored paths
|
||||
/// removes the permanent dependency on the symlink.
|
||||
/// Returns `true` if any paths were rewritten.
|
||||
static Future<bool> _migrateExtensionPaths(
|
||||
Directory filesDir,
|
||||
Directory profileDir,
|
||||
) async {
|
||||
final profileIds = fs.getMozillaProfileIds(profileDir);
|
||||
// Pattern: /data/user/0/eu.weblibre.gecko/files/mozilla/
|
||||
// Should become: /data/user/0/eu.weblibre.gecko/files/weblibre_profiles/profile-…/files/mozilla/
|
||||
final oldPrefix = '${filesDir.path}/mozilla/';
|
||||
final newPrefix = '${profileDir.path}/files/mozilla/';
|
||||
|
||||
if (oldPrefix == newPrefix) return false;
|
||||
|
||||
var migrated = false;
|
||||
|
||||
for (final profileId in profileIds) {
|
||||
final extensionsFile = File(
|
||||
p.join(
|
||||
profileDir.path,
|
||||
'files',
|
||||
'mozilla',
|
||||
profileId,
|
||||
'extensions.json',
|
||||
),
|
||||
);
|
||||
|
||||
if (!await extensionsFile.exists()) continue;
|
||||
|
||||
try {
|
||||
final content = await extensionsFile.readAsString();
|
||||
if (!content.contains(oldPrefix)) continue;
|
||||
|
||||
final json = jsonDecode(content);
|
||||
var changed = false;
|
||||
|
||||
if (json is Map<String, dynamic> && json['addons'] is List) {
|
||||
for (final addon in json['addons'] as List) {
|
||||
if (addon is! Map<String, dynamic>) continue;
|
||||
|
||||
final path = addon['path'];
|
||||
if (path is String && path.startsWith(oldPrefix)) {
|
||||
addon['path'] = path.replaceFirst(oldPrefix, newPrefix);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
final rootURI = addon['rootURI'];
|
||||
if (rootURI is String && rootURI.contains(oldPrefix)) {
|
||||
addon['rootURI'] = rootURI.replaceFirst(oldPrefix, newPrefix);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
await extensionsFile.writeAsString(jsonEncode(json), flush: true);
|
||||
logger.i('Migrated extension paths in $profileId/extensions.json');
|
||||
migrated = true;
|
||||
}
|
||||
} catch (e, s) {
|
||||
logger.w(
|
||||
'Failed to migrate extension paths for $profileId',
|
||||
error: e,
|
||||
stackTrace: s,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return migrated;
|
||||
}
|
||||
|
||||
/// Run all post-migration healing for a profile directory.
|
||||
/// Called during init for the active profile and after backup restore.
|
||||
Future<void> healProfile(Directory profileDir) async {
|
||||
final filesDir = profilesDir.parent;
|
||||
final dirMigrated = await _migrateMozillaDirToFiles(profileDir);
|
||||
await _migrateGeckoCache(profileDir);
|
||||
final pathsMigrated = await _migrateExtensionPaths(filesDir, profileDir);
|
||||
|
||||
// Only update the global symlink for the currently active profile —
|
||||
// restoring a non-active profile must not repoint it.
|
||||
final isActiveProfile = profileDir.path == selectedProfileDir.path;
|
||||
if (isActiveProfile) {
|
||||
await _linkMozillaDir(filesDir, profileDir);
|
||||
}
|
||||
|
||||
// Only clear Gecko startup caches when a path migration actually happened,
|
||||
// to avoid a recurring startup performance penalty.
|
||||
if (dirMigrated || pathsMigrated) {
|
||||
await _healGeckoStartupCaches(profileDir);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _setupSqliteCache() async {
|
||||
// Make sqlite3 pick a more suitable location for temporary files - the
|
||||
// one from the system may be inaccessible due to sandboxing.
|
||||
final cachebase = (await path_provider.getTemporaryDirectory()).path;
|
||||
// We can't access /tmp on Android, which sqlite3 would try by default.
|
||||
// Explicitly tell it about the correct temporary directory.
|
||||
sqlite3.tempDirectory = cachebase;
|
||||
}
|
||||
|
||||
Future<void> _copyDirectory(
|
||||
Directory source,
|
||||
Directory destination,
|
||||
bool Function(FileSystemEntity e) filter,
|
||||
) async {
|
||||
// Create destination directory
|
||||
await destination.create(recursive: true);
|
||||
|
||||
// List all contents
|
||||
await for (final entity in source.list().where(filter)) {
|
||||
final newPath = p.join(destination.path, p.basename(entity.path));
|
||||
|
||||
if (entity is Directory) {
|
||||
// Recursively copy subdirectory
|
||||
await _copyDirectory(entity, Directory(newPath), filter);
|
||||
} else if (entity is File) {
|
||||
// Copy file
|
||||
await entity.copy(newPath);
|
||||
} else if (entity is Link) {
|
||||
// Copy link
|
||||
await Link(newPath).create(await entity.target());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> init() async {
|
||||
final filesDir = await path_provider.getApplicationSupportDirectory();
|
||||
|
||||
dataDir = filesDir.parent;
|
||||
|
||||
profilesDir = Directory(p.join(filesDir.path, fs.profilesDirName));
|
||||
await profilesDir.create();
|
||||
|
||||
final selectedProfile = await fs.selectStartupProfile(profilesDir);
|
||||
if (selectedProfile == null) {
|
||||
final defaultProfile = Profile.create(name: 'Default');
|
||||
if (!await fs.createNewProfile(profilesDir, defaultProfile)) {
|
||||
throw Exception('Unable to create default profile');
|
||||
}
|
||||
|
||||
this.selectedProfile = defaultProfile.uuidValue;
|
||||
await fs.writeStartupProfile(profilesDir, defaultProfile.uuidValue);
|
||||
|
||||
final mozillaDir = Directory(p.join(filesDir.path, 'mozilla'));
|
||||
if (await mozillaDir.exists()) {
|
||||
final type = await FileSystemEntity.type(mozillaDir.path);
|
||||
if (type != FileSystemEntityType.link) {
|
||||
await _migrate(defaultProfile, mozillaDir, filesDir);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.selectedProfile = selectedProfile;
|
||||
}
|
||||
|
||||
relativeProfilePath = p.join(
|
||||
fs.profilesDirName,
|
||||
'${fs.profileDirPrefix}${this.selectedProfile.uuid}',
|
||||
);
|
||||
selectedProfileDir = Directory(p.join(filesDir.path, relativeProfilePath));
|
||||
await selectedProfileDir.create();
|
||||
|
||||
profileDatabasesDir = Directory(
|
||||
p.join(selectedProfileDir.path, 'databases'),
|
||||
);
|
||||
await profileDatabasesDir.create();
|
||||
|
||||
await healProfile(selectedProfileDir);
|
||||
await _setupSqliteCache();
|
||||
}
|
||||
|
||||
Future<void> _migrate(
|
||||
Profile defaultProfile,
|
||||
Directory mozillaDir,
|
||||
Directory filesDir,
|
||||
) async {
|
||||
final profileDir = getProfileDir(defaultProfile.uuidValue);
|
||||
|
||||
final newMozillaDir = Directory(
|
||||
p.join(profileDir.path, 'files', 'mozilla'),
|
||||
);
|
||||
await newMozillaDir.create(recursive: true);
|
||||
await mozillaDir.rename(newMozillaDir.path);
|
||||
|
||||
await _copyDirectory(
|
||||
filesDir,
|
||||
Directory(p.join(profileDir.path, 'files')),
|
||||
(e) => e is! Directory || p.basename(e.path) != fs.profilesDirName,
|
||||
);
|
||||
|
||||
final profileDatabasesDir = Directory(p.join(profileDir.path, 'databases'));
|
||||
|
||||
await _copyDirectory(
|
||||
Directory(p.join(dataDir.path, 'databases')),
|
||||
profileDatabasesDir,
|
||||
(e) => true,
|
||||
);
|
||||
|
||||
final dbFolder = await path_provider.getApplicationDocumentsDirectory();
|
||||
|
||||
final bangDb = File(p.join(dbFolder.path, 'bang3.db'));
|
||||
await bangDb.copy(p.join(profileDatabasesDir.path, 'bang.db'));
|
||||
final feedDb = File(p.join(dbFolder.path, 'feed.db'));
|
||||
await feedDb.copy(p.join(profileDatabasesDir.path, 'feed.db'));
|
||||
final tabDb = File(p.join(dbFolder.path, 'tab2.db'));
|
||||
await tabDb.copy(p.join(profileDatabasesDir.path, 'tab.db'));
|
||||
final userDb = File(p.join(dbFolder.path, 'user.db'));
|
||||
await userDb.copy(p.join(profileDatabasesDir.path, 'user.db'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:exceptions/exceptions.dart';
|
||||
import 'package:http/http.dart';
|
||||
|
||||
ErrorMessage handleHttpError(Exception exception, StackTrace stackTrace) {
|
||||
return switch (exception) {
|
||||
SocketException() => const ErrorMessage(
|
||||
source: 'http',
|
||||
message: 'Could not contact remote service',
|
||||
),
|
||||
HttpException() => const ErrorMessage(
|
||||
source: 'http',
|
||||
message: 'Web request returned error',
|
||||
),
|
||||
FormatException() => const ErrorMessage(
|
||||
source: 'http',
|
||||
message: 'Bad response format',
|
||||
),
|
||||
ClientException() => const ErrorMessage(
|
||||
source: 'http',
|
||||
message: 'Could not contact remote service',
|
||||
),
|
||||
_ => ErrorMessage.fromException(exception, stackTrace),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'package:logger/logger.dart';
|
||||
|
||||
class _DebugLogFilter extends LogFilter {
|
||||
@override
|
||||
bool shouldLog(LogEvent event) {
|
||||
//Log all events
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
final loggerMemory = MemoryOutput(
|
||||
bufferSize: 255,
|
||||
secondOutput: ConsoleOutput(),
|
||||
);
|
||||
final logger = Logger(
|
||||
filter: _DebugLogFilter(),
|
||||
printer: PrettyPrinter(dateTimeFormat: DateTimeFormat.onlyTimeAndSinceStart),
|
||||
output: loggerMemory,
|
||||
);
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'app_state.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class AppStateKey extends _$AppStateKey {
|
||||
void reset() {
|
||||
state = GlobalKey(debugLabel: 'RootKey');
|
||||
}
|
||||
|
||||
@override
|
||||
GlobalKey build() {
|
||||
return GlobalKey(debugLabel: 'RootKey');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'app_state.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(AppStateKey)
|
||||
final appStateKeyProvider = AppStateKeyProvider._();
|
||||
|
||||
final class AppStateKeyProvider
|
||||
extends $NotifierProvider<AppStateKey, GlobalKey<State<StatefulWidget>>> {
|
||||
AppStateKeyProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'appStateKeyProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$appStateKeyHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
AppStateKey create() => AppStateKey();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(GlobalKey<State<StatefulWidget>> value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<GlobalKey<State<StatefulWidget>>>(
|
||||
value,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$appStateKeyHash() => r'113c208139d84cdd52f623c2c6298b430fc1a889';
|
||||
|
||||
abstract class _$AppStateKey
|
||||
extends $Notifier<GlobalKey<State<StatefulWidget>>> {
|
||||
GlobalKey<State<StatefulWidget>> build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final ref =
|
||||
this.ref
|
||||
as $Ref<
|
||||
GlobalKey<State<StatefulWidget>>,
|
||||
GlobalKey<State<StatefulWidget>>
|
||||
>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<
|
||||
GlobalKey<State<StatefulWidget>>,
|
||||
GlobalKey<State<StatefulWidget>>
|
||||
>,
|
||||
GlobalKey<State<StatefulWidget>>,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleCreate(ref, build);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:weblibre/core/design/app_colors.dart';
|
||||
|
||||
part 'defaults.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
Color lightSeedColorFallback(Ref ref) => AppColors.light.seedColor;
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
Color darkSeedColorFallback(Ref ref) => AppColors.dark.seedColor;
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
Uri docsUri(Ref ref) => Uri.parse('https://docs.weblibre.eu/');
|
||||
@@ -0,0 +1,134 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'defaults.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(lightSeedColorFallback)
|
||||
final lightSeedColorFallbackProvider = LightSeedColorFallbackProvider._();
|
||||
|
||||
final class LightSeedColorFallbackProvider
|
||||
extends $FunctionalProvider<Color, Color, Color>
|
||||
with $Provider<Color> {
|
||||
LightSeedColorFallbackProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'lightSeedColorFallbackProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$lightSeedColorFallbackHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<Color> $createElement($ProviderPointer pointer) =>
|
||||
$ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
Color create(Ref ref) {
|
||||
return lightSeedColorFallback(ref);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(Color value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<Color>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$lightSeedColorFallbackHash() =>
|
||||
r'851efdcb11e4367ea2e54f1884a73fd4cd841d4a';
|
||||
|
||||
@ProviderFor(darkSeedColorFallback)
|
||||
final darkSeedColorFallbackProvider = DarkSeedColorFallbackProvider._();
|
||||
|
||||
final class DarkSeedColorFallbackProvider
|
||||
extends $FunctionalProvider<Color, Color, Color>
|
||||
with $Provider<Color> {
|
||||
DarkSeedColorFallbackProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'darkSeedColorFallbackProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$darkSeedColorFallbackHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<Color> $createElement($ProviderPointer pointer) =>
|
||||
$ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
Color create(Ref ref) {
|
||||
return darkSeedColorFallback(ref);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(Color value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<Color>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$darkSeedColorFallbackHash() =>
|
||||
r'161a8c4318108c31d5c300441bc3332d08c24496';
|
||||
|
||||
@ProviderFor(docsUri)
|
||||
final docsUriProvider = DocsUriProvider._();
|
||||
|
||||
final class DocsUriProvider extends $FunctionalProvider<Uri, Uri, Uri>
|
||||
with $Provider<Uri> {
|
||||
DocsUriProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'docsUriProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$docsUriHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<Uri> $createElement($ProviderPointer pointer) =>
|
||||
$ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
Uri create(Ref ref) {
|
||||
return docsUri(ref);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(Uri value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<Uri>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$docsUriHash() => r'6456efcf97ddc7ee87a67e2d3380f7241e3d76ea';
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'device_info.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class AndroidDeviceInfo extends _$AndroidDeviceInfo {
|
||||
@override
|
||||
Future<AndroidDeviceInfoData?> build() async {
|
||||
if (!Platform.isAndroid) return null;
|
||||
|
||||
final deviceInfo = DeviceInfoPlugin();
|
||||
final androidInfo = await deviceInfo.androidInfo;
|
||||
|
||||
return AndroidDeviceInfoData(sdkInt: androidInfo.version.sdkInt);
|
||||
}
|
||||
}
|
||||
|
||||
class AndroidDeviceInfoData {
|
||||
final int sdkInt;
|
||||
|
||||
const AndroidDeviceInfoData({required this.sdkInt});
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'device_info.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(AndroidDeviceInfo)
|
||||
final androidDeviceInfoProvider = AndroidDeviceInfoProvider._();
|
||||
|
||||
final class AndroidDeviceInfoProvider
|
||||
extends $AsyncNotifierProvider<AndroidDeviceInfo, AndroidDeviceInfoData?> {
|
||||
AndroidDeviceInfoProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'androidDeviceInfoProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$androidDeviceInfoHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
AndroidDeviceInfo create() => AndroidDeviceInfo();
|
||||
}
|
||||
|
||||
String _$androidDeviceInfoHash() => r'05c6cc63a6ee34f137aef538d65e8ab94b9cca89';
|
||||
|
||||
abstract class _$AndroidDeviceInfo
|
||||
extends $AsyncNotifier<AndroidDeviceInfoData?> {
|
||||
FutureOr<AndroidDeviceInfoData?> build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final ref =
|
||||
this.ref
|
||||
as $Ref<AsyncValue<AndroidDeviceInfoData?>, AndroidDeviceInfoData?>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<
|
||||
AsyncValue<AndroidDeviceInfoData?>,
|
||||
AndroidDeviceInfoData?
|
||||
>,
|
||||
AsyncValue<AndroidDeviceInfoData?>,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleCreate(ref, build);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'package:intl/date_symbol_data_local.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'format.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class Format extends _$Format {
|
||||
String fullDateTime(DateTime date) {
|
||||
final pattern = DateFormat('yMMMMd').addPattern('Hm');
|
||||
|
||||
return pattern.format(date);
|
||||
}
|
||||
|
||||
String shortDate(DateTime date) {
|
||||
return DateFormat('yyyy-MM-dd').format(date);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> build() async {
|
||||
await initializeDateFormatting();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'format.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(Format)
|
||||
final formatProvider = FormatProvider._();
|
||||
|
||||
final class FormatProvider extends $AsyncNotifierProvider<Format, void> {
|
||||
FormatProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'formatProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$formatHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
Format create() => Format();
|
||||
}
|
||||
|
||||
String _$formatHash() => r'695b94cc4b25596e4ef953d08ba954424a5ec33e';
|
||||
|
||||
abstract class _$Format extends $AsyncNotifier<void> {
|
||||
FutureOr<void> build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final ref = this.ref as $Ref<AsyncValue<void>, void>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<AsyncValue<void>, void>,
|
||||
AsyncValue<void>,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleCreate(ref, build);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:weblibre/data/models/drag_data.dart';
|
||||
|
||||
part 'global_drop.g.dart';
|
||||
|
||||
@Riverpod()
|
||||
class WillAcceptDrop extends _$WillAcceptDrop {
|
||||
// ignore: use_setters_to_change_properties api decision
|
||||
void setData(DropTargetData data) {
|
||||
state = data;
|
||||
}
|
||||
|
||||
void clear() {
|
||||
state = null;
|
||||
}
|
||||
|
||||
@override
|
||||
DropTargetData? build() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'global_drop.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(WillAcceptDrop)
|
||||
final willAcceptDropProvider = WillAcceptDropProvider._();
|
||||
|
||||
final class WillAcceptDropProvider
|
||||
extends $NotifierProvider<WillAcceptDrop, DropTargetData?> {
|
||||
WillAcceptDropProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'willAcceptDropProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$willAcceptDropHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
WillAcceptDrop create() => WillAcceptDrop();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(DropTargetData? value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<DropTargetData?>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$willAcceptDropHash() => r'97b47784ef9101757602b4408d1f545ef2308830';
|
||||
|
||||
abstract class _$WillAcceptDrop extends $Notifier<DropTargetData?> {
|
||||
DropTargetData? build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final ref = this.ref as $Ref<DropTargetData?, DropTargetData?>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<DropTargetData?, DropTargetData?>,
|
||||
DropTargetData?,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleCreate(ref, build);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:riverpod/experimental/persist.dart';
|
||||
import 'package:riverpod_annotation/experimental/persist.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:weblibre/features/user/data/providers.dart';
|
||||
|
||||
part 'persisted_bool.g.dart';
|
||||
|
||||
enum PersistedBoolKey {
|
||||
extensionsExpanded(key: 'ExtensionsExpanded', defaultValue: false),
|
||||
searchSuggestionsExpanded(
|
||||
key: 'SearchSuggestionsExpanded',
|
||||
defaultValue: true,
|
||||
),
|
||||
tabSuggestions(key: 'TabSuggestions', defaultValue: false);
|
||||
|
||||
const PersistedBoolKey({required this.key, required this.defaultValue});
|
||||
|
||||
final String key;
|
||||
final bool defaultValue;
|
||||
}
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class PersistedBool extends _$PersistedBool {
|
||||
void toggle() => state = !state;
|
||||
|
||||
// ignore: use_setters_to_change_properties
|
||||
void set(bool value) => state = value;
|
||||
|
||||
@override
|
||||
bool build(PersistedBoolKey key) {
|
||||
persist(
|
||||
ref.watch(riverpodDatabaseStorageProvider),
|
||||
key: key.key,
|
||||
encode: (state) => jsonEncode([state]),
|
||||
decode: (encoded) => (jsonDecode(encoded) as List<dynamic>).first as bool,
|
||||
);
|
||||
|
||||
return stateOrNull ?? key.defaultValue;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'persisted_bool.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(PersistedBool)
|
||||
final persistedBoolProvider = PersistedBoolFamily._();
|
||||
|
||||
final class PersistedBoolProvider
|
||||
extends $NotifierProvider<PersistedBool, bool> {
|
||||
PersistedBoolProvider._({
|
||||
required PersistedBoolFamily super.from,
|
||||
required PersistedBoolKey super.argument,
|
||||
}) : super(
|
||||
retry: null,
|
||||
name: r'persistedBoolProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$persistedBoolHash();
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return r'persistedBoolProvider'
|
||||
''
|
||||
'($argument)';
|
||||
}
|
||||
|
||||
@$internal
|
||||
@override
|
||||
PersistedBool create() => PersistedBool();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(bool value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<bool>(value),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is PersistedBoolProvider && other.argument == argument;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return argument.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
String _$persistedBoolHash() => r'97fd7ed82dfde444fddf4475a3982be00f5f197a';
|
||||
|
||||
final class PersistedBoolFamily extends $Family
|
||||
with
|
||||
$ClassFamilyOverride<
|
||||
PersistedBool,
|
||||
bool,
|
||||
bool,
|
||||
bool,
|
||||
PersistedBoolKey
|
||||
> {
|
||||
PersistedBoolFamily._()
|
||||
: super(
|
||||
retry: null,
|
||||
name: r'persistedBoolProvider',
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
isAutoDispose: false,
|
||||
);
|
||||
|
||||
PersistedBoolProvider call(PersistedBoolKey key) =>
|
||||
PersistedBoolProvider._(argument: key, from: this);
|
||||
|
||||
@override
|
||||
String toString() => r'persistedBoolProvider';
|
||||
}
|
||||
|
||||
abstract class _$PersistedBool extends $Notifier<bool> {
|
||||
late final _$args = ref.$arg as PersistedBoolKey;
|
||||
PersistedBoolKey get key => _$args;
|
||||
|
||||
bool build(PersistedBoolKey key);
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final ref = this.ref as $Ref<bool, bool>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<bool, bool>,
|
||||
bool,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleCreate(ref, () => build(_$args));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:weblibre/core/providers/app_state.dart';
|
||||
import 'package:weblibre/core/routing/routes.dart';
|
||||
import 'package:weblibre/features/user/domain/providers/profile_auth.dart';
|
||||
import 'package:weblibre/features/user/domain/repositories/onboarding.dart';
|
||||
|
||||
part 'router.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
Future<GoRouter> router(Ref ref) async {
|
||||
ref.watch(appStateKeyProvider); //Rebuild router on key changes
|
||||
|
||||
final onboardingRepository = ref.read(onboardingRepositoryProvider.notifier);
|
||||
unawaited(ref.read(profileAuthStateProvider.notifier).bootstrapFromProfile());
|
||||
|
||||
String? initialLocation;
|
||||
|
||||
final onboardingMandatory = await onboardingRepository.isOutdated();
|
||||
|
||||
if (onboardingMandatory) {
|
||||
final current = await onboardingRepository.getCurrentRevision();
|
||||
|
||||
final route = OnboardingRoute(
|
||||
currentRevision: current ?? -1,
|
||||
targetRevision: OnboardingRepository.targetRevision,
|
||||
);
|
||||
|
||||
initialLocation = route.location;
|
||||
}
|
||||
|
||||
final profileAuthRefreshListenable = ref.watch(profileAuthProvider);
|
||||
|
||||
return GoRouter(
|
||||
debugLogDiagnostics: true,
|
||||
routes: $appRoutes,
|
||||
initialLocation: initialLocation ?? const LockRoute().location,
|
||||
refreshListenable: profileAuthRefreshListenable,
|
||||
redirect: (context, state) {
|
||||
final authenticated = ref.read(profileAuthStateProvider);
|
||||
final currentTopRouteName = state.topRoute?.name;
|
||||
final isOnLockRoute = currentTopRouteName == LockRoute.name;
|
||||
final isOnOnboarding = currentTopRouteName == OnboardingRoute.name;
|
||||
|
||||
// Don't redirect during onboarding
|
||||
if (isOnOnboarding) return null;
|
||||
|
||||
if (!authenticated && !isOnLockRoute) {
|
||||
return const LockRoute().location;
|
||||
}
|
||||
|
||||
if (authenticated && isOnLockRoute) {
|
||||
return const BrowserRoute().location;
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class CurrentTopRoute extends _$CurrentTopRoute {
|
||||
@override
|
||||
RouteBase? build() {
|
||||
final router = ref.watch(routerProvider).value;
|
||||
if (router == null) return null;
|
||||
|
||||
GoRoute? getCurrentRoute() {
|
||||
final config = router.routerDelegate.currentConfiguration;
|
||||
|
||||
if (config.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final match = config.last;
|
||||
return match.route;
|
||||
}
|
||||
|
||||
void update() {
|
||||
unawaited(
|
||||
Future(() {
|
||||
state = getCurrentRoute();
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
router.routerDelegate.addListener(update);
|
||||
ref.onDispose(() => router.routerDelegate.removeListener(update));
|
||||
|
||||
return getCurrentRoute();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'router.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(router)
|
||||
final routerProvider = RouterProvider._();
|
||||
|
||||
final class RouterProvider
|
||||
extends
|
||||
$FunctionalProvider<AsyncValue<GoRouter>, GoRouter, FutureOr<GoRouter>>
|
||||
with $FutureModifier<GoRouter>, $FutureProvider<GoRouter> {
|
||||
RouterProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'routerProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$routerHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$FutureProviderElement<GoRouter> $createElement($ProviderPointer pointer) =>
|
||||
$FutureProviderElement(pointer);
|
||||
|
||||
@override
|
||||
FutureOr<GoRouter> create(Ref ref) {
|
||||
return router(ref);
|
||||
}
|
||||
}
|
||||
|
||||
String _$routerHash() => r'4402ca2d7061945c395f3963d6bde29be8999f96';
|
||||
|
||||
@ProviderFor(CurrentTopRoute)
|
||||
final currentTopRouteProvider = CurrentTopRouteProvider._();
|
||||
|
||||
final class CurrentTopRouteProvider
|
||||
extends $NotifierProvider<CurrentTopRoute, RouteBase?> {
|
||||
CurrentTopRouteProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'currentTopRouteProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$currentTopRouteHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
CurrentTopRoute create() => CurrentTopRoute();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(RouteBase? value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<RouteBase?>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$currentTopRouteHash() => r'bf9956935ede399b815348926bb75c6bad22619e';
|
||||
|
||||
abstract class _$CurrentTopRoute extends $Notifier<RouteBase?> {
|
||||
RouteBase? build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final ref = this.ref as $Ref<RouteBase?, RouteBase?>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<RouteBase?, RouteBase?>,
|
||||
RouteBase?,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleCreate(ref, build);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
part of 'routes.dart';
|
||||
|
||||
@TypedGoRoute<BangMenuRoute>(
|
||||
name: 'BangRoute',
|
||||
path: '/bangs',
|
||||
routes: [
|
||||
TypedGoRoute<UserBangsRoute>(
|
||||
name: 'UserBangsRoute',
|
||||
path: 'user',
|
||||
routes: [
|
||||
TypedGoRoute<NewUserBangRoute>(name: 'NewUserBangRoute', path: 'new'),
|
||||
TypedGoRoute<EditUserBangRoute>(
|
||||
name: 'EditUserBangRoute',
|
||||
path: 'edit',
|
||||
),
|
||||
],
|
||||
),
|
||||
TypedGoRoute<BangSearchRoute>(
|
||||
name: 'BangSearchRoute',
|
||||
path: 'search/:searchText',
|
||||
),
|
||||
TypedGoRoute<BangCategoriesRoute>(
|
||||
name: 'BangCategoriesRoute',
|
||||
path: 'categories',
|
||||
routes: [
|
||||
TypedGoRoute<BangCategoryRoute>(
|
||||
name: 'BangCategoryRoute',
|
||||
path: 'category/:category',
|
||||
routes: [
|
||||
TypedGoRoute<BangSubCategoryRoute>(
|
||||
name: 'BangSubCategoryRoute',
|
||||
path: ':subCategory',
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
class BangMenuRoute extends GoRouteData with $BangMenuRoute {
|
||||
const BangMenuRoute();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const BangMenuScreen();
|
||||
}
|
||||
}
|
||||
|
||||
class BangCategoriesRoute extends GoRouteData with $BangCategoriesRoute {
|
||||
const BangCategoriesRoute();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const BangCategoriesScreen();
|
||||
}
|
||||
}
|
||||
|
||||
class BangCategoryRoute extends GoRouteData with $BangCategoryRoute {
|
||||
final String category;
|
||||
|
||||
const BangCategoryRoute({required this.category});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return BangCategoryScreen(category: category);
|
||||
}
|
||||
}
|
||||
|
||||
class BangSubCategoryRoute extends GoRouteData with $BangSubCategoryRoute {
|
||||
final String category;
|
||||
final String subCategory;
|
||||
|
||||
const BangSubCategoryRoute({
|
||||
required this.category,
|
||||
required this.subCategory,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return BangCategoryScreen(category: category, subCategory: subCategory);
|
||||
}
|
||||
}
|
||||
|
||||
class BangSearchRoute extends GoRouteData with $BangSearchRoute {
|
||||
static const String emptySearchText = ' ';
|
||||
|
||||
//This should be nullable but isnt allowed by go_router
|
||||
final String searchText;
|
||||
|
||||
const BangSearchRoute({this.searchText = BangSearchRoute.emptySearchText});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return BangSearchScreen(
|
||||
initialSearchText: (searchText.isEmpty || searchText == emptySearchText)
|
||||
? null
|
||||
: searchText,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class UserBangsRoute extends GoRouteData with $UserBangsRoute {
|
||||
const UserBangsRoute();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const UserBangs();
|
||||
}
|
||||
}
|
||||
|
||||
class NewUserBangRoute extends GoRouteData with $NewUserBangRoute {
|
||||
const NewUserBangRoute();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const EditBangScreen(initialBang: null);
|
||||
}
|
||||
}
|
||||
|
||||
class EditUserBangRoute extends GoRouteData with $EditUserBangRoute {
|
||||
final String initialBang;
|
||||
|
||||
const EditUserBangRoute({required this.initialBang});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return EditBangScreen(
|
||||
initialBang: Bang.fromJson(
|
||||
jsonDecode(initialBang) as Map<String, dynamic>,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
part of 'routes.dart';
|
||||
|
||||
@TypedGoRoute<BookmarksRoute>(
|
||||
name: 'BookmarksRoute',
|
||||
path: '/bookmarks',
|
||||
routes: [
|
||||
TypedGoRoute<BookmarkListRoute>(
|
||||
name: 'BookmarkListRoute',
|
||||
path: 'list/:entryGuid',
|
||||
),
|
||||
TypedGoRoute<BookmarkFolderAddRoute>(
|
||||
name: 'BookmarkFolderAddRoute',
|
||||
path: 'createFolder',
|
||||
),
|
||||
TypedGoRoute<BookmarkFolderEditRoute>(
|
||||
name: 'BookmarkFolderEditRoute',
|
||||
path: 'editFolder',
|
||||
),
|
||||
TypedGoRoute<BookmarkEntryAddRoute>(
|
||||
name: 'BookmarkEntryAddRoute',
|
||||
path: 'createEntry',
|
||||
),
|
||||
TypedGoRoute<BookmarkEntryEditRoute>(
|
||||
name: 'BookmarkEntryEditRoute',
|
||||
path: 'editEntry',
|
||||
),
|
||||
],
|
||||
)
|
||||
class BookmarksRoute extends GoRouteData with $BookmarksRoute {
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
}
|
||||
|
||||
class BookmarkListRoute extends GoRouteData with $BookmarkListRoute {
|
||||
final String entryGuid;
|
||||
|
||||
const BookmarkListRoute({required this.entryGuid});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return BookmarkListScreen(entryGuid: entryGuid);
|
||||
}
|
||||
}
|
||||
|
||||
class BookmarkFolderAddRoute extends GoRouteData with $BookmarkFolderAddRoute {
|
||||
final String? parentGuid;
|
||||
|
||||
const BookmarkFolderAddRoute({required this.parentGuid});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return BookmarkFolderEditScreen(parentGuid: parentGuid, folder: null);
|
||||
}
|
||||
}
|
||||
|
||||
class BookmarkFolderEditRoute extends GoRouteData
|
||||
with $BookmarkFolderEditRoute {
|
||||
final String folder;
|
||||
|
||||
const BookmarkFolderEditRoute({required this.folder});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return BookmarkFolderEditScreen(
|
||||
folder: BookmarkFolder.fromJson(
|
||||
jsonDecode(folder) as Map<String, dynamic>,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class BookmarkEntryAddRoute extends GoRouteData with $BookmarkEntryAddRoute {
|
||||
final String bookmarkInfo;
|
||||
|
||||
const BookmarkEntryAddRoute({required this.bookmarkInfo});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return BookmarkEntryEditScreen(
|
||||
initialInfo: BookmarkInfo.decode(jsonDecode(bookmarkInfo) as Object),
|
||||
exisitingEntry: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class BookmarkEntryEditRoute extends GoRouteData with $BookmarkEntryEditRoute {
|
||||
final String bookmarkEntry;
|
||||
|
||||
const BookmarkEntryEditRoute({required this.bookmarkEntry});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return BookmarkEntryEditScreen(
|
||||
initialInfo: null,
|
||||
exisitingEntry: BookmarkEntry.fromJson(
|
||||
jsonDecode(bookmarkEntry) as Map<String, dynamic>,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
part of 'routes.dart';
|
||||
|
||||
@TypedGoRoute<BrowserRoute>(
|
||||
name: BrowserRoute.name,
|
||||
path: '/browser',
|
||||
routes: [
|
||||
TypedGoRoute<SearchRoute>(
|
||||
name: 'SearchRoute',
|
||||
path: 'search/:tabType/:searchText',
|
||||
),
|
||||
TypedGoRoute<TabViewRoute>(name: 'TabViewRoute', path: 'tab_view'),
|
||||
TypedGoRoute<ContextMenuRoute>(
|
||||
name: 'ContextMenuRoute',
|
||||
path: 'context_menu',
|
||||
),
|
||||
TypedGoRoute<ContainerDraftRoute>(
|
||||
name: 'ContainerDraftRoute',
|
||||
path: 'container_draft',
|
||||
),
|
||||
TypedGoRoute<ContainerListRoute>(
|
||||
name: 'ContainerListRoute',
|
||||
path: 'containers',
|
||||
routes: [
|
||||
TypedGoRoute<ContainerCreateRoute>(
|
||||
name: 'ContainerCreateRoute',
|
||||
path: 'create/:containerData',
|
||||
),
|
||||
TypedGoRoute<ContainerEditRoute>(
|
||||
name: 'ContainerEditRoute',
|
||||
path: 'edit/:containerData',
|
||||
),
|
||||
],
|
||||
),
|
||||
TypedGoRoute<ContainerSelectionRoute>(
|
||||
name: 'ContainerSelectionRoute',
|
||||
path: 'select_container',
|
||||
),
|
||||
TypedGoRoute<TabTreeRoute>(
|
||||
name: 'TabTreeRoute',
|
||||
path: 'tab_tree/:rootTabId',
|
||||
),
|
||||
TypedGoRoute<OpenSharedContentRoute>(
|
||||
name: 'OpenSharedContentRoute',
|
||||
path: 'open_content',
|
||||
),
|
||||
TypedGoRoute<SelectProfileRoute>(
|
||||
name: 'SelectProfileRoute',
|
||||
path: 'profile',
|
||||
),
|
||||
],
|
||||
)
|
||||
class BrowserRoute extends GoRouteData with $BrowserRoute {
|
||||
static const name = 'BrowserRoute';
|
||||
|
||||
const BrowserRoute();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const BrowserScreen();
|
||||
}
|
||||
}
|
||||
|
||||
enum TabType { regular, private, child, isolated }
|
||||
|
||||
class SearchRoute extends GoRouteData with $SearchRoute {
|
||||
static const String emptySearchText = ' ';
|
||||
|
||||
final TabType tabType;
|
||||
|
||||
//This should be nullable but isnt allowed by go_router
|
||||
final String searchText;
|
||||
|
||||
final bool launchedFromIntent;
|
||||
|
||||
/// When provided, the search screen will load URLs into this existing tab
|
||||
/// instead of creating a new tab. This also changes the UI to show
|
||||
/// site-specific bangs instead of the tab type selector.
|
||||
final String? tabId;
|
||||
|
||||
const SearchRoute({
|
||||
required this.tabType,
|
||||
this.searchText = SearchRoute.emptySearchText,
|
||||
this.launchedFromIntent = false,
|
||||
this.tabId,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return SearchScreen(
|
||||
tabType: tabType,
|
||||
initialSearchText: (searchText.isEmpty || searchText == emptySearchText)
|
||||
? null
|
||||
: searchText,
|
||||
launchedFromIntent: launchedFromIntent,
|
||||
tabId: tabId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
bool _isContainerUiEnabled(BuildContext context) {
|
||||
final settings = ProviderScope.containerOf(
|
||||
context,
|
||||
listen: false,
|
||||
).read(generalSettingsWithDefaultsProvider);
|
||||
|
||||
return settings.showContainerUi;
|
||||
}
|
||||
|
||||
class ContainerDraftRoute extends GoRouteData with $ContainerDraftRoute {
|
||||
const ContainerDraftRoute();
|
||||
|
||||
@override
|
||||
String? redirect(BuildContext context, GoRouterState state) {
|
||||
return _isContainerUiEnabled(context)
|
||||
? null
|
||||
: const BrowserRoute().location;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const ContainerDraftSuggestionsScreen();
|
||||
}
|
||||
}
|
||||
|
||||
class ContainerListRoute extends GoRouteData with $ContainerListRoute {
|
||||
const ContainerListRoute();
|
||||
|
||||
@override
|
||||
String? redirect(BuildContext context, GoRouterState state) {
|
||||
return _isContainerUiEnabled(context)
|
||||
? null
|
||||
: const BrowserRoute().location;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const ContainerListScreen();
|
||||
}
|
||||
}
|
||||
|
||||
class ContainerSelectionRoute extends GoRouteData
|
||||
with $ContainerSelectionRoute {
|
||||
const ContainerSelectionRoute();
|
||||
|
||||
@override
|
||||
String? redirect(BuildContext context, GoRouterState state) {
|
||||
return _isContainerUiEnabled(context)
|
||||
? null
|
||||
: const BrowserRoute().location;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const ContainerSelectionScreen();
|
||||
}
|
||||
}
|
||||
|
||||
class ContainerEditRoute extends GoRouteData with $ContainerEditRoute {
|
||||
final String containerData;
|
||||
|
||||
const ContainerEditRoute({required this.containerData});
|
||||
|
||||
@override
|
||||
String? redirect(BuildContext context, GoRouterState state) {
|
||||
return _isContainerUiEnabled(context)
|
||||
? null
|
||||
: const BrowserRoute().location;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return ContainerEditScreen.edit(
|
||||
initialContainer: ContainerDataWithCount.fromJson(
|
||||
jsonDecode(containerData) as Map<String, dynamic>,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ContainerCreateRoute extends GoRouteData with $ContainerCreateRoute {
|
||||
final String containerData;
|
||||
final String tabIds;
|
||||
|
||||
ContainerCreateRoute({required this.containerData, this.tabIds = '[]'});
|
||||
|
||||
@override
|
||||
String? redirect(BuildContext context, GoRouterState state) {
|
||||
return _isContainerUiEnabled(context)
|
||||
? null
|
||||
: const BrowserRoute().location;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
final tabIdsList = jsonDecode(tabIds) as List;
|
||||
final tabIdsSet = tabIdsList.cast<String>().toSet();
|
||||
|
||||
return ContainerEditScreen.create(
|
||||
initialContainer: ContainerData.fromJson(
|
||||
jsonDecode(containerData) as Map<String, dynamic>,
|
||||
),
|
||||
tabIds: tabIdsSet.isNotEmpty ? tabIdsSet : null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ContextMenuRoute extends GoRouteData with $ContextMenuRoute {
|
||||
final String hitResult;
|
||||
|
||||
const ContextMenuRoute({required this.hitResult});
|
||||
|
||||
@override
|
||||
Page<void> buildPage(BuildContext context, GoRouterState state) {
|
||||
return DialogPage(
|
||||
builder: (_) =>
|
||||
ContextMenuDialog(hitResult: HitResultJson.fromJson(hitResult)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TabTreeRoute extends GoRouteData with $TabTreeRoute {
|
||||
final String rootTabId;
|
||||
|
||||
const TabTreeRoute(this.rootTabId);
|
||||
|
||||
@override
|
||||
Page<void> buildPage(BuildContext context, GoRouterState state) {
|
||||
return DialogPage(builder: (_) => TabTreeDialog(rootTabId));
|
||||
}
|
||||
}
|
||||
|
||||
class OpenSharedContentRoute extends GoRouteData with $OpenSharedContentRoute {
|
||||
final String sharedUrl;
|
||||
|
||||
const OpenSharedContentRoute({this.sharedUrl = 'about:blank'});
|
||||
|
||||
@override
|
||||
Page<void> buildPage(BuildContext context, GoRouterState state) {
|
||||
return BottomSheetPage(
|
||||
builder: (_) => OpenSharedContent(
|
||||
sharedUrl: Uri.tryParse(sharedUrl) ?? Uri.parse('about:blank'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TabViewRoute extends GoRouteData with $TabViewRoute {
|
||||
const TabViewRoute();
|
||||
|
||||
@override
|
||||
Page<void> buildPage(BuildContext context, GoRouterState state) {
|
||||
return DialogPage(builder: (_) => const TabViewScreen());
|
||||
}
|
||||
}
|
||||
|
||||
class SelectProfileRoute extends GoRouteData with $SelectProfileRoute {
|
||||
const SelectProfileRoute();
|
||||
|
||||
@override
|
||||
Page<void> buildPage(BuildContext context, GoRouterState state) {
|
||||
return BottomSheetPage(builder: (_) => const SelectProfileDialog());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:nullability/nullability.dart';
|
||||
import 'package:weblibre/core/routing/widgets/bottom_sheet_page.dart';
|
||||
import 'package:weblibre/core/routing/widgets/dialog_page.dart';
|
||||
import 'package:weblibre/domain/entities/profile.dart';
|
||||
import 'package:weblibre/features/about/presentation/screens/about.dart';
|
||||
import 'package:weblibre/features/bangs/data/models/bang.dart';
|
||||
import 'package:weblibre/features/bangs/presentation/screens/categories.dart';
|
||||
import 'package:weblibre/features/bangs/presentation/screens/category.dart';
|
||||
import 'package:weblibre/features/bangs/presentation/screens/edit.dart';
|
||||
import 'package:weblibre/features/bangs/presentation/screens/menu.dart';
|
||||
import 'package:weblibre/features/bangs/presentation/screens/search.dart';
|
||||
import 'package:weblibre/features/bangs/presentation/screens/user.dart';
|
||||
import 'package:weblibre/features/geckoview/features/bookmarks/domain/entities/bookmark_item.dart';
|
||||
import 'package:weblibre/features/geckoview/features/bookmarks/presentation/screens/bookmark_entry_edit.dart';
|
||||
import 'package:weblibre/features/geckoview/features/bookmarks/presentation/screens/bookmark_folder_edit.dart';
|
||||
import 'package:weblibre/features/geckoview/features/bookmarks/presentation/screens/bookmark_list.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/dialogs/tab_tree.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/screens/browser.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/screens/tab_view.dart';
|
||||
import 'package:weblibre/features/geckoview/features/contextmenu/extensions/hit_result.dart';
|
||||
import 'package:weblibre/features/geckoview/features/contextmenu/presentation/context_menu_dialog.dart';
|
||||
import 'package:weblibre/features/geckoview/features/history/presentation/screens/history.dart';
|
||||
import 'package:weblibre/features/geckoview/features/open_link_tools/presentation/dialogs/open_shared_content.dart';
|
||||
import 'package:weblibre/features/geckoview/features/open_link_tools/presentation/screens/unshortener_settings.dart';
|
||||
import 'package:weblibre/features/geckoview/features/open_link_tools/presentation/screens/url_cleaner_settings.dart';
|
||||
import 'package:weblibre/features/geckoview/features/search/presentation/screens/search.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/data/models/container_data.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/presentation/screens/container_draft_suggestions.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/presentation/screens/container_edit.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/presentation/screens/container_list.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/presentation/screens/container_selection.dart';
|
||||
import 'package:weblibre/features/onboarding/presentation/onboarding.dart';
|
||||
import 'package:weblibre/features/settings/presentation/screens/addon_collection.dart';
|
||||
import 'package:weblibre/features/settings/presentation/screens/advanced_settings.dart';
|
||||
import 'package:weblibre/features/settings/presentation/screens/bang_settings.dart';
|
||||
import 'package:weblibre/features/settings/presentation/screens/browsing_settings.dart';
|
||||
import 'package:weblibre/features/settings/presentation/screens/contextual_toolbar_settings.dart';
|
||||
import 'package:weblibre/features/settings/presentation/screens/custom_tracking_protection.dart';
|
||||
import 'package:weblibre/features/settings/presentation/screens/doh_settings.dart';
|
||||
import 'package:weblibre/features/settings/presentation/screens/error_logs_screen.dart';
|
||||
import 'package:weblibre/features/settings/presentation/screens/experimental_settings.dart';
|
||||
import 'package:weblibre/features/settings/presentation/screens/extensions_settings.dart';
|
||||
import 'package:weblibre/features/settings/presentation/screens/fingerprint_settings.dart';
|
||||
import 'package:weblibre/features/settings/presentation/screens/general_settings.dart';
|
||||
import 'package:weblibre/features/settings/presentation/screens/locale_settings.dart';
|
||||
import 'package:weblibre/features/settings/presentation/screens/privacy_security_settings.dart';
|
||||
import 'package:weblibre/features/settings/presentation/screens/search_settings.dart';
|
||||
import 'package:weblibre/features/settings/presentation/screens/settings.dart';
|
||||
import 'package:weblibre/features/settings/presentation/screens/toolbar_layout_settings.dart';
|
||||
import 'package:weblibre/features/settings/presentation/screens/tracking_protection_exceptions.dart';
|
||||
import 'package:weblibre/features/settings/presentation/screens/web_content_settings.dart';
|
||||
import 'package:weblibre/features/settings/presentation/screens/web_engine_hardening.dart';
|
||||
import 'package:weblibre/features/settings/presentation/screens/web_engine_hardening_group.dart';
|
||||
import 'package:weblibre/features/sync/presentation/screens/sync_settings.dart';
|
||||
import 'package:weblibre/features/tor/presentation/screens/country_picker.dart';
|
||||
import 'package:weblibre/features/tor/presentation/screens/tor_proxy.dart';
|
||||
import 'package:weblibre/features/user/domain/presentation/dialogs/select_profile.dart';
|
||||
import 'package:weblibre/features/user/domain/presentation/screens/profile_backup.dart';
|
||||
import 'package:weblibre/features/user/domain/presentation/screens/profile_backup_list.dart';
|
||||
import 'package:weblibre/features/user/domain/presentation/screens/profile_edit.dart';
|
||||
import 'package:weblibre/features/user/domain/presentation/screens/profile_list.dart';
|
||||
import 'package:weblibre/features/user/domain/presentation/screens/profile_restore.dart';
|
||||
import 'package:weblibre/features/user/domain/presentation/widgets/auth_gate.dart';
|
||||
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
||||
import 'package:weblibre/features/web_feed/presentation/add_feed_dialog.dart';
|
||||
import 'package:weblibre/features/web_feed/presentation/screens/feed_article.dart';
|
||||
import 'package:weblibre/features/web_feed/presentation/screens/feed_article_list.dart';
|
||||
import 'package:weblibre/features/web_feed/presentation/screens/feed_edit.dart';
|
||||
import 'package:weblibre/features/web_feed/presentation/screens/feed_list.dart';
|
||||
import 'package:weblibre/features/web_feed/presentation/select_feed_dialog.dart';
|
||||
|
||||
part 'routes.bangs.dart';
|
||||
part 'routes.bookmarks.dart';
|
||||
part 'routes.browser.dart';
|
||||
part 'routes.feeds.dart';
|
||||
part 'routes.g.dart';
|
||||
part 'routes.history.dart';
|
||||
part 'routes.profiles.dart';
|
||||
part 'routes.settings.dart';
|
||||
part 'routes.tor.dart';
|
||||
|
||||
@TypedGoRoute<AboutRoute>(name: 'AboutRoute', path: '/about')
|
||||
class AboutRoute extends GoRouteData with $AboutRoute {
|
||||
@override
|
||||
Page<void> buildPage(BuildContext context, GoRouterState state) {
|
||||
return DialogPage(builder: (_) => const AboutDialogScreen());
|
||||
}
|
||||
}
|
||||
|
||||
@TypedGoRoute<OnboardingRoute>(
|
||||
name: OnboardingRoute.name,
|
||||
path: '${OnboardingRoute.pathPrefix}/:currentRevision/:targetRevision',
|
||||
)
|
||||
class OnboardingRoute extends GoRouteData with $OnboardingRoute {
|
||||
static const name = 'OnboardingRoute';
|
||||
static const pathPrefix = '/onboarding';
|
||||
|
||||
final int currentRevision;
|
||||
final int targetRevision;
|
||||
|
||||
const OnboardingRoute({
|
||||
required this.currentRevision,
|
||||
required this.targetRevision,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return OnboardingScreen(
|
||||
currentRevision: currentRevision,
|
||||
targetRevision: targetRevision,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@TypedGoRoute<LockRoute>(name: LockRoute.name, path: LockRoute.path)
|
||||
class LockRoute extends GoRouteData with $LockRoute {
|
||||
static const name = 'LockRoute';
|
||||
static const path = '/lock';
|
||||
|
||||
const LockRoute();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const LockScreen();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
part of 'routes.dart';
|
||||
|
||||
@TypedGoRoute<FeedListRoute>(
|
||||
name: 'FeedListRoute',
|
||||
path: '/feeds',
|
||||
routes: [
|
||||
TypedGoRoute<FeedAddRoute>(name: FeedAddRoute.name, path: 'add'),
|
||||
TypedGoRoute<FeedArticleListRoute>(
|
||||
name: 'FeedArticleListRoute',
|
||||
path: 'articles/:feedId',
|
||||
),
|
||||
TypedGoRoute<FeedArticleRoute>(
|
||||
name: 'FeedArticleRoute',
|
||||
path: 'article/:articleId',
|
||||
),
|
||||
TypedGoRoute<FeedCreateRoute>(
|
||||
name: 'FeedCreateRoute',
|
||||
path: 'create/:feedId',
|
||||
),
|
||||
TypedGoRoute<SelectFeedDialogRoute>(
|
||||
name: 'SelectFeedDialogRoute',
|
||||
path: 'available/:feedsJson',
|
||||
),
|
||||
TypedGoRoute<FeedEditRoute>(name: 'FeedEditRoute', path: 'edit/:feedId'),
|
||||
],
|
||||
)
|
||||
class FeedListRoute extends GoRouteData with $FeedListRoute {
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const FeedListScreen();
|
||||
}
|
||||
}
|
||||
|
||||
class FeedCreateRoute extends GoRouteData with $FeedCreateRoute {
|
||||
final Uri feedId;
|
||||
|
||||
FeedCreateRoute({required this.feedId});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return FeedEditScreen.create(feedId: feedId);
|
||||
}
|
||||
}
|
||||
|
||||
class SelectFeedDialogRoute extends GoRouteData with $SelectFeedDialogRoute {
|
||||
final String feedsJson;
|
||||
|
||||
const SelectFeedDialogRoute({required this.feedsJson});
|
||||
|
||||
@override
|
||||
Page<void> buildPage(BuildContext context, GoRouterState state) {
|
||||
final feedUris = Set<Uri>.from(
|
||||
(jsonDecode(feedsJson) as List<dynamic>).map(
|
||||
(url) => Uri.parse(url as String),
|
||||
),
|
||||
);
|
||||
|
||||
return BottomSheetPage(
|
||||
builder: (_) => SelectFeedDialog(feedUris: feedUris),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FeedEditRoute extends GoRouteData with $FeedEditRoute {
|
||||
final Uri feedId;
|
||||
|
||||
const FeedEditRoute({required this.feedId});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return FeedEditScreen.edit(feedId: feedId);
|
||||
}
|
||||
}
|
||||
|
||||
class FeedAddRoute extends GoRouteData with $FeedAddRoute {
|
||||
final String? uri;
|
||||
|
||||
static const name = 'FeedAddRoute';
|
||||
|
||||
const FeedAddRoute({required this.uri});
|
||||
|
||||
@override
|
||||
Page<void> buildPage(BuildContext context, GoRouterState state) {
|
||||
return DialogPage(
|
||||
builder: (_) =>
|
||||
AddFeedDialog(initialUri: uri.mapNotNull((uri) => Uri.tryParse(uri))),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FeedArticleListRoute extends GoRouteData with $FeedArticleListRoute {
|
||||
final Uri feedId;
|
||||
|
||||
FeedArticleListRoute({required this.feedId});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return FeedArticleListScreen(feedId: feedId);
|
||||
}
|
||||
}
|
||||
|
||||
class FeedArticleRoute extends GoRouteData with $FeedArticleRoute {
|
||||
final String articleId;
|
||||
|
||||
FeedArticleRoute({required this.articleId});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return FeedArticleScreen(articleId: articleId);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
part of 'routes.dart';
|
||||
|
||||
@TypedGoRoute<HistoryRoute>(
|
||||
name: 'HistoryRoute',
|
||||
path: '/history',
|
||||
routes: [
|
||||
TypedGoRoute<HistoryDownloadsRoute>(
|
||||
name: 'HistoryDownloadsRoute',
|
||||
path: 'downloads',
|
||||
),
|
||||
],
|
||||
)
|
||||
class HistoryRoute extends GoRouteData with $HistoryRoute {
|
||||
const HistoryRoute();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const HistoryScreen();
|
||||
}
|
||||
}
|
||||
|
||||
class HistoryDownloadsRoute extends GoRouteData with $HistoryDownloadsRoute {
|
||||
const HistoryDownloadsRoute();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const HistoryScreen(mode: HistoryScreenMode.downloads);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
part of 'routes.dart';
|
||||
|
||||
@TypedGoRoute<ProfileListRoute>(
|
||||
name: 'ProfileListRoute',
|
||||
path: '/profiles',
|
||||
routes: [
|
||||
TypedGoRoute<EditProfileRoute>(name: 'ProfileEditRoute', path: 'edit'),
|
||||
TypedGoRoute<ProfileBackupListRoute>(
|
||||
name: 'ProfileBackupListRoute',
|
||||
path: 'backup_list',
|
||||
),
|
||||
TypedGoRoute<RestoreProfileRoute>(
|
||||
name: 'RestoreProfileRoute',
|
||||
path: 'restore',
|
||||
),
|
||||
TypedGoRoute<BackupProfileRoute>(
|
||||
name: 'BackupProfileRoute',
|
||||
path: 'backup',
|
||||
),
|
||||
TypedGoRoute<CreateProfileRoute>(
|
||||
name: 'CreateProfileRoute',
|
||||
path: 'create',
|
||||
),
|
||||
],
|
||||
)
|
||||
class ProfileListRoute extends GoRouteData with $ProfileListRoute {
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const ProfileListScreen();
|
||||
}
|
||||
}
|
||||
|
||||
class CreateProfileRoute extends GoRouteData with $CreateProfileRoute {
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const ProfileEditScreen(profile: null);
|
||||
}
|
||||
}
|
||||
|
||||
class EditProfileRoute extends GoRouteData with $EditProfileRoute {
|
||||
final String profile;
|
||||
|
||||
const EditProfileRoute({required this.profile});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return ProfileEditScreen(
|
||||
profile: Profile.fromJson(jsonDecode(profile) as Map<String, dynamic>),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class BackupProfileRoute extends GoRouteData with $BackupProfileRoute {
|
||||
final String profile;
|
||||
|
||||
const BackupProfileRoute({required this.profile});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return ProfileBackupScreen(
|
||||
profile: Profile.fromJson(jsonDecode(profile) as Map<String, dynamic>),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RestoreProfileRoute extends GoRouteData with $RestoreProfileRoute {
|
||||
final String backupFileUri;
|
||||
|
||||
const RestoreProfileRoute({required this.backupFileUri});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return ProfileRestoreScreen(backupFileUri: Uri.parse(backupFileUri));
|
||||
}
|
||||
}
|
||||
|
||||
class ProfileBackupListRoute extends GoRouteData with $ProfileBackupListRoute {
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const ProfileBackupListScreen();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
part of 'routes.dart';
|
||||
|
||||
@TypedGoRoute<SettingsRoute>(
|
||||
name: 'SettingsRoute',
|
||||
path: '/settings',
|
||||
routes: [
|
||||
TypedGoRoute<GeneralSettingsRoute>(
|
||||
name: 'GeneralSettingsRoute',
|
||||
path: 'general',
|
||||
),
|
||||
TypedGoRoute<BrowsingSettingsRoute>(
|
||||
name: 'BrowsingSettingsRoute',
|
||||
path: 'browsing',
|
||||
),
|
||||
TypedGoRoute<PrivacySecuritySettingsRoute>(
|
||||
name: 'PrivacySecuritySettingsRoute',
|
||||
path: 'privacy_security',
|
||||
),
|
||||
TypedGoRoute<ToolbarLayoutSettingsRoute>(
|
||||
name: 'ToolbarLayoutSettingsRoute',
|
||||
path: 'toolbar_layout',
|
||||
),
|
||||
TypedGoRoute<WebContentSettingsRoute>(
|
||||
name: 'WebContentSettingsRoute',
|
||||
path: 'web_content',
|
||||
),
|
||||
TypedGoRoute<SearchSettingsRoute>(
|
||||
name: 'SearchSettingsRoute',
|
||||
path: 'search',
|
||||
),
|
||||
TypedGoRoute<ExtensionsSettingsRoute>(
|
||||
name: 'ExtensionsSettingsRoute',
|
||||
path: 'extensions',
|
||||
),
|
||||
TypedGoRoute<AdvancedSettingsRoute>(
|
||||
name: 'AdvancedSettingsRoute',
|
||||
path: 'advanced',
|
||||
),
|
||||
TypedGoRoute<ExperimentalSettingsRoute>(
|
||||
name: 'ExperimentalSettingsRoute',
|
||||
path: 'experimental',
|
||||
),
|
||||
TypedGoRoute<BangSettingsRoute>(name: 'BangSettingsRoute', path: 'bang'),
|
||||
TypedGoRoute<WebEngineHardeningRoute>(
|
||||
name: 'WebEngineHardeningRoute',
|
||||
path: 'hardening',
|
||||
routes: [
|
||||
TypedGoRoute<WebEngineHardeningGroupRoute>(
|
||||
name: 'WebEngineHardeningGroupRoute',
|
||||
path: 'group/:group',
|
||||
),
|
||||
],
|
||||
),
|
||||
TypedGoRoute<DohSettingsRoute>(name: 'DohSettingsRoute', path: 'doh'),
|
||||
TypedGoRoute<FingerprintSettingsRoute>(
|
||||
name: 'FingerprintSettingsRoute',
|
||||
path: 'fingerprint',
|
||||
),
|
||||
TypedGoRoute<LocaleSettingsRoute>(
|
||||
name: 'LocaleSettingsRoute',
|
||||
path: 'locales',
|
||||
),
|
||||
TypedGoRoute<AddonCollectionRoute>(
|
||||
name: 'AddonCollectionRoute',
|
||||
path: 'addon_collection',
|
||||
),
|
||||
TypedGoRoute<TrackingProtectionExceptionsRoute>(
|
||||
name: 'TrackingProtectionExceptionsRoute',
|
||||
path: 'tracking_protection_exceptions',
|
||||
),
|
||||
TypedGoRoute<CustomTrackingProtectionRoute>(
|
||||
name: 'CustomTrackingProtectionRoute',
|
||||
path: 'custom_tracking_protection',
|
||||
),
|
||||
TypedGoRoute<ErrorLogsRoute>(name: 'ErrorLogsRoute', path: 'error_logs'),
|
||||
TypedGoRoute<SyncSettingsRoute>(name: 'SyncSettingsRoute', path: 'sync'),
|
||||
TypedGoRoute<UrlCleanerSettingsRoute>(
|
||||
name: 'UrlCleanerSettingsRoute',
|
||||
path: 'url_cleaner',
|
||||
),
|
||||
TypedGoRoute<UnshortenerSettingsRoute>(
|
||||
name: 'UnshortenerSettingsRoute',
|
||||
path: 'unshortener',
|
||||
),
|
||||
TypedGoRoute<ContextualToolbarSettingsRoute>(
|
||||
name: 'ContextualToolbarSettingsRoute',
|
||||
path: 'contextual_toolbar',
|
||||
),
|
||||
],
|
||||
)
|
||||
class SettingsRoute extends GoRouteData with $SettingsRoute {
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const SettingsScreen();
|
||||
}
|
||||
}
|
||||
|
||||
class GeneralSettingsRoute extends GoRouteData with $GeneralSettingsRoute {
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const GeneralSettingsScreen();
|
||||
}
|
||||
}
|
||||
|
||||
class BrowsingSettingsRoute extends GoRouteData with $BrowsingSettingsRoute {
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const BrowsingSettingsScreen();
|
||||
}
|
||||
}
|
||||
|
||||
class PrivacySecuritySettingsRoute extends GoRouteData
|
||||
with $PrivacySecuritySettingsRoute {
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const PrivacySecuritySettingsScreen();
|
||||
}
|
||||
}
|
||||
|
||||
class ToolbarLayoutSettingsRoute extends GoRouteData
|
||||
with $ToolbarLayoutSettingsRoute {
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const ToolbarLayoutSettingsScreen();
|
||||
}
|
||||
}
|
||||
|
||||
class WebContentSettingsRoute extends GoRouteData
|
||||
with $WebContentSettingsRoute {
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const WebContentSettingsScreen();
|
||||
}
|
||||
}
|
||||
|
||||
class SearchSettingsRoute extends GoRouteData with $SearchSettingsRoute {
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const SearchSettingsScreen();
|
||||
}
|
||||
}
|
||||
|
||||
class ExtensionsSettingsRoute extends GoRouteData
|
||||
with $ExtensionsSettingsRoute {
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const ExtensionsSettingsScreen();
|
||||
}
|
||||
}
|
||||
|
||||
class AdvancedSettingsRoute extends GoRouteData with $AdvancedSettingsRoute {
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const AdvancedSettingsScreen();
|
||||
}
|
||||
}
|
||||
|
||||
class ExperimentalSettingsRoute extends GoRouteData
|
||||
with $ExperimentalSettingsRoute {
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const ExperimentalSettingsScreen();
|
||||
}
|
||||
}
|
||||
|
||||
class BangSettingsRoute extends GoRouteData with $BangSettingsRoute {
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const BangSettingsScreen();
|
||||
}
|
||||
}
|
||||
|
||||
class DohSettingsRoute extends GoRouteData with $DohSettingsRoute {
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const DohSettingsScreen();
|
||||
}
|
||||
}
|
||||
|
||||
class FingerprintSettingsRoute extends GoRouteData
|
||||
with $FingerprintSettingsRoute {
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const FingerprintSettingsScreen();
|
||||
}
|
||||
}
|
||||
|
||||
class LocaleSettingsRoute extends GoRouteData with $LocaleSettingsRoute {
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const LocaleSettingsScreen();
|
||||
}
|
||||
}
|
||||
|
||||
class AddonCollectionRoute extends GoRouteData with $AddonCollectionRoute {
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const AddonCollectionScreen();
|
||||
}
|
||||
}
|
||||
|
||||
class WebEngineHardeningRoute extends GoRouteData
|
||||
with $WebEngineHardeningRoute {
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const WebEngineHardeningScreen();
|
||||
}
|
||||
}
|
||||
|
||||
class WebEngineHardeningGroupRoute extends GoRouteData
|
||||
with $WebEngineHardeningGroupRoute {
|
||||
final String group;
|
||||
|
||||
const WebEngineHardeningGroupRoute({required this.group});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return WebEngineHardeningGroupScreen(groupName: group);
|
||||
}
|
||||
}
|
||||
|
||||
class TrackingProtectionExceptionsRoute extends GoRouteData
|
||||
with $TrackingProtectionExceptionsRoute {
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const TrackingProtectionExceptionsScreen();
|
||||
}
|
||||
}
|
||||
|
||||
class ErrorLogsRoute extends GoRouteData with $ErrorLogsRoute {
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const ErrorLogsScreen();
|
||||
}
|
||||
}
|
||||
|
||||
class CustomTrackingProtectionRoute extends GoRouteData
|
||||
with $CustomTrackingProtectionRoute {
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const CustomTrackingProtectionScreen();
|
||||
}
|
||||
}
|
||||
|
||||
class SyncSettingsRoute extends GoRouteData with $SyncSettingsRoute {
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const SyncSettingsScreen();
|
||||
}
|
||||
}
|
||||
|
||||
class UrlCleanerSettingsRoute extends GoRouteData
|
||||
with $UrlCleanerSettingsRoute {
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const UrlCleanerSettingsScreen();
|
||||
}
|
||||
}
|
||||
|
||||
class UnshortenerSettingsRoute extends GoRouteData
|
||||
with $UnshortenerSettingsRoute {
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const UnshortenerSettingsScreen();
|
||||
}
|
||||
}
|
||||
|
||||
class ContextualToolbarSettingsRoute extends GoRouteData
|
||||
with $ContextualToolbarSettingsRoute {
|
||||
const ContextualToolbarSettingsRoute();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const ContextualToolbarSettingsScreen();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
part of 'routes.dart';
|
||||
|
||||
@TypedGoRoute<TorProxyRoute>(
|
||||
name: 'TorProxyRoute',
|
||||
path: '/tor',
|
||||
routes: [
|
||||
TypedGoRoute<TorCountryPickerRoute>(
|
||||
name: 'TorCountryPickerRoute',
|
||||
path: 'country_picker',
|
||||
),
|
||||
],
|
||||
)
|
||||
class TorProxyRoute extends GoRouteData with $TorProxyRoute {
|
||||
const TorProxyRoute();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const TorProxyScreen();
|
||||
}
|
||||
}
|
||||
|
||||
class TorCountryPickerRoute extends GoRouteData with $TorCountryPickerRoute {
|
||||
final String title;
|
||||
final String? $extra;
|
||||
|
||||
const TorCountryPickerRoute({required this.title, this.$extra});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return CountryPickerScreen(title: title, selectedCountryCode: $extra);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// A bottom sheet page with Material entrance and exit animations.
|
||||
/// Similar to DialogPage but displays content as a modal bottom sheet.
|
||||
class BottomSheetPage<T> extends Page<T> {
|
||||
final WidgetBuilder builder;
|
||||
final Color? barrierColor;
|
||||
final bool barrierDismissible;
|
||||
final String? barrierLabel;
|
||||
final bool isScrollControlled;
|
||||
final bool useSafeArea;
|
||||
|
||||
const BottomSheetPage({
|
||||
required this.builder,
|
||||
this.barrierColor,
|
||||
this.barrierDismissible = true,
|
||||
this.barrierLabel,
|
||||
this.isScrollControlled = true,
|
||||
this.useSafeArea = true,
|
||||
super.key,
|
||||
super.name,
|
||||
super.arguments,
|
||||
super.restorationId,
|
||||
});
|
||||
|
||||
@override
|
||||
Route<T> createRoute(BuildContext context) => ModalBottomSheetRoute<T>(
|
||||
settings: this,
|
||||
builder: builder,
|
||||
barrierLabel:
|
||||
barrierLabel ??
|
||||
MaterialLocalizations.of(context).modalBarrierDismissLabel,
|
||||
backgroundColor: Theme.of(context).bottomSheetTheme.modalBackgroundColor,
|
||||
elevation: Theme.of(context).bottomSheetTheme.modalElevation,
|
||||
shape: Theme.of(context).bottomSheetTheme.shape,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
constraints: Theme.of(context).bottomSheetTheme.constraints,
|
||||
modalBarrierColor:
|
||||
barrierColor ?? Theme.of(context).bottomSheetTheme.modalBarrierColor,
|
||||
isScrollControlled: isScrollControlled,
|
||||
isDismissible: barrierDismissible,
|
||||
useSafeArea: useSafeArea,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// A dialog page with Material entrance and exit animations, modal barrier color,
|
||||
/// and modal barrier behavior (dialog is dismissible with a tap on the barrier).
|
||||
class DialogPage<T> extends Page<T> {
|
||||
final Offset? anchorPoint;
|
||||
final Color? barrierColor;
|
||||
final bool barrierDismissible;
|
||||
final String? barrierLabel;
|
||||
final bool useSafeArea;
|
||||
final CapturedThemes? themes;
|
||||
final WidgetBuilder builder;
|
||||
|
||||
const DialogPage({
|
||||
required this.builder,
|
||||
this.anchorPoint,
|
||||
this.barrierColor,
|
||||
this.barrierDismissible = true,
|
||||
this.barrierLabel,
|
||||
this.useSafeArea = true,
|
||||
this.themes,
|
||||
super.key,
|
||||
super.name,
|
||||
super.arguments,
|
||||
super.restorationId,
|
||||
});
|
||||
|
||||
@override
|
||||
Route<T> createRoute(BuildContext context) => DialogRoute<T>(
|
||||
context: context,
|
||||
settings: this,
|
||||
builder: builder,
|
||||
anchorPoint: anchorPoint,
|
||||
barrierColor:
|
||||
barrierColor ??
|
||||
DialogTheme.of(context).barrierColor ??
|
||||
Theme.of(context).dialogTheme.barrierColor ??
|
||||
Colors.black54,
|
||||
barrierDismissible: barrierDismissible,
|
||||
barrierLabel: barrierLabel,
|
||||
useSafeArea: useSafeArea,
|
||||
themes: themes,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/// Reusable sort directions for list items that share common sortable fields
|
||||
/// (title, URL, date).
|
||||
enum SortField { titleAsc, titleDesc, urlAsc, urlDesc, dateAsc, dateDesc }
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'package:uuid/data.dart';
|
||||
import 'package:uuid/rng.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
const uuid = Uuid(goptions: GlobalOptions(CryptoRNG()));
|
||||
Reference in New Issue
Block a user