refactor: extract shared mixins and helpers, drop dead abstractions

Introduces shared seams for paginated views, D-pad reorder, media control
routing, async singletons and the device method channel, then points the
open-coded copies at them.

Also removes unused models and duplicated provider/server plumbing, folds
the twice-implemented artifact store in the server, and factors the
repeated Flutter toolchain prologue in CI into a composite action.
This commit is contained in:
edde746
2026-07-26 06:09:48 +02:00
parent 61344f7862
commit 352b88109b
217 changed files with 6813 additions and 8773 deletions
+6 -34
View File
@@ -10,6 +10,7 @@ import 'future_extensions.dart';
import 'isolate_helper.dart';
import 'log_redaction_manager.dart';
import 'managed_http_client.dart';
import 'url_utils.dart';
import '../exceptions/media_server_exceptions.dart';
// Platform-specific imports are conditional
@@ -412,39 +413,13 @@ class MediaServerHttpClient {
/// Append query parameters to an already-parsed URI.
Uri _appendQuery(Uri uri, Map<String, dynamic>? queryParameters) {
if (queryParameters == null || queryParameters.isEmpty) return uri;
final query = MediaServerHttpClient.encodeQueryParameters(queryParameters);
final query = encodeQueryParameters(queryParameters);
if (query.isEmpty) return uri;
final existing = uri.query;
final combined = existing.isEmpty ? query : '$existing&$query';
return uri.replace(query: combined);
}
/// Encode query params with `%20` for spaces (not `+`).
/// Null values are omitted and iterable values are emitted as repeated keys.
static String encodeQueryParameters(Map<String, Object?>? params) {
if (params == null || params.isEmpty) return '';
final parts = <String>[];
void add(String key, Object? value) {
if (value == null) return;
if (value is Iterable) {
for (final item in value) {
add(key, item);
}
return;
}
parts.add(
'${Uri.encodeComponent(key)}='
'${Uri.encodeComponent(value.toString())}',
);
}
for (final entry in params.entries) {
add(entry.key, entry.value);
}
return parts.join('&');
}
static bool _isAbsoluteUrl(String url) => url.startsWith('http://') || url.startsWith('https://');
/// Set the request body, choosing encoding based on the body type.
@@ -461,14 +436,11 @@ class MediaServerHttpClient {
return;
}
// Content type comes from the caller's headers (Jellyfin/Plex put
// `application/json` in their defaults); `request.body` falls back to
// text/plain. Don't add one here — `request.headers` is case-insensitive,
// and the setter above has already filled the key in either way.
request.body = jsonEncode(body);
// http.BaseRequest's headers map is case-sensitive; Jellyfin returns 415
// if both `Content-Type` (from defaults) and `content-type` (added below)
// end up coexisting, so check both casings before adding.
final hasContentType = request.headers.keys.any((k) => k.toLowerCase() == 'content-type');
if (!hasContentType) {
request.headers['content-type'] = 'application/json';
}
}
/// Decode the response body: lenient UTF-8, then JSON parse if applicable.