refactor: consolidate files

This commit is contained in:
edde746
2026-02-26 13:41:51 +01:00
parent e489a97847
commit 4588457319
23 changed files with 123 additions and 314 deletions
+2 -1
View File
@@ -40,7 +40,8 @@ platform :android do
"../build/app/outputs/flutter-apk/app-arm64-v8a-release.apk"
],
overwrite_upload: true,
overwrite_upload_mode: 'reuse'
overwrite_upload_mode: 'reuse',
changes_not_sent_for_review: true
)
end
end
+2 -2
View File
@@ -8,7 +8,7 @@ import 'package:flutter_svg/flutter_svg.dart';
import 'screens/main_screen.dart';
import 'screens/auth_screen.dart';
import 'services/storage_service.dart';
import 'services/macos_titlebar_service.dart';
import 'services/macos_window_service.dart';
import 'services/fullscreen_state_manager.dart';
import 'services/settings_service.dart';
import 'utils/platform_detector.dart';
@@ -100,7 +100,7 @@ void main() async {
}
// Configure macOS window with custom titlebar (depends on window manager)
futures.add(MacOSTitlebarService.setupCustomTitlebar());
futures.add(MacOSWindowService.setupCustomTitlebar());
// Initialize storage service
futures.add(StorageService.getInstance());
@@ -1,4 +1,56 @@
import 'remote_command_type.dart';
enum RemoteCommandType {
// Navigation
dpadUp,
dpadDown,
dpadLeft,
dpadRight,
select,
back,
contextMenu,
// Playback
play,
pause,
playPause,
stop,
seekForward,
seekBackward,
nextTrack,
previousTrack,
skipIntro,
skipCredits,
// Volume
volumeUp,
volumeDown,
volumeMute,
volumeSet,
// Tab Navigation
tabNext,
tabPrevious,
tabDiscover,
tabLibraries,
tabSearch,
tabDownloads,
tabSettings,
// Quick Actions
home,
search,
subtitles,
audioTracks,
qualitySettings,
fullscreen,
// Session Management
ping,
pong,
deviceInfo,
disconnect,
ack,
syncState,
}
class RemoteCommand {
final RemoteCommandType type;
@@ -1,53 +0,0 @@
enum RemoteCommandType {
// Navigation
dpadUp,
dpadDown,
dpadLeft,
dpadRight,
select,
back,
contextMenu,
// Playback
play,
pause,
playPause,
stop,
seekForward,
seekBackward,
nextTrack,
previousTrack,
skipIntro,
skipCredits,
// Volume
volumeUp,
volumeDown,
volumeMute,
volumeSet,
// Tab Navigation
tabNext,
tabPrevious,
tabDiscover,
tabLibraries,
tabSearch,
tabDownloads,
tabSettings,
// Quick Actions
home,
search,
subtitles,
audioTracks,
qualitySettings,
fullscreen,
// Session Management
ping,
pong,
deviceInfo,
disconnect,
ack,
syncState,
}
@@ -1,54 +0,0 @@
import 'package:json_annotation/json_annotation.dart';
part 'trusted_device.g.dart';
@JsonSerializable()
class TrustedDevice {
final String peerId;
final String deviceName;
final String platform;
final DateTime firstConnected;
final DateTime lastConnected;
final bool isApproved;
TrustedDevice({
required this.peerId,
required this.deviceName,
required this.platform,
DateTime? firstConnected,
DateTime? lastConnected,
this.isApproved = false,
}) : firstConnected = firstConnected ?? DateTime.now(),
lastConnected = lastConnected ?? DateTime.now();
factory TrustedDevice.fromJson(Map<String, dynamic> json) => _$TrustedDeviceFromJson(json);
Map<String, dynamic> toJson() => _$TrustedDeviceToJson(this);
TrustedDevice copyWith({
String? peerId,
String? deviceName,
String? platform,
DateTime? firstConnected,
DateTime? lastConnected,
bool? isApproved,
}) {
return TrustedDevice(
peerId: peerId ?? this.peerId,
deviceName: deviceName ?? this.deviceName,
platform: platform ?? this.platform,
firstConnected: firstConnected ?? this.firstConnected,
lastConnected: lastConnected ?? this.lastConnected,
isApproved: isApproved ?? this.isApproved,
);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is TrustedDevice && other.peerId == peerId;
}
@override
int get hashCode => peerId.hashCode;
}
@@ -1,25 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'trusted_device.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
TrustedDevice _$TrustedDeviceFromJson(Map<String, dynamic> json) => TrustedDevice(
peerId: json['peerId'] as String,
deviceName: json['deviceName'] as String,
platform: json['platform'] as String,
firstConnected: json['firstConnected'] == null ? null : DateTime.parse(json['firstConnected'] as String),
lastConnected: json['lastConnected'] == null ? null : DateTime.parse(json['lastConnected'] as String),
isApproved: json['isApproved'] as bool? ?? false,
);
Map<String, dynamic> _$TrustedDeviceToJson(TrustedDevice instance) => <String, dynamic>{
'peerId': instance.peerId,
'deviceName': instance.deviceName,
'platform': instance.platform,
'firstConnected': instance.firstConnected.toIso8601String(),
'lastConnected': instance.lastConnected.toIso8601String(),
'isApproved': instance.isApproved,
};
+1 -1
View File
@@ -1,7 +1,7 @@
import 'dart:io' show Platform;
import '../models.dart';
import 'player_android.dart';
import 'platform/player_android.dart';
import 'player_native.dart';
import 'player_state.dart';
import 'player_streams.dart';
+1 -100
View File
@@ -1,31 +1,23 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:device_info_plus/device_info_plus.dart';
import '../models/companion_remote/remote_command.dart';
import '../models/companion_remote/remote_command_type.dart';
import '../models/companion_remote/remote_session.dart';
import '../models/companion_remote/trusted_device.dart';
import '../services/companion_remote/companion_remote_peer_service.dart';
import '../services/storage_service.dart';
import '../utils/app_logger.dart';
typedef CommandReceivedCallback = void Function(RemoteCommand command);
typedef DeviceApprovalCallback = Future<bool> Function(RemoteDevice device);
class CompanionRemoteProvider with ChangeNotifier {
RemoteSession? _session;
CompanionRemotePeerService? _peerService;
String _deviceName = 'Unknown Device';
String _platform = 'unknown';
final List<TrustedDevice> _trustedDevices = [];
bool _isPlayerActive = false;
static const String _storageKey = 'companion_remote_trusted_devices';
static const String _lastDeviceKey = 'companion_remote_last_device';
static const int _maxReconnectAttempts = 5;
Timer? _reconnectTimer;
@@ -44,7 +36,6 @@ class CompanionRemoteProvider with ChangeNotifier {
StreamSubscription<RemoteSessionStatus>? _statusSubscription;
CommandReceivedCallback? onCommandReceived;
DeviceApprovalCallback? onDeviceApprovalRequired;
bool get isInSession => _session != null && _session!.status != RemoteSessionStatus.disconnected;
bool get isHost => _session?.isHost ?? false;
@@ -55,12 +46,10 @@ class CompanionRemoteProvider with ChangeNotifier {
String? get sessionId => _session?.sessionId;
String? get pin => _session?.pin;
RemoteDevice? get connectedDevice => _session?.connectedDevice;
List<TrustedDevice> get trustedDevices => List.unmodifiable(_trustedDevices);
bool get isPlayerActive => _isPlayerActive;
CompanionRemoteProvider() {
_initializeDeviceInfo();
_loadTrustedDevices();
}
Future<void> _initializeDeviceInfo() async {
@@ -117,12 +106,10 @@ class CompanionRemoteProvider with ChangeNotifier {
},
);
_deviceConnectedSubscription = _peerService!.onDeviceConnected.listen((device) async {
_deviceConnectedSubscription = _peerService!.onDeviceConnected.listen((device) {
appLogger.d('CompanionRemote: Device connected: ${device.name}');
_session = _session?.copyWith(status: RemoteSessionStatus.connected, connectedDevice: device);
notifyListeners();
await addTrustedDevice(device, requireApproval: isHost);
});
_deviceDisconnectedSubscription = _peerService!.onDeviceDisconnected.listen((_) {
@@ -375,92 +362,6 @@ class CompanionRemoteProvider with ChangeNotifier {
notifyListeners();
}
Future<void> _loadTrustedDevices() async {
try {
final storage = await StorageService.getInstance();
final json = storage.prefs.getString(_storageKey);
if (json != null) {
final List<dynamic> list = jsonDecode(json);
_trustedDevices.clear();
_trustedDevices.addAll(list.map((e) => TrustedDevice.fromJson(e as Map<String, dynamic>)));
appLogger.d('CompanionRemote: Loaded ${_trustedDevices.length} trusted devices');
}
} catch (e) {
appLogger.e('CompanionRemote: Failed to load trusted devices', error: e);
}
}
Future<void> _saveTrustedDevices() async {
try {
final storage = await StorageService.getInstance();
final json = jsonEncode(_trustedDevices.map((e) => e.toJson()).toList());
await storage.prefs.setString(_storageKey, json);
appLogger.d('CompanionRemote: Saved ${_trustedDevices.length} trusted devices');
} catch (e) {
appLogger.e('CompanionRemote: Failed to save trusted devices', error: e);
}
}
bool isDeviceTrusted(String peerId) {
return _trustedDevices.any((d) => d.peerId == peerId && d.isApproved);
}
Future<void> addTrustedDevice(RemoteDevice device, {bool requireApproval = true}) async {
final existing = _trustedDevices.where((d) => d.peerId == device.id).firstOrNull;
if (existing != null) {
final updated = existing.copyWith(
deviceName: device.name,
platform: device.platform,
lastConnected: DateTime.now(),
isApproved: !requireApproval || existing.isApproved,
);
_trustedDevices.remove(existing);
_trustedDevices.add(updated);
} else {
bool approved = !requireApproval;
if (requireApproval && onDeviceApprovalRequired != null) {
approved = await onDeviceApprovalRequired!(device);
}
_trustedDevices.add(
TrustedDevice(peerId: device.id, deviceName: device.name, platform: device.platform, isApproved: approved),
);
}
await _saveTrustedDevices();
if (isRemote) {
final storage = await StorageService.getInstance();
await storage.prefs.setString(_lastDeviceKey, device.id);
}
notifyListeners();
}
Future<void> removeTrustedDevice(String peerId) async {
_trustedDevices.removeWhere((d) => d.peerId == peerId);
await _saveTrustedDevices();
notifyListeners();
}
Future<void> approveTrustedDevice(String peerId) async {
final device = _trustedDevices.where((d) => d.peerId == peerId).firstOrNull;
if (device != null) {
final updated = device.copyWith(isApproved: true);
_trustedDevices.remove(device);
_trustedDevices.add(updated);
await _saveTrustedDevices();
notifyListeners();
}
}
Future<String?> getLastConnectedDevicePeerId() async {
final storage = await StorageService.getInstance();
return storage.prefs.getString(_lastDeviceKey);
}
@override
void dispose() {
_reconnectTimer?.cancel();
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import '../../models/companion_remote/remote_command_type.dart';
import '../../models/companion_remote/remote_command.dart';
import '../../models/companion_remote/remote_session.dart';
import '../../i18n/strings.g.dart';
import '../../providers/companion_remote_provider.dart';
@@ -3,7 +3,7 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../models/plex_first_character.dart';
import '../../models/plex_first_character.dart';
import 'alpha_jump_helper.dart';
/// Vertical strip of letters for jumping through sorted library items.
@@ -1,5 +1,5 @@
import '../data/ducet_order.dart';
import '../models/plex_first_character.dart';
import '../../data/ducet_order.dart';
import '../../models/plex_first_character.dart';
/// Shared letter-index mapping logic used by both [AlphaJumpBar] (desktop/tablet/TV)
/// and [AlphaScrollHandle] (phone).
@@ -2,7 +2,7 @@ import 'dart:async';
import 'package:flutter/material.dart';
import '../models/plex_first_character.dart';
import '../../models/plex_first_character.dart';
import 'alpha_jump_helper.dart';
/// Phone-optimized draggable scroll handle that appears on scroll and shows
@@ -14,9 +14,9 @@ import '../../../providers/settings_provider.dart';
import '../../../utils/error_message_utils.dart';
import '../../../utils/grid_size_calculator.dart';
import '../../../utils/layout_constants.dart';
import '../../../widgets/alpha_jump_bar.dart';
import '../../../widgets/alpha_jump_helper.dart';
import '../../../widgets/alpha_scroll_handle.dart';
import '../alpha_jump_bar.dart';
import '../alpha_jump_helper.dart';
import '../alpha_scroll_handle.dart';
import '../../../widgets/focusable_media_card.dart';
import '../../../widgets/media_card.dart';
import '../../../widgets/focusable_filter_chip.dart';
+1 -1
View File
@@ -22,7 +22,7 @@ import '../../providers/settings_provider.dart';
import '../../providers/theme_provider.dart';
import '../../providers/user_profile_provider.dart';
import '../../services/keyboard_shortcuts_service.dart';
import '../../mpv/player/player_android.dart';
import '../../mpv/player/platform/player_android.dart';
import '../../services/settings_service.dart' as settings;
import '../../services/update_service.dart';
import '../../utils/snackbar_helper.dart';
+2 -2
View File
@@ -11,7 +11,7 @@ import 'package:wakelock_plus/wakelock_plus.dart';
import 'package:window_manager/window_manager.dart';
import '../mpv/mpv.dart';
import '../mpv/player/player_android.dart';
import '../mpv/player/platform/player_android.dart';
import '../../services/plex_client.dart';
import '../models/livetv_channel.dart';
@@ -24,7 +24,7 @@ import '../models/plex_media_info.dart';
import '../providers/download_provider.dart';
import '../providers/multi_server_provider.dart';
import '../providers/playback_state_provider.dart';
import '../models/companion_remote/remote_command_type.dart';
import '../models/companion_remote/remote_command.dart';
import '../providers/companion_remote_provider.dart';
import '../services/companion_remote/companion_remote_receiver.dart';
import '../services/fullscreen_state_manager.dart';
@@ -6,7 +6,6 @@ import 'dart:math';
import 'package:web_socket_channel/io.dart';
import '../../models/companion_remote/remote_command.dart';
import '../../models/companion_remote/remote_command_type.dart';
import '../../models/companion_remote/remote_session.dart';
import '../../utils/app_logger.dart';
@@ -2,7 +2,6 @@ import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import '../../models/companion_remote/remote_command.dart';
import '../../models/companion_remote/remote_command_type.dart';
import '../../utils/app_logger.dart';
import '../../utils/key_event_simulator.dart';
@@ -1,17 +0,0 @@
import 'fullscreen_state_manager.dart';
import 'macos_window_delegate.dart';
/// Custom window delegate that manages fullscreen state
/// Note: Window manipulation (toolbar, titlebar, traffic lights) is now handled
/// directly in Swift's WindowDelegate. This class only updates Dart-side state.
class FullscreenWindowDelegate extends MacOSWindowDelegate {
@override
void windowWillEnterFullScreen() {
FullscreenStateManager().setFullscreen(true);
}
@override
void windowDidExitFullScreen() {
FullscreenStateManager().setFullscreen(false);
}
}
-25
View File
@@ -1,25 +0,0 @@
import 'dart:io' show Platform;
import 'fullscreen_window_delegate.dart';
import 'macos_window_service.dart';
/// Service to manage macOS titlebar configuration
class MacOSTitlebarService {
static bool _initialized = false;
/// Initialize the custom titlebar setup.
///
/// Note: The initial window configuration (transparent titlebar, toolbar,
/// button positions, fullscreen presentation options) is now applied in
/// MainFlutterWindow.swift / WindowDelegate.swift BEFORE frame restoration
/// to prevent the window from shrinking on launch.
///
/// This method only sets up the Dart-side callbacks.
static Future<void> setupCustomTitlebar() async {
if (!Platform.isMacOS || _initialized) return;
_initialized = true;
await MacOSWindowService.initialize(enableWindowDelegate: true);
final delegate = FullscreenWindowDelegate();
MacOSWindowService.addWindowDelegate(delegate);
}
}
-20
View File
@@ -1,20 +0,0 @@
/// Abstract class for receiving macOS window delegate callbacks.
/// Extend this class and register with MacOSWindowService to receive
/// fullscreen transition events.
abstract class MacOSWindowDelegate {
/// Called when the window is about to enter fullscreen mode.
// ignore: no-empty-block - default no-op, subclasses override as needed
void windowWillEnterFullScreen() {}
/// Called when the window has entered fullscreen mode.
// ignore: no-empty-block - default no-op, subclasses override as needed
void windowDidEnterFullScreen() {}
/// Called when the window is about to exit fullscreen mode.
// ignore: no-empty-block - default no-op, subclasses override as needed
void windowWillExitFullScreen() {}
/// Called when the window has exited fullscreen mode.
// ignore: no-empty-block - default no-op, subclasses override as needed
void windowDidExitFullScreen() {}
}
+52 -1
View File
@@ -1,6 +1,27 @@
import 'dart:io' show Platform;
import 'package:flutter/services.dart';
import 'macos_window_delegate.dart';
import 'fullscreen_state_manager.dart';
/// Abstract class for receiving macOS window delegate callbacks.
/// Extend this class and register with [MacOSWindowService] to receive
/// fullscreen transition events.
abstract class MacOSWindowDelegate {
/// Called when the window is about to enter fullscreen mode.
// ignore: no-empty-block - default no-op, subclasses override as needed
void windowWillEnterFullScreen() {}
/// Called when the window has entered fullscreen mode.
// ignore: no-empty-block - default no-op, subclasses override as needed
void windowDidEnterFullScreen() {}
/// Called when the window is about to exit fullscreen mode.
// ignore: no-empty-block - default no-op, subclasses override as needed
void windowWillExitFullScreen() {}
/// Called when the window has exited fullscreen mode.
// ignore: no-empty-block - default no-op, subclasses override as needed
void windowDidExitFullScreen() {}
}
/// Service for manipulating macOS window properties.
/// This is a native implementation replacing the macos_window_utils package.
@@ -45,6 +66,21 @@ class MacOSWindowService {
// MARK: - Initialization
/// Initialize the window service and set up the titlebar.
///
/// Note: The initial window configuration (transparent titlebar, toolbar,
/// button positions, fullscreen presentation options) is now applied in
/// MainFlutterWindow.swift / WindowDelegate.swift BEFORE frame restoration
/// to prevent the window from shrinking on launch.
///
/// This method sets up the Dart-side callbacks for fullscreen state tracking.
static Future<void> setupCustomTitlebar() async {
if (!Platform.isMacOS || _initialized) return;
await initialize(enableWindowDelegate: true);
addWindowDelegate(_FullscreenWindowDelegate());
}
/// Initialize the window service.
/// Must be called before using other methods.
/// Set [enableWindowDelegate] to true to receive fullscreen callbacks.
@@ -94,3 +130,18 @@ class MacOSWindowService {
return await _channel.invokeMethod<bool>('isFullscreen') ?? false;
}
}
/// Internal window delegate that manages fullscreen state.
/// Note: Window manipulation (toolbar, titlebar, traffic lights) is now handled
/// directly in Swift's WindowDelegate. This class only updates Dart-side state.
class _FullscreenWindowDelegate extends MacOSWindowDelegate {
@override
void windowWillEnterFullScreen() {
FullscreenStateManager().setFullscreen(true);
}
@override
void windowDidExitFullScreen() {
FullscreenStateManager().setFullscreen(false);
}
}
@@ -4,7 +4,7 @@ import 'dart:io' show ProcessInfo;
import 'package:flutter/scheduler.dart';
import '../../../../mpv/mpv.dart';
import '../../../../mpv/player/player_android.dart';
import '../../../../mpv/player/platform/player_android.dart';
import '../../../../utils/app_logger.dart';
import 'performance_stats.dart';