Files
plezy/lib/services/pip_service.dart
edde746 84bc4b2618 fix(i18n): translate the strings that still reached the UI in English
Non-English users saw English text in a dozen places and blank labels in
sixteen more.

The English came from sites that produce their copy away from the widget
that renders it, which is what the structural hardcoded-string check
cannot see: picture-in-picture refused with a raw literal instead of the
pipErrors.notSupported key that already existed; the two Jellyfin/Emby
auth throws missing display: rendered their developer message on the
add-server form; ServerParsingException.toString() fed its English into
the localized "Failed to load servers" wrapper; Watch Together
interpolated the whole PeerError, so a failed create read "Failed to
create session: PeerError(PeerErrorType.timeout): Timed out creating
session" and join printed its prefix twice; the hub and playlist
continuation footers rendered exception.toString(); shader rows showed an
English title over an already translated subtitle; the player queue fell
back to the raw Dart enum name; a Plex home user with no title showed
"Unknown"; a failed player start showed "Exception: Failed to initialize
player"; and the tvOS top-shelf header was hardcoded in an extension that
has no Flutter engine.

The blanks came from three recent features that added English keys
without translations. clean_translations.py filled all 21 siblings with
empty strings, so the Android TV resolution switch, every Jellyfin/Emby
recording-rule field, the demuxer row, the Companion Remote address
caption and the Seerr blocklist pill rendered nothing at all.

Two fixes are structural rather than key swaps. ContinuationStatusSliver
now takes an errorContext and calls a new non-logging
localizedLoadErrorText, so no future throw can leak through it. lib/mpv
stays free of user-facing copy: it raises a PlayerInitializationException
sentinel and a PlayerError.playerInitFailed cause tag that the player
screen resolves to localized text. The tvOS section title travels in the
shelf payload, additively, so an older cache still renders.
2026-08-24 05:23:05 +02:00

83 lines
3.1 KiB
Dart

import 'dart:io' show Platform;
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:plezy/i18n/strings.g.dart';
class PipService {
static const MethodChannel _channel = MethodChannel('com.plezy/pip');
/// PiP is only implemented natively on Android, iOS, and macOS.
static bool get _isAvailable => Platform.isAndroid || Platform.isIOS || Platform.isMacOS;
static final PipService _instance = PipService._internal();
factory PipService() => _instance;
PipService._internal() {
if (!_isAvailable) return;
// Listen for callbacks from native Android
_channel.setMethodCallHandler(_handleMethodCall);
}
/// ValueNotifier for PiP state - widgets can listen to this
final ValueNotifier<bool> isPipActive = ValueNotifier<bool>(false);
/// Callback invoked when native side is about to auto-enter PiP (API 26-30 path)
static VoidCallback? onAutoPipEntering;
Future<dynamic> _handleMethodCall(MethodCall call) async {
switch (call.method) {
case 'onPipChanged':
final isInPip = call.arguments as bool;
isPipActive.value = isInPip;
break;
case 'onAutoPipEntering':
onAutoPipEntering?.call();
break;
}
}
static Future<bool> isSupported() async {
if (!_isAvailable) return false;
return await _channel.invokeMethod<bool>('isSupported') ?? false;
}
/// Tell the native side whether auto-PiP is ready and the current video dimensions
static Future<void> setAutoPipReady({required bool ready, int? width, int? height}) async {
if (!_isAvailable) return;
await _channel.invokeMethod('setAutoPipReady', {'ready': ready, 'width': width, 'height': height});
}
static Future<void> exit() async {
if (!_isAvailable) return;
await _channel.invokeMethod('exit');
}
static Future<(bool success, String? error)> enter({int? width, int? height}) async {
if (!_isAvailable) return (false, null);
final result = await _channel.invokeMethod<Map>('enter', {'width': width, 'height': height});
if (result == null) {
return (false, t.videoControls.pipErrors.unknown(error: t.common.unknown));
}
final success = result['success'] as bool? ?? false;
final errorCode = result['errorCode'] as String?;
final errorMessage = result['errorMessage'] as String?;
final error = errorCode != null ? _getLocalizedError(errorCode, errorMessage) : null;
return (success, error);
}
static String _getLocalizedError(String errorCode, String? errorMessage) {
return switch (errorCode) {
'android_version' => t.videoControls.pipErrors.androidVersion,
'ios_version' => t.videoControls.pipErrors.iosVersion,
'macos_version' => t.videoControls.pipErrors.notSupported,
'permission_disabled' => t.videoControls.pipErrors.permissionDisabled,
'not_supported' => t.videoControls.pipErrors.notSupported,
'pip_prepare_failed' => t.videoControls.pipErrors.prepareFailed,
'vo_switch_failed' => t.videoControls.pipErrors.voSwitchFailed,
'failed' => t.videoControls.pipErrors.failed,
_ => t.videoControls.pipErrors.unknown(error: errorMessage ?? t.common.unknown),
};
}
}