refactor: clean up
This commit is contained in:
@@ -7,8 +7,6 @@ import 'package:url_launcher/url_launcher.dart';
|
||||
import 'screens/main_screen.dart';
|
||||
import 'screens/auth_screen.dart';
|
||||
import 'services/storage_service.dart';
|
||||
import 'services/plex_auth_service.dart';
|
||||
import 'services/server_connection_service.dart';
|
||||
import 'services/macos_titlebar_service.dart';
|
||||
import 'services/fullscreen_state_manager.dart';
|
||||
import 'services/update_service.dart';
|
||||
@@ -26,7 +24,6 @@ import 'services/data_aggregation_service.dart';
|
||||
import 'services/server_registry.dart';
|
||||
import 'utils/language_codes.dart';
|
||||
import 'utils/app_logger.dart';
|
||||
import 'utils/provider_extensions.dart';
|
||||
import 'utils/orientation_helper.dart';
|
||||
import 'i18n/strings.g.dart';
|
||||
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'plex_server_info.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
class PlexServerInfo {
|
||||
final String name;
|
||||
final String? host;
|
||||
final int? port;
|
||||
final String? machineIdentifier;
|
||||
final String version;
|
||||
final bool? owned;
|
||||
final bool? https;
|
||||
|
||||
PlexServerInfo({
|
||||
required this.name,
|
||||
this.host,
|
||||
this.port,
|
||||
this.machineIdentifier,
|
||||
required this.version,
|
||||
this.owned,
|
||||
this.https,
|
||||
});
|
||||
|
||||
factory PlexServerInfo.fromJson(Map<String, dynamic> json) =>
|
||||
_$PlexServerInfoFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$PlexServerInfoToJson(this);
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'plex_server_info.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
PlexServerInfo _$PlexServerInfoFromJson(Map<String, dynamic> json) =>
|
||||
PlexServerInfo(
|
||||
name: json['name'] as String,
|
||||
host: json['host'] as String?,
|
||||
port: (json['port'] as num?)?.toInt(),
|
||||
machineIdentifier: json['machineIdentifier'] as String?,
|
||||
version: json['version'] as String,
|
||||
owned: json['owned'] as bool?,
|
||||
https: json['https'] as bool?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$PlexServerInfoToJson(PlexServerInfo instance) =>
|
||||
<String, dynamic>{
|
||||
'name': instance.name,
|
||||
'host': instance.host,
|
||||
'port': instance.port,
|
||||
'machineIdentifier': instance.machineIdentifier,
|
||||
'version': instance.version,
|
||||
'owned': instance.owned,
|
||||
'https': instance.https,
|
||||
};
|
||||
@@ -7,7 +7,6 @@ import 'package:qr_flutter/qr_flutter.dart';
|
||||
import '../services/plex_auth_service.dart';
|
||||
import '../services/storage_service.dart';
|
||||
import '../services/server_registry.dart';
|
||||
import '../services/multi_server_manager.dart';
|
||||
import '../providers/multi_server_provider.dart';
|
||||
import '../providers/plex_client_provider.dart';
|
||||
import '../i18n/strings.g.dart';
|
||||
|
||||
@@ -5,16 +5,12 @@ import 'package:provider/provider.dart';
|
||||
import '../client/plex_client.dart';
|
||||
import '../models/plex_metadata.dart';
|
||||
import '../models/plex_hub.dart';
|
||||
import '../providers/plex_client_provider.dart';
|
||||
import '../providers/multi_server_provider.dart';
|
||||
import '../services/storage_service.dart';
|
||||
import '../services/plex_auth_service.dart';
|
||||
import '../widgets/media_card.dart';
|
||||
import '../widgets/desktop_app_bar.dart';
|
||||
import '../widgets/user_avatar_widget.dart';
|
||||
import '../widgets/horizontal_scroll_with_arrows.dart';
|
||||
import '../widgets/hub_section.dart';
|
||||
import '../widgets/server_badge.dart';
|
||||
import 'profile_switch_screen.dart';
|
||||
import '../providers/user_profile_provider.dart';
|
||||
import '../providers/settings_provider.dart';
|
||||
|
||||
@@ -1111,7 +1111,6 @@ class _LibraryManagementSheet extends StatefulWidget {
|
||||
class _LibraryManagementSheetState extends State<_LibraryManagementSheet> {
|
||||
late List<PlexLibrary> _tempLibraries;
|
||||
List<String>? _serverOrder;
|
||||
bool _isLoadingServerOrder = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -1128,7 +1127,6 @@ class _LibraryManagementSheetState extends State<_LibraryManagementSheet> {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_serverOrder = savedOrder;
|
||||
_isLoadingServerOrder = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1441,43 +1439,6 @@ class _LibraryManagementSheetState extends State<_LibraryManagementSheet> {
|
||||
);
|
||||
}
|
||||
|
||||
/// Reorder libraries within a server group
|
||||
void _reorderLibrariesInServer(
|
||||
String serverKey,
|
||||
List<PlexLibrary> serverLibraries,
|
||||
int oldIndex,
|
||||
int newIndex,
|
||||
) {
|
||||
setState(() {
|
||||
if (newIndex > oldIndex) {
|
||||
newIndex -= 1;
|
||||
}
|
||||
|
||||
// Get the library being moved
|
||||
final library = serverLibraries[oldIndex];
|
||||
|
||||
// Find global indices
|
||||
final globalOldIndex = _tempLibraries.indexOf(library);
|
||||
|
||||
// Calculate the new global index
|
||||
// We need to find where this library should go in the global list
|
||||
final targetLibrary = newIndex < serverLibraries.length
|
||||
? serverLibraries[newIndex]
|
||||
: serverLibraries.last;
|
||||
final globalNewIndex = _tempLibraries.indexOf(targetLibrary);
|
||||
|
||||
// Reorder in global list
|
||||
_tempLibraries.removeAt(globalOldIndex);
|
||||
_tempLibraries.insert(
|
||||
globalOldIndex < globalNewIndex ? globalNewIndex : globalNewIndex,
|
||||
library,
|
||||
);
|
||||
});
|
||||
|
||||
// Apply immediately
|
||||
widget.onReorder(_tempLibraries);
|
||||
}
|
||||
|
||||
/// Build a single library tile
|
||||
Widget _buildLibraryTile(
|
||||
PlexLibrary library,
|
||||
|
||||
@@ -3,9 +3,7 @@ import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../client/plex_client.dart';
|
||||
import '../i18n/strings.g.dart';
|
||||
import '../mixins/item_updatable.dart';
|
||||
import '../mixins/refreshable.dart';
|
||||
import '../models/plex_metadata.dart';
|
||||
import '../providers/multi_server_provider.dart';
|
||||
@@ -13,10 +11,8 @@ import '../providers/settings_provider.dart';
|
||||
import '../services/settings_service.dart';
|
||||
import '../utils/app_logger.dart';
|
||||
import '../utils/grid_cross_axis_extent.dart';
|
||||
import '../utils/provider_extensions.dart';
|
||||
import '../widgets/desktop_app_bar.dart';
|
||||
import '../widgets/media_card.dart';
|
||||
import '../widgets/server_badge.dart';
|
||||
|
||||
class SearchScreen extends StatefulWidget {
|
||||
const SearchScreen({super.key});
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../client/plex_client.dart';
|
||||
import '../models/plex_metadata.dart';
|
||||
import '../utils/provider_extensions.dart';
|
||||
import '../utils/video_player_navigation.dart';
|
||||
import '../utils/duration_formatter.dart';
|
||||
import '../utils/app_logger.dart';
|
||||
import '../widgets/desktop_app_bar.dart';
|
||||
import '../widgets/media_context_menu.dart';
|
||||
import '../mixins/item_updatable.dart';
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:flutter/services.dart';
|
||||
import 'package:media_kit/media_kit.dart';
|
||||
import 'package:hotkey_manager/hotkey_manager.dart';
|
||||
import 'settings_service.dart';
|
||||
import '../utils/player_utils.dart';
|
||||
|
||||
class KeyboardShortcutsService {
|
||||
static KeyboardShortcutsService? _instance;
|
||||
@@ -230,21 +231,6 @@ class KeyboardShortcutsService {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
||||
/// Seeks by the given offset (can be positive or negative) while clamping
|
||||
/// the result between 0 and the video duration
|
||||
void _seekWithClamping(Player player, Duration offset) {
|
||||
final currentPosition = player.state.position;
|
||||
final duration = player.state.duration;
|
||||
final newPosition = currentPosition + offset;
|
||||
|
||||
// Clamp between 0 and video duration
|
||||
final clampedPosition = newPosition.isNegative
|
||||
? Duration.zero
|
||||
: (newPosition > duration ? duration : newPosition);
|
||||
|
||||
player.seek(clampedPosition);
|
||||
}
|
||||
|
||||
void _executeAction(
|
||||
String action,
|
||||
Player player,
|
||||
@@ -270,16 +256,16 @@ class KeyboardShortcutsService {
|
||||
_settingsService.setVolume(newVolume);
|
||||
break;
|
||||
case 'seek_forward':
|
||||
_seekWithClamping(player, Duration(seconds: _seekTimeSmall));
|
||||
seekWithClamping(player, Duration(seconds: _seekTimeSmall));
|
||||
break;
|
||||
case 'seek_backward':
|
||||
_seekWithClamping(player, Duration(seconds: -_seekTimeSmall));
|
||||
seekWithClamping(player, Duration(seconds: -_seekTimeSmall));
|
||||
break;
|
||||
case 'seek_forward_large':
|
||||
_seekWithClamping(player, Duration(seconds: _seekTimeLarge));
|
||||
seekWithClamping(player, Duration(seconds: _seekTimeLarge));
|
||||
break;
|
||||
case 'seek_backward_large':
|
||||
_seekWithClamping(player, Duration(seconds: -_seekTimeLarge));
|
||||
seekWithClamping(player, Duration(seconds: -_seekTimeLarge));
|
||||
break;
|
||||
case 'fullscreen_toggle':
|
||||
onToggleFullscreen?.call();
|
||||
|
||||
@@ -7,9 +7,6 @@ import 'storage_service.dart';
|
||||
/// Centralized server configuration registry
|
||||
/// Manages which servers are available, enabled/disabled, and their configurations
|
||||
class ServerRegistry {
|
||||
static const String _keyServersList = 'servers_list';
|
||||
static const String _keyEnabledServers = 'enabled_servers';
|
||||
|
||||
final StorageService _storage;
|
||||
|
||||
ServerRegistry(this._storage);
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import 'package:media_kit/media_kit.dart';
|
||||
|
||||
/// Seeks by the given offset (can be positive or negative) while clamping
|
||||
/// the result between 0 and the video duration
|
||||
void seekWithClamping(Player player, Duration offset) {
|
||||
final currentPosition = player.state.position;
|
||||
final duration = player.state.duration;
|
||||
final newPosition = currentPosition + offset;
|
||||
|
||||
// Clamp between 0 and video duration
|
||||
final clampedPosition = newPosition.isNegative
|
||||
? Duration.zero
|
||||
: (newPosition > duration ? duration : newPosition);
|
||||
|
||||
player.seek(clampedPosition);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Get the replay icon based on the duration
|
||||
/// Returns numbered icons (replay_5, replay_10, replay_30) when available,
|
||||
/// otherwise returns generic replay icon
|
||||
IconData getReplayIcon(int seconds) {
|
||||
switch (seconds) {
|
||||
case 5:
|
||||
return Icons.replay_5;
|
||||
case 10:
|
||||
return Icons.replay_10;
|
||||
case 30:
|
||||
return Icons.replay_30;
|
||||
default:
|
||||
return Icons.replay; // Generic icon for custom durations
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the forward icon based on the duration
|
||||
/// Returns numbered icons (forward_5, forward_10, forward_30) when available,
|
||||
/// otherwise returns generic forward icon
|
||||
IconData getForwardIcon(int seconds) {
|
||||
switch (seconds) {
|
||||
case 5:
|
||||
return Icons.forward_5;
|
||||
case 10:
|
||||
return Icons.forward_10;
|
||||
case 30:
|
||||
return Icons.forward_30;
|
||||
default:
|
||||
return Icons.forward; // Generic icon for custom durations
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,6 @@ import 'package:provider/provider.dart';
|
||||
import '../client/plex_client.dart';
|
||||
import '../models/plex_metadata.dart';
|
||||
import '../models/plex_playlist.dart';
|
||||
import '../providers/plex_client_provider.dart';
|
||||
import '../providers/multi_server_provider.dart';
|
||||
import '../providers/playback_state_provider.dart';
|
||||
import '../utils/provider_extensions.dart';
|
||||
import '../utils/app_logger.dart';
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import '../client/plex_client.dart';
|
||||
import '../models/plex_metadata.dart';
|
||||
|
||||
@@ -10,7 +10,7 @@ import '../../utils/desktop_window_padding.dart';
|
||||
import '../../utils/duration_formatter.dart';
|
||||
import '../../i18n/strings.g.dart';
|
||||
import '../app_bar_back_button.dart';
|
||||
import 'painters/chapter_marker_painter.dart';
|
||||
import 'widgets/timeline_slider.dart';
|
||||
|
||||
/// Desktop-specific video controls layout with top bar and bottom controls
|
||||
class DesktopVideoControls extends StatelessWidget {
|
||||
@@ -188,9 +188,13 @@ class DesktopVideoControls extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _buildTimelineWithChapters(
|
||||
child: TimelineSlider(
|
||||
position: position,
|
||||
duration: duration,
|
||||
chapters: chapters,
|
||||
chaptersLoaded: chaptersLoaded,
|
||||
onSeek: onSeek,
|
||||
onSeekEnd: onSeekEnd,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
@@ -313,88 +317,4 @@ class DesktopVideoControls extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTimelineWithChapters({
|
||||
required Duration position,
|
||||
required Duration duration,
|
||||
}) {
|
||||
return Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
// Chapter markers layer
|
||||
if (chaptersLoaded &&
|
||||
chapters.isNotEmpty &&
|
||||
duration.inMilliseconds > 0)
|
||||
Positioned.fill(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: Row(
|
||||
children:
|
||||
chapters.map((chapter) {
|
||||
final chapterPosition =
|
||||
(chapter.startTimeOffset ?? 0) /
|
||||
duration.inMilliseconds;
|
||||
return Expanded(
|
||||
flex: (chapterPosition * 1000).toInt(),
|
||||
child: const SizedBox(),
|
||||
);
|
||||
}).toList()..add(
|
||||
Expanded(
|
||||
flex:
|
||||
1000 -
|
||||
chapters.fold<int>(
|
||||
0,
|
||||
(sum, chapter) =>
|
||||
sum +
|
||||
((chapter.startTimeOffset ?? 0) /
|
||||
duration.inMilliseconds *
|
||||
1000)
|
||||
.toInt(),
|
||||
),
|
||||
child: const SizedBox(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Slider
|
||||
Semantics(
|
||||
label: t.videoControls.timelineSlider,
|
||||
slider: true,
|
||||
child: Slider(
|
||||
value: duration.inMilliseconds > 0
|
||||
? position.inMilliseconds.toDouble()
|
||||
: 0.0,
|
||||
min: 0.0,
|
||||
max: duration.inMilliseconds.toDouble(),
|
||||
onChanged: (value) {
|
||||
onSeek(Duration(milliseconds: value.toInt()));
|
||||
},
|
||||
onChangeEnd: (value) {
|
||||
onSeekEnd(Duration(milliseconds: value.toInt()));
|
||||
},
|
||||
activeColor: Colors.white,
|
||||
inactiveColor: Colors.white.withValues(alpha: 0.3),
|
||||
),
|
||||
),
|
||||
// Chapter marker indicators
|
||||
if (chaptersLoaded &&
|
||||
chapters.isNotEmpty &&
|
||||
duration.inMilliseconds > 0)
|
||||
Positioned.fill(
|
||||
child: IgnorePointer(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: CustomPaint(
|
||||
painter: ChapterMarkerPainter(
|
||||
chapters: chapters,
|
||||
duration: duration,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,11 @@ import 'package:media_kit/media_kit.dart';
|
||||
import '../../models/plex_media_info.dart';
|
||||
import '../../models/plex_metadata.dart';
|
||||
import '../../utils/duration_formatter.dart';
|
||||
import '../../utils/player_utils.dart';
|
||||
import '../../utils/video_control_icons.dart';
|
||||
import '../../i18n/strings.g.dart';
|
||||
import '../app_bar_back_button.dart';
|
||||
import 'painters/chapter_marker_painter.dart';
|
||||
import 'widgets/timeline_slider.dart';
|
||||
|
||||
/// Mobile video controls layout for Plex video player
|
||||
///
|
||||
@@ -142,13 +144,13 @@ class MobileVideoControls extends StatelessWidget {
|
||||
excludeSemantics: true,
|
||||
child: IconButton(
|
||||
icon: Icon(
|
||||
_getReplayIcon(seekTimeSmall),
|
||||
getReplayIcon(seekTimeSmall),
|
||||
color: Colors.white,
|
||||
size: 48,
|
||||
),
|
||||
iconSize: 48,
|
||||
onPressed: () {
|
||||
_seekWithClamping(Duration(seconds: -seekTimeSmall));
|
||||
seekWithClamping(player, Duration(seconds: -seekTimeSmall));
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -198,13 +200,13 @@ class MobileVideoControls extends StatelessWidget {
|
||||
excludeSemantics: true,
|
||||
child: IconButton(
|
||||
icon: Icon(
|
||||
_getForwardIcon(seekTimeSmall),
|
||||
getForwardIcon(seekTimeSmall),
|
||||
color: Colors.white,
|
||||
size: 48,
|
||||
),
|
||||
iconSize: 48,
|
||||
onPressed: () {
|
||||
_seekWithClamping(Duration(seconds: seekTimeSmall));
|
||||
seekWithClamping(player, Duration(seconds: seekTimeSmall));
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -234,9 +236,13 @@ class MobileVideoControls extends StatelessWidget {
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
_buildTimelineWithChapters(
|
||||
TimelineSlider(
|
||||
position: position,
|
||||
duration: duration,
|
||||
chapters: chapters,
|
||||
chaptersLoaded: chaptersLoaded,
|
||||
onSeek: onSeek,
|
||||
onSeekEnd: onSeekEnd,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
@@ -270,90 +276,6 @@ class MobileVideoControls extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTimelineWithChapters({
|
||||
required Duration position,
|
||||
required Duration duration,
|
||||
}) {
|
||||
return Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
// Chapter markers layer
|
||||
if (chaptersLoaded &&
|
||||
chapters.isNotEmpty &&
|
||||
duration.inMilliseconds > 0)
|
||||
Positioned.fill(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: Row(
|
||||
children:
|
||||
chapters.map((chapter) {
|
||||
final chapterPosition =
|
||||
(chapter.startTimeOffset ?? 0) /
|
||||
duration.inMilliseconds;
|
||||
return Expanded(
|
||||
flex: (chapterPosition * 1000).toInt(),
|
||||
child: const SizedBox(),
|
||||
);
|
||||
}).toList()..add(
|
||||
Expanded(
|
||||
flex:
|
||||
1000 -
|
||||
chapters.fold<int>(
|
||||
0,
|
||||
(sum, chapter) =>
|
||||
sum +
|
||||
((chapter.startTimeOffset ?? 0) /
|
||||
duration.inMilliseconds *
|
||||
1000)
|
||||
.toInt(),
|
||||
),
|
||||
child: const SizedBox(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Slider
|
||||
Semantics(
|
||||
label: t.videoControls.timelineSlider,
|
||||
slider: true,
|
||||
child: Slider(
|
||||
value: duration.inMilliseconds > 0
|
||||
? position.inMilliseconds.toDouble()
|
||||
: 0.0,
|
||||
min: 0.0,
|
||||
max: duration.inMilliseconds.toDouble(),
|
||||
onChanged: (value) {
|
||||
onSeek(Duration(milliseconds: value.toInt()));
|
||||
},
|
||||
onChangeEnd: (value) {
|
||||
onSeekEnd(Duration(milliseconds: value.toInt()));
|
||||
},
|
||||
activeColor: Colors.white,
|
||||
inactiveColor: Colors.white.withValues(alpha: 0.3),
|
||||
),
|
||||
),
|
||||
// Chapter marker indicators
|
||||
if (chaptersLoaded &&
|
||||
chapters.isNotEmpty &&
|
||||
duration.inMilliseconds > 0)
|
||||
Positioned.fill(
|
||||
child: IgnorePointer(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: CustomPaint(
|
||||
painter: ChapterMarkerPainter(
|
||||
chapters: chapters,
|
||||
duration: duration,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// Conditionally wraps child with SafeArea only in portrait mode
|
||||
Widget _conditionalSafeArea({
|
||||
required BuildContext context,
|
||||
@@ -372,49 +294,4 @@ class MobileVideoControls extends StatelessWidget {
|
||||
// In landscape, return child without SafeArea
|
||||
return child;
|
||||
}
|
||||
|
||||
void _seekWithClamping(Duration offset) {
|
||||
final currentPosition = player.state.position;
|
||||
final duration = player.state.duration;
|
||||
final newPosition = currentPosition + offset;
|
||||
|
||||
// Clamp between 0 and video duration
|
||||
final clampedPosition = newPosition.isNegative
|
||||
? Duration.zero
|
||||
: (newPosition > duration ? duration : newPosition);
|
||||
|
||||
player.seek(clampedPosition);
|
||||
}
|
||||
|
||||
/// Get the replay icon based on the duration
|
||||
/// Returns numbered icons (replay_5, replay_10, replay_30) when available,
|
||||
/// otherwise returns generic replay icon
|
||||
IconData _getReplayIcon(int seconds) {
|
||||
switch (seconds) {
|
||||
case 5:
|
||||
return Icons.replay_5;
|
||||
case 10:
|
||||
return Icons.replay_10;
|
||||
case 30:
|
||||
return Icons.replay_30;
|
||||
default:
|
||||
return Icons.replay; // Generic icon for custom durations
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the forward icon based on the duration
|
||||
/// Returns numbered icons (forward_5, forward_10, forward_30) when available,
|
||||
/// otherwise returns generic forward icon
|
||||
IconData _getForwardIcon(int seconds) {
|
||||
switch (seconds) {
|
||||
case 5:
|
||||
return Icons.forward_5;
|
||||
case 10:
|
||||
return Icons.forward_10;
|
||||
case 30:
|
||||
return Icons.forward_30;
|
||||
default:
|
||||
return Icons.forward; // Generic icon for custom durations
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,9 @@ import '../../screens/video_player_screen.dart';
|
||||
import '../../services/keyboard_shortcuts_service.dart';
|
||||
import '../../services/settings_service.dart';
|
||||
import '../../utils/platform_detector.dart';
|
||||
import '../../utils/player_utils.dart';
|
||||
import '../../utils/provider_extensions.dart';
|
||||
import '../../utils/video_control_icons.dart';
|
||||
import '../../i18n/strings.g.dart';
|
||||
import 'widgets/volume_control.dart';
|
||||
import 'widgets/track_chapter_controls.dart';
|
||||
@@ -399,7 +401,7 @@ class _PlexVideoControlsState extends State<PlexVideoControls>
|
||||
void _seekToPreviousChapter() {
|
||||
if (_chapters.isEmpty) {
|
||||
// No chapters - seek backward by configured amount
|
||||
_seekWithClamping(Duration(seconds: -_seekTimeSmall));
|
||||
seekWithClamping(widget.player, Duration(seconds: -_seekTimeSmall));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -422,7 +424,7 @@ class _PlexVideoControlsState extends State<PlexVideoControls>
|
||||
void _seekToNextChapter() {
|
||||
if (_chapters.isEmpty) {
|
||||
// No chapters - seek forward by configured amount
|
||||
_seekWithClamping(Duration(seconds: _seekTimeSmall));
|
||||
seekWithClamping(widget.player, Duration(seconds: _seekTimeSmall));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -438,21 +440,6 @@ class _PlexVideoControlsState extends State<PlexVideoControls>
|
||||
}
|
||||
}
|
||||
|
||||
/// Seeks by the given offset (can be positive or negative) while clamping
|
||||
/// the result between 0 and the video duration
|
||||
void _seekWithClamping(Duration offset) {
|
||||
final currentPosition = widget.player.state.position;
|
||||
final duration = widget.player.state.duration;
|
||||
final newPosition = currentPosition + offset;
|
||||
|
||||
// Clamp between 0 and video duration
|
||||
final clampedPosition = newPosition.isNegative
|
||||
? Duration.zero
|
||||
: (newPosition > duration ? duration : newPosition);
|
||||
|
||||
widget.player.seek(clampedPosition);
|
||||
}
|
||||
|
||||
/// Throttled seek for timeline slider - only sends seek events at most every 100ms
|
||||
void _throttledSeek(Duration position) {
|
||||
// Store the pending position
|
||||
@@ -487,42 +474,11 @@ class _PlexVideoControlsState extends State<PlexVideoControls>
|
||||
_pendingSeekPosition = null;
|
||||
}
|
||||
|
||||
/// Get the replay icon based on the duration
|
||||
/// Returns numbered icons (replay_5, replay_10, replay_30) when available,
|
||||
/// otherwise returns generic replay icon
|
||||
IconData _getReplayIcon(int seconds) {
|
||||
switch (seconds) {
|
||||
case 5:
|
||||
return Icons.replay_5;
|
||||
case 10:
|
||||
return Icons.replay_10;
|
||||
case 30:
|
||||
return Icons.replay_30;
|
||||
default:
|
||||
return Icons.replay; // Generic icon for custom durations
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the forward icon based on the duration
|
||||
/// Returns numbered icons (forward_5, forward_10, forward_30) when available,
|
||||
/// otherwise returns generic forward icon
|
||||
IconData _getForwardIcon(int seconds) {
|
||||
switch (seconds) {
|
||||
case 5:
|
||||
return Icons.forward_5;
|
||||
case 10:
|
||||
return Icons.forward_10;
|
||||
case 30:
|
||||
return Icons.forward_30;
|
||||
default:
|
||||
return Icons.forward; // Generic icon for custom durations
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle double-tap skip forward or backward
|
||||
void _handleDoubleTapSkip({required bool isForward}) {
|
||||
// Perform the seek
|
||||
_seekWithClamping(
|
||||
seekWithClamping(
|
||||
widget.player,
|
||||
Duration(seconds: isForward ? _seekTimeSmall : -_seekTimeSmall),
|
||||
);
|
||||
|
||||
@@ -573,8 +529,8 @@ class _PlexVideoControlsState extends State<PlexVideoControls>
|
||||
),
|
||||
child: Icon(
|
||||
_lastDoubleTapWasForward
|
||||
? _getForwardIcon(_seekTimeSmall)
|
||||
: _getReplayIcon(_seekTimeSmall),
|
||||
? getForwardIcon(_seekTimeSmall)
|
||||
: getReplayIcon(_seekTimeSmall),
|
||||
color: Colors.white,
|
||||
size: 48,
|
||||
),
|
||||
@@ -713,8 +669,8 @@ class _PlexVideoControlsState extends State<PlexVideoControls>
|
||||
onSeekToNextChapter: _seekToNextChapter,
|
||||
onSeek: _throttledSeek,
|
||||
onSeekEnd: _finalizeSeek,
|
||||
getReplayIcon: _getReplayIcon,
|
||||
getForwardIcon: _getForwardIcon,
|
||||
getReplayIcon: getReplayIcon,
|
||||
getForwardIcon: getForwardIcon,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user