merged develop

This commit is contained in:
2026-09-27 14:58:07 +02:00
188 changed files with 7474 additions and 2558 deletions
+10 -19
View File
@@ -2,17 +2,15 @@ import 'dart:developer';
import 'dart:io';
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:localstore/localstore.dart';
import '../api/request_cache.dart';
import '../api/cache_store.dart';
import '../session/account_codec.dart';
import 'hydrated_storage_bootstrap.dart';
/// Per-account local data: every account gets its own HydratedBloc box and
/// Localstore cache collection, so switching back is instant and offline.
/// Only the app settings live in the shared [global] box.
/// request cache directory (see [CacheStore]), so switching back is instant
/// and offline. Only the app settings live in the shared [global] box.
abstract final class AccountStorage {
static const _legacyCollection = 'MarianumMobile';
static const _settingsKey = 'SettingsCubit';
static late String _baseDir;
@@ -36,17 +34,13 @@ abstract final class AccountStorage {
}
}
/// Points HydratedBloc and the request cache at [account]'s data. Blocs
/// keep the storage they were created with, so the per-account blocs have
/// to be recreated afterwards.
/// Points HydratedBloc at [account]'s data; the request cache follows the
/// active account on its own. Blocs keep the storage they were created
/// with, so the per-account blocs have to be recreated afterwards.
static Future<void> activate(AccountEntry? account) async {
if (account == null) {
HydratedBloc.storage = InMemoryStorage();
RequestCache.collection = _legacyCollection;
return;
}
HydratedBloc.storage = await _storageFor(account.namespace);
RequestCache.collection = cacheCollection(account.namespace);
HydratedBloc.storage = account == null
? InMemoryStorage()
: await _storageFor(account.namespace);
}
/// Removes all local data of the account with [namespace].
@@ -62,15 +56,12 @@ abstract final class AccountStorage {
final dir = Directory(_dirFor(namespace));
if (dir.existsSync()) await dir.delete(recursive: true);
}
await Localstore.instance.collection(cacheCollection(namespace)).delete();
} on Object catch (e, s) {
log('Account storage delete failed: $e', stackTrace: s);
}
await CacheStore.instance.deleteNamespace(namespace);
}
static String cacheCollection(String namespace) =>
namespace.isEmpty ? _legacyCollection : '$_legacyCollection-$namespace';
static Future<Storage> _storageFor(String namespace) async =>
_open[namespace] ??= await buildHydratedStorageWithFallback(
_ensureDir(_dirFor(namespace)),
+23 -5
View File
@@ -8,8 +8,22 @@ enum MarianumConnectEndpoint { live, beta, custom }
@JsonSerializable()
class DevToolsSettings {
bool showPerformanceOverlay;
bool checkerboardOffscreenLayers;
bool checkerboardRasterCacheImages;
@JsonKey(defaultValue: false)
bool showFrameStats;
@JsonKey(defaultValue: false)
bool showMemoryStats;
@JsonKey(defaultValue: false)
bool showSemanticsDebugger;
@JsonKey(defaultValue: false)
bool slowAnimations;
/// Forced text scale factor; `0` follows the system setting.
@JsonKey(defaultValue: 0)
double textScaleOverride;
@JsonKey(defaultValue: MarianumConnectEndpoint.live)
MarianumConnectEndpoint marianumConnectEndpoint;
@@ -24,8 +38,11 @@ class DevToolsSettings {
DevToolsSettings({
required this.showPerformanceOverlay,
required this.checkerboardOffscreenLayers,
required this.checkerboardRasterCacheImages,
this.showFrameStats = false,
this.showMemoryStats = false,
this.showSemanticsDebugger = false,
this.slowAnimations = false,
this.textScaleOverride = 0,
this.marianumConnectEndpoint = MarianumConnectEndpoint.live,
this.marianumConnectCustomUrl = '',
this.emergencyNoticeUrl = '',
@@ -51,7 +68,8 @@ class DevToolsSettings {
/// Compiled-in source for the backend-independent emergency notice. Must live
/// on infrastructure that is reachable even when MarianumConnect is down.
static const String emergencyNoticeDefaultUrl = 'https://www.marianum-fulda.de/~aushang/marMobile.override';
static const String emergencyNoticeDefaultUrl =
'https://www.marianum-fulda.de/~aushang/marMobile.override';
String? resolveEmergencyNoticeUrl() =>
sanitizeCustomUrl(emergencyNoticeUrl) ??
+23 -17
View File
@@ -6,27 +6,33 @@ part of 'dev_tools_settings.dart';
// JsonSerializableGenerator
// **************************************************************************
DevToolsSettings _$DevToolsSettingsFromJson(
Map<String, dynamic> json,
) => DevToolsSettings(
showPerformanceOverlay: json['showPerformanceOverlay'] as bool,
checkerboardOffscreenLayers: json['checkerboardOffscreenLayers'] as bool,
checkerboardRasterCacheImages: json['checkerboardRasterCacheImages'] as bool,
marianumConnectEndpoint:
$enumDecodeNullable(
_$MarianumConnectEndpointEnumMap,
json['marianumConnectEndpoint'],
) ??
MarianumConnectEndpoint.live,
marianumConnectCustomUrl: json['marianumConnectCustomUrl'] as String? ?? '',
emergencyNoticeUrl: json['emergencyNoticeUrl'] as String? ?? '',
);
DevToolsSettings _$DevToolsSettingsFromJson(Map<String, dynamic> json) =>
DevToolsSettings(
showPerformanceOverlay: json['showPerformanceOverlay'] as bool,
showFrameStats: json['showFrameStats'] as bool? ?? false,
showMemoryStats: json['showMemoryStats'] as bool? ?? false,
showSemanticsDebugger: json['showSemanticsDebugger'] as bool? ?? false,
slowAnimations: json['slowAnimations'] as bool? ?? false,
textScaleOverride: (json['textScaleOverride'] as num?)?.toDouble() ?? 0,
marianumConnectEndpoint:
$enumDecodeNullable(
_$MarianumConnectEndpointEnumMap,
json['marianumConnectEndpoint'],
) ??
MarianumConnectEndpoint.live,
marianumConnectCustomUrl:
json['marianumConnectCustomUrl'] as String? ?? '',
emergencyNoticeUrl: json['emergencyNoticeUrl'] as String? ?? '',
);
Map<String, dynamic> _$DevToolsSettingsToJson(DevToolsSettings instance) =>
<String, dynamic>{
'showPerformanceOverlay': instance.showPerformanceOverlay,
'checkerboardOffscreenLayers': instance.checkerboardOffscreenLayers,
'checkerboardRasterCacheImages': instance.checkerboardRasterCacheImages,
'showFrameStats': instance.showFrameStats,
'showMemoryStats': instance.showMemoryStats,
'showSemanticsDebugger': instance.showSemanticsDebugger,
'slowAnimations': instance.slowAnimations,
'textScaleOverride': instance.textScaleOverride,
'marianumConnectEndpoint':
_$MarianumConnectEndpointEnumMap[instance.marianumConnectEndpoint]!,
'marianumConnectCustomUrl': instance.marianumConnectCustomUrl,
+28 -2
View File
@@ -9,19 +9,43 @@ import 'package:hydrated_bloc/hydrated_bloc.dart';
Future<Storage> buildHydratedStorageWithFallback(String directoryPath) async {
final directory = HydratedStorageDirectory(directoryPath);
try {
return await HydratedStorage.build(storageDirectory: directory);
return await _build(directory);
} catch (e, s) {
log('HydratedStorage open failed, rebuilding: $e', stackTrace: s);
}
try {
await _deleteHydratedBox(directoryPath);
return await HydratedStorage.build(storageDirectory: directory);
return await _build(directory);
} catch (e, s) {
log('HydratedStorage rebuild failed, using memory: $e', stackTrace: s);
return InMemoryStorage();
}
}
final Expando<List<String> Function()> _keyReaders = Expando();
Future<Storage> _build(HydratedStorageDirectory directory) async {
final storage = await HydratedStorage.build(storageDirectory: directory);
// hydrated_bloc exposes no key listing; the Hive instance is the only way
// in. Every build replaces the static one, so it is captured per storage.
// ignore: invalid_use_of_visible_for_testing_member
final hive = HydratedStorage.hive;
_keyReaders[storage] = () =>
hive.box<dynamic>('hydrated_box').keys.map((k) => '$k').toList();
return storage;
}
/// All keys persisted in [storage], sorted; `null` when they can't be
/// enumerated.
List<String>? hydratedStorageKeys(Storage storage) {
if (storage is InMemoryStorage) return storage.keys.toList()..sort();
try {
return _keyReaders[storage]?.call()?..sort();
} on Object {
return null;
}
}
Future<void> _deleteHydratedBox(String directoryPath) async {
final directory = Directory(directoryPath);
if (!directory.existsSync()) return;
@@ -37,6 +61,8 @@ Future<void> _deleteHydratedBox(String directoryPath) async {
class InMemoryStorage implements Storage {
final Map<String, dynamic> _values = {};
Iterable<String> get keys => _values.keys;
@override
dynamic read(String key) => _values[key];
+4
View File
@@ -10,12 +10,16 @@ class ModulesSettings {
List<Modules> hiddenModules;
bool autoFillBottomBar;
int fixedBottomBarSlots;
// Set once the teacher module defaults were applied, so a module the teacher
// re-enables afterwards stays visible.
bool teacherDefaultsApplied;
ModulesSettings({
required this.moduleOrder,
required this.hiddenModules,
this.autoFillBottomBar = true,
this.fixedBottomBarSlots = 3,
this.teacherDefaultsApplied = false,
});
factory ModulesSettings.fromJson(Map<String, dynamic> json) =>
+2
View File
@@ -16,6 +16,7 @@ ModulesSettings _$ModulesSettingsFromJson(Map<String, dynamic> json) =>
.toList(),
autoFillBottomBar: json['autoFillBottomBar'] as bool? ?? true,
fixedBottomBarSlots: (json['fixedBottomBarSlots'] as num?)?.toInt() ?? 3,
teacherDefaultsApplied: json['teacherDefaultsApplied'] as bool? ?? false,
);
Map<String, dynamic> _$ModulesSettingsToJson(
@@ -27,6 +28,7 @@ Map<String, dynamic> _$ModulesSettingsToJson(
.toList(),
'autoFillBottomBar': instance.autoFillBottomBar,
'fixedBottomBarSlots': instance.fixedBottomBarSlots,
'teacherDefaultsApplied': instance.teacherDefaultsApplied,
};
const _$ModulesEnumMap = {