prepare for multiple apps

This commit is contained in:
Fabian Freund
2026-04-06 12:23:11 +02:00
parent bd1600e8dc
commit 5afc323f04
904 changed files with 29 additions and 29 deletions
@@ -0,0 +1,32 @@
/*
* Copyright (c) 2024-2026 Fabian Freund.
*
* This file is part of WebLibre
* (see https://weblibre.eu).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
extension DateTimeFormat on DateTime {
String _twoDigits(int n) => n.toString().padLeft(2, '0');
String formatWithMinutePrecision() {
final year = this.year.toString();
final month = _twoDigits(this.month);
final day = _twoDigits(this.day);
final hour = _twoDigits(this.hour);
final minute = _twoDigits(this.minute);
return '$year-$month-$day $hour:$minute';
}
}
@@ -0,0 +1,28 @@
/*
* Copyright (c) 2024-2026 Fabian Freund.
*
* This file is part of WebLibre
* (see https://weblibre.eu).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
extension DurationX on Duration {
String formatTimeZoneOffset() {
final hours = inHours.abs().toString().padLeft(2, '0');
final minutes = (inMinutes.abs() % 60).toString().padLeft(2, '0');
final sign = isNegative ? '-' : '+';
return '$sign$hours$minutes';
}
}
@@ -0,0 +1,57 @@
/*
* Copyright (c) 2024-2026 Fabian Freund.
*
* This file is part of WebLibre
* (see https://weblibre.eu).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import 'dart:convert';
import 'package:http/http.dart';
/// This is modified from default latin1 fallback to use utf8
Encoding _encodingForHeaders(Map<String, String> headers) =>
_encodingForContentTypeHeader(_contentTypeForHeaders(headers), utf8);
/// Returns the [MediaType] object for the given headers' content-type.
///
/// Defaults to `application/octet-stream`.
MediaType _contentTypeForHeaders(Map<String, String> headers) {
final contentType = headers['content-type'];
if (contentType != null) return MediaType.parse(contentType);
return MediaType('application', 'octet-stream');
}
Encoding _encodingForContentTypeHeader(
MediaType contentTypeHeader, [
Encoding fallback = latin1,
]) {
final charset = contentTypeHeader.parameters['charset'];
// Default to utf8 for application/json when charset is unspecified.
if (contentTypeHeader.type == 'application' &&
contentTypeHeader.subtype == 'json' &&
charset == null) {
return utf8;
}
// Attempt to find the encoding or fall back to the default.
return charset != null ? Encoding.getByName(charset) ?? fallback : fallback;
}
extension ResponesEncoding on Response {
String get bodyUnicodeFallback =>
_encodingForHeaders(headers).decode(bodyBytes);
}
+38
View File
@@ -0,0 +1,38 @@
/*
* Copyright (c) 2024-2026 Fabian Freund.
*
* This file is part of WebLibre
* (see https://weblibre.eu).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import 'dart:ui';
import 'package:fast_equatable/hash.dart';
extension ImageHash on Image {
Future<int?> calculateHash() async {
final byteData = await toByteData();
if (byteData != null) {
final digest = secureHash(byteData);
return digest;
}
return null;
}
// Future<EquatableImage> toEquatable() {
// return EquatableImage.calculate(this);
// }
}
@@ -0,0 +1,32 @@
/*
* Copyright (c) 2024-2026 Fabian Freund.
*
* This file is part of WebLibre
* (see https://weblibre.eu).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
extension UniqueItems<T> on Iterable<T> {
Iterable<T> findDuplicates() sync* {
final seen = <T>{};
for (final item in this) {
if (seen.contains(item)) {
yield item;
} else {
seen.add(item);
}
}
}
}
+46
View File
@@ -0,0 +1,46 @@
/*
* Copyright (c) 2024-2026 Fabian Freund.
*
* This file is part of WebLibre
* (see https://weblibre.eu).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import 'dart:ui' as ui;
import 'package:intl/locale.dart' as intl;
extension LocaleFormat on intl.Locale {
String rawToString(String separator) {
final StringBuffer out = StringBuffer(languageCode);
if (scriptCode != null && scriptCode!.isNotEmpty) {
out.write('$separator$scriptCode');
}
final String? countryCode = this.countryCode;
if (countryCode != null && countryCode.isNotEmpty) {
out.write('$separator${this.countryCode}');
}
return out.toString();
}
}
extension LocaleConverter on ui.Locale {
intl.Locale toIntlLocale() {
return intl.Locale.fromSubtags(
languageCode: languageCode,
countryCode: countryCode,
scriptCode: scriptCode,
);
}
}
@@ -0,0 +1,26 @@
/*
* Copyright (c) 2024-2026 Fabian Freund.
*
* This file is part of WebLibre
* (see https://weblibre.eu).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import 'package:flutter/material.dart';
extension RelativeSafeArea on MediaQueryData {
double relativeSafeArea() {
return 1.0 - (padding.top / size.height);
}
}
@@ -0,0 +1,48 @@
/*
* Copyright (c) 2024-2026 Fabian Freund.
*
* This file is part of WebLibre
* (see https://weblibre.eu).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import 'dart:async';
import 'package:hooks_riverpod/misc.dart';
import 'package:riverpod/riverpod.dart';
extension CacheForExtension on Ref {
/// Keeps the provider alive for [duration] after the last listener is removed.
KeepAliveLink cacheFor(Duration duration) {
Timer? timer;
final link = keepAlive();
onCancel(() {
// All listeners are gone — start the dispose timer.
timer = Timer(duration, link.close);
});
onResume(() {
// A new listener was added — cancel the timer.
timer?.cancel();
});
onDispose(() {
// Provider is being fully disposed — clean up.
timer?.cancel();
});
return link;
}
}
+26
View File
@@ -0,0 +1,26 @@
/*
* Copyright (c) 2024-2026 Fabian Freund.
*
* This file is part of WebLibre
* (see https://weblibre.eu).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
extension StringExtension on String {
String toCapitalized() =>
isEmpty ? this : '${this[0].toUpperCase()}${substring(1)}';
bool startsWithIgnoreCase(String prefix) =>
toLowerCase().startsWith(prefix.toLowerCase());
}
+44
View File
@@ -0,0 +1,44 @@
/*
* Copyright (c) 2024-2026 Fabian Freund.
*
* This file is part of WebLibre
* (see https://weblibre.eu).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import 'package:weblibre/utils/uri_policy.dart';
extension UriX on Uri {
Uri get base => Uri.parse('$scheme://$authority');
bool get hasSupportedScheme =>
allSupportedSchemes.any((s) => s.name == scheme);
bool get isHttp => isScheme('http');
bool get isHttps => isScheme('https');
bool get isHttpOrHttps => isHttp || isHttps;
bool get isLocalhost => host == 'localhost' || host == '127.0.0.1';
/// Removes a bare root path (`/`) when there is no query or fragment, so
/// that `https://example.com/` and `https://example.com` are treated as
/// equivalent.
Uri get normalized {
if (path == '/' && !hasQuery && !hasFragment) {
return replace(path: '');
}
return this;
}
}