fix: add const constructors and remove unused imports
This commit is contained in:
+14
-27
@@ -137,7 +137,7 @@ void main() async {
|
||||
|
||||
void _registerShaderLicenses() {
|
||||
LicenseRegistry.addLicense(() async* {
|
||||
yield LicenseEntryWithLineBreaks(
|
||||
yield const LicenseEntryWithLineBreaks(
|
||||
['Anime4K'],
|
||||
'MIT License\n'
|
||||
'\n'
|
||||
@@ -162,7 +162,7 @@ void _registerShaderLicenses() {
|
||||
'OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE '
|
||||
'SOFTWARE.',
|
||||
);
|
||||
yield LicenseEntryWithLineBreaks(
|
||||
yield const LicenseEntryWithLineBreaks(
|
||||
['NVIDIA Image Scaling (NVScaler)'],
|
||||
'The MIT License (MIT)\n'
|
||||
'\n'
|
||||
@@ -390,9 +390,10 @@ class _SetupScreenState extends State<SetupScreen> {
|
||||
|
||||
// Check network connectivity early to fast-path airplane mode.
|
||||
// Timeout guards against connectivity_plus hanging on some Android TV devices after force-close.
|
||||
final connectivityResult = await Connectivity()
|
||||
.checkConnectivity()
|
||||
.timeout(const Duration(seconds: 3), onTimeout: () => [ConnectivityResult.other]);
|
||||
final connectivityResult = await Connectivity().checkConnectivity().timeout(
|
||||
const Duration(seconds: 3),
|
||||
onTimeout: () => [ConnectivityResult.other],
|
||||
);
|
||||
final hasNetwork = !connectivityResult.contains(ConnectivityResult.none);
|
||||
|
||||
if (hasNetwork) {
|
||||
@@ -430,10 +431,7 @@ class _SetupScreenState extends State<SetupScreen> {
|
||||
_setStatus(t.common.startingOfflineMode);
|
||||
await context.read<DownloadProvider>().ensureInitialized();
|
||||
if (!mounted) return;
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
fadeRoute(const MainScreen(isOfflineMode: true)),
|
||||
);
|
||||
Navigator.pushReplacement(context, fadeRoute(const MainScreen(isOfflineMode: true)));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -457,18 +455,12 @@ class _SetupScreenState extends State<SetupScreen> {
|
||||
downloadProvider.resumeQueuedDownloads(result.firstClient!);
|
||||
});
|
||||
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
fadeRoute(MainScreen(client: result.firstClient!)),
|
||||
);
|
||||
Navigator.pushReplacement(context, fadeRoute(MainScreen(client: result.firstClient!)));
|
||||
} else {
|
||||
_setStatus(t.common.startingOfflineMode);
|
||||
await context.read<DownloadProvider>().ensureInitialized();
|
||||
if (!mounted) return;
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
fadeRoute(const MainScreen(isOfflineMode: true)),
|
||||
);
|
||||
Navigator.pushReplacement(context, fadeRoute(const MainScreen(isOfflineMode: true)));
|
||||
}
|
||||
} catch (e, stackTrace) {
|
||||
appLogger.e('Error during multi-server connection', error: e, stackTrace: stackTrace);
|
||||
@@ -477,10 +469,7 @@ class _SetupScreenState extends State<SetupScreen> {
|
||||
_setStatus(t.common.startingOfflineMode);
|
||||
await context.read<DownloadProvider>().ensureInitialized();
|
||||
if (!mounted) return;
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
fadeRoute(const MainScreen(isOfflineMode: true)),
|
||||
);
|
||||
Navigator.pushReplacement(context, fadeRoute(const MainScreen(isOfflineMode: true)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -493,9 +482,7 @@ class _SetupScreenState extends State<SetupScreen> {
|
||||
children: [
|
||||
// Icon dead-center, matching Android 12+ splash position.
|
||||
// 192dp accounts for the 16% inset in ic_launcher.xml.
|
||||
Center(
|
||||
child: SvgPicture.asset('assets/plezy_adaptive_foreground.svg', width: 288, height: 288),
|
||||
),
|
||||
Center(child: SvgPicture.asset('assets/plezy_adaptive_foreground.svg', width: 288, height: 288)),
|
||||
// Status text below center, independent of icon position.
|
||||
Positioned(
|
||||
left: 0,
|
||||
@@ -507,9 +494,9 @@ class _SetupScreenState extends State<SetupScreen> {
|
||||
_statusMessage,
|
||||
key: ValueKey(_statusMessage),
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodyMedium?.copyWith(color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6)),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -113,10 +113,7 @@ class _AuthScreenState extends State<AuthScreen> {
|
||||
await profileFuture;
|
||||
|
||||
if (!mounted) return;
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
fadeRoute(MainScreen(client: result.firstClient!)),
|
||||
);
|
||||
Navigator.pushReplacement(context, fadeRoute(MainScreen(client: result.firstClient!)));
|
||||
} catch (e) {
|
||||
appLogger.e('Failed to connect to servers', error: e);
|
||||
setState(() {
|
||||
@@ -455,7 +452,7 @@ class _AuthScreenState extends State<AuthScreen> {
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
side: BorderSide(color: Theme.of(context).colorScheme.outline.withValues(alpha: 0.5)),
|
||||
),
|
||||
child: Text(t.auth.debugEnterToken, style: TextStyle(fontSize: 12)),
|
||||
child: Text(t.auth.debugEnterToken, style: const TextStyle(fontSize: 12)),
|
||||
),
|
||||
],
|
||||
if (_errorMessage != null) ...[
|
||||
|
||||
@@ -201,7 +201,7 @@ class _RemoteControlContentState extends State<_RemoteControlContent> {
|
||||
Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(color: Colors.green, shape: BoxShape.circle),
|
||||
decoration: const BoxDecoration(color: Colors.green, shape: BoxShape.circle),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -648,7 +648,7 @@ class _SearchBottomSheetState extends State<_SearchBottomSheet> {
|
||||
hintText: t.companionRemote.remote.searchHint,
|
||||
prefixIcon: const Icon(Icons.search),
|
||||
suffixIcon: IconButton(icon: const Icon(Icons.send), onPressed: () => _submit(_controller.text)),
|
||||
border: OutlineInputBorder(borderRadius: const BorderRadius.all(Radius.circular(100))),
|
||||
border: const OutlineInputBorder(borderRadius: BorderRadius.all(Radius.circular(100))),
|
||||
),
|
||||
onSubmitted: _submit,
|
||||
),
|
||||
|
||||
@@ -258,7 +258,6 @@ class _DiscoverScreenState extends State<DiscoverScreen>
|
||||
_loadContent();
|
||||
}
|
||||
|
||||
|
||||
/// Handle key events for the hero section
|
||||
late final _handleHeroKeyEvent = dpadKeyHandler(
|
||||
onDown: () {
|
||||
@@ -285,7 +284,6 @@ class _DiscoverScreenState extends State<DiscoverScreen>
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_hiddenLibrariesProvider?.removeListener(_onHiddenLibrariesChanged);
|
||||
@@ -781,12 +779,18 @@ class _DiscoverScreenState extends State<DiscoverScreen>
|
||||
PopupMenuItem(
|
||||
value: 'switch_profile',
|
||||
child: Row(
|
||||
children: [AppIcon(Symbols.people_rounded, fill: 1), SizedBox(width: 8), Text(t.discover.switchProfile)],
|
||||
children: [
|
||||
AppIcon(Symbols.people_rounded, fill: 1),
|
||||
const SizedBox(width: 8),
|
||||
Text(t.discover.switchProfile),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: 'logout',
|
||||
child: Row(children: [AppIcon(Symbols.logout_rounded, fill: 1), SizedBox(width: 8), Text(t.common.logout)]),
|
||||
child: Row(
|
||||
children: [AppIcon(Symbols.logout_rounded, fill: 1), const SizedBox(width: 8), Text(t.common.logout)],
|
||||
),
|
||||
),
|
||||
],
|
||||
).then((value) {
|
||||
@@ -838,14 +842,11 @@ class _DiscoverScreenState extends State<DiscoverScreen>
|
||||
onNavigateLeft: _navigateToSidebar,
|
||||
onNavigateDown: _focusContentFromAppBar,
|
||||
actions: [
|
||||
FocusableAction(
|
||||
icon: Symbols.refresh_rounded,
|
||||
iconColor: Colors.white,
|
||||
onPressed: _loadContent,
|
||||
),
|
||||
FocusableAction(icon: Symbols.refresh_rounded, iconColor: Colors.white, onPressed: _loadContent),
|
||||
// Watch Together
|
||||
FocusableAction(
|
||||
onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const WatchTogetherScreen())),
|
||||
onPressed: () =>
|
||||
Navigator.push(context, MaterialPageRoute(builder: (_) => const WatchTogetherScreen())),
|
||||
child: Stack(
|
||||
children: [
|
||||
IconButton(
|
||||
@@ -854,7 +855,10 @@ class _DiscoverScreenState extends State<DiscoverScreen>
|
||||
fill: watchTogether.isInSession ? 1 : 0,
|
||||
color: watchTogether.isInSession ? Theme.of(context).colorScheme.primary : Colors.white,
|
||||
),
|
||||
onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const WatchTogetherScreen())),
|
||||
onPressed: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => const WatchTogetherScreen()),
|
||||
),
|
||||
tooltip: 'Watch Together',
|
||||
),
|
||||
if (watchTogether.isInSession && watchTogether.participantCount > 1)
|
||||
@@ -886,7 +890,10 @@ class _DiscoverScreenState extends State<DiscoverScreen>
|
||||
if (isDesktop) {
|
||||
RemoteSessionDialog.show(context);
|
||||
} else {
|
||||
Navigator.push(context, MaterialPageRoute(builder: (context) => MobileRemoteScreen()));
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => const MobileRemoteScreen()),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Stack(
|
||||
@@ -895,13 +902,18 @@ class _DiscoverScreenState extends State<DiscoverScreen>
|
||||
icon: AppIcon(
|
||||
Symbols.phone_android_rounded,
|
||||
fill: companionRemote.isConnected ? 1 : 0,
|
||||
color: companionRemote.isConnected ? Theme.of(context).colorScheme.primary : Colors.white,
|
||||
color: companionRemote.isConnected
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Colors.white,
|
||||
),
|
||||
onPressed: () {
|
||||
if (isDesktop) {
|
||||
RemoteSessionDialog.show(context);
|
||||
} else {
|
||||
Navigator.push(context, MaterialPageRoute(builder: (context) => MobileRemoteScreen()));
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => const MobileRemoteScreen()),
|
||||
);
|
||||
}
|
||||
},
|
||||
tooltip: t.companionRemote.title,
|
||||
@@ -913,10 +925,10 @@ class _DiscoverScreenState extends State<DiscoverScreen>
|
||||
child: Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.green,
|
||||
shape: BoxShape.circle,
|
||||
border: const Border.fromBorderSide(BorderSide(color: Colors.white, width: 1)),
|
||||
border: Border.fromBorderSide(BorderSide(color: Colors.white, width: 1)),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -944,7 +956,7 @@ class _DiscoverScreenState extends State<DiscoverScreen>
|
||||
child: Row(
|
||||
children: [
|
||||
AppIcon(Symbols.people_rounded, fill: 1),
|
||||
SizedBox(width: 8),
|
||||
const SizedBox(width: 8),
|
||||
Text(t.discover.switchProfile),
|
||||
],
|
||||
),
|
||||
@@ -954,7 +966,7 @@ class _DiscoverScreenState extends State<DiscoverScreen>
|
||||
child: Row(
|
||||
children: [
|
||||
AppIcon(Symbols.logout_rounded, fill: 1),
|
||||
SizedBox(width: 8),
|
||||
const SizedBox(width: 8),
|
||||
Text(t.common.logout),
|
||||
],
|
||||
),
|
||||
@@ -1097,11 +1109,11 @@ class _DiscoverScreenState extends State<DiscoverScreen>
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
AppIcon(Symbols.movie_rounded, fill: 1, size: 64, color: Colors.grey),
|
||||
SizedBox(height: 16),
|
||||
const AppIcon(Symbols.movie_rounded, fill: 1, size: 64, color: Colors.grey),
|
||||
const SizedBox(height: 16),
|
||||
Text(t.discover.noContentAvailable),
|
||||
SizedBox(height: 8),
|
||||
Text(t.discover.addMediaToLibraries, style: TextStyle(color: Colors.grey)),
|
||||
const SizedBox(height: 8),
|
||||
Text(t.discover.addMediaToLibraries, style: const TextStyle(color: Colors.grey)),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -1575,7 +1587,7 @@ class _DiscoverScreenState extends State<DiscoverScreen>
|
||||
] else
|
||||
Text(
|
||||
t.common.play,
|
||||
style: TextStyle(color: Colors.black, fontSize: 14, fontWeight: FontWeight.w600),
|
||||
style: const TextStyle(color: Colors.black, fontSize: 14, fontWeight: FontWeight.w600),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../focus/focusable_action_bar.dart';
|
||||
import '../focus/input_mode_tracker.dart';
|
||||
import '../focus/key_event_utils.dart';
|
||||
import '../mixins/grid_focus_node_mixin.dart';
|
||||
import '../providers/settings_provider.dart';
|
||||
import '../utils/grid_size_calculator.dart';
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:plezy/widgets/app_icon.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../../services/plex_client.dart';
|
||||
@@ -15,11 +14,8 @@ import '../widgets/focusable_media_card.dart';
|
||||
import '../widgets/media_grid_delegate.dart';
|
||||
import '../widgets/desktop_app_bar.dart';
|
||||
import '../widgets/overlay_sheet.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import '../focus/dpad_navigator.dart';
|
||||
import '../focus/focusable_action_bar.dart';
|
||||
import '../focus/input_mode_tracker.dart';
|
||||
import '../focus/key_event_utils.dart';
|
||||
import '../mixins/grid_focus_node_mixin.dart';
|
||||
import 'libraries/sort_bottom_sheet.dart';
|
||||
import 'libraries/state_messages.dart';
|
||||
|
||||
@@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../focus/dpad_navigator.dart';
|
||||
import '../../focus/focusable_action_bar.dart';
|
||||
import '../../i18n/strings.g.dart';
|
||||
import '../../models/livetv_channel.dart';
|
||||
|
||||
@@ -533,7 +533,7 @@ class GuideTabState extends State<GuideTab> {
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
SizedBox(width: _channelColumnWidth, height: _timeHeaderHeight),
|
||||
const SizedBox(width: _channelColumnWidth, height: _timeHeaderHeight),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
controller: _headerHorizontalController,
|
||||
|
||||
@@ -365,7 +365,7 @@ class _TvPinInputState extends State<_TvPinInput> {
|
||||
_digits[index] = digit;
|
||||
_activeIndex = index;
|
||||
_mobileControllers[index].text = digit.toString();
|
||||
_mobileControllers[index].selection = TextSelection.collapsed(offset: 1);
|
||||
_mobileControllers[index].selection = const TextSelection.collapsed(offset: 1);
|
||||
});
|
||||
|
||||
if (index < 3) {
|
||||
@@ -436,12 +436,12 @@ class _TvPinInputState extends State<_TvPinInput> {
|
||||
maxLength: 2, // allow overwrite
|
||||
obscureText: true,
|
||||
style: Theme.of(context).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold),
|
||||
decoration: InputDecoration(
|
||||
decoration: const InputDecoration(
|
||||
counterText: '',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(FocusTheme.defaultBorderRadius)),
|
||||
borderRadius: BorderRadius.all(Radius.circular(FocusTheme.defaultBorderRadius)),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 14),
|
||||
contentPadding: EdgeInsets.symmetric(vertical: 14),
|
||||
),
|
||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||
onChanged: (value) => _onMobileDigitChanged(i, value),
|
||||
|
||||
@@ -235,16 +235,16 @@ class _SearchScreenState extends State<SearchScreen> with Refreshable, FullRefre
|
||||
: null,
|
||||
filled: true,
|
||||
fillColor: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(100)),
|
||||
border: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(100)),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(100)),
|
||||
enabledBorder: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(100)),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(100)),
|
||||
focusedBorder: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(100)),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
|
||||
@@ -59,7 +59,8 @@ class _SeasonDetailScreenState extends State<SeasonDetailScreen>
|
||||
bool _suppressNextBackKeyUp = false;
|
||||
bool _routeSubscribed = false;
|
||||
|
||||
String _toGlobalKey(String ratingKey, {String? serverId}) => buildGlobalKey(serverId ?? widget.season.serverId ?? '', ratingKey);
|
||||
String _toGlobalKey(String ratingKey, {String? serverId}) =>
|
||||
buildGlobalKey(serverId ?? widget.season.serverId ?? '', ratingKey);
|
||||
|
||||
// WatchStateAware: watch all episode ratingKeys
|
||||
@override
|
||||
@@ -364,14 +365,18 @@ class _EpisodeCardState extends State<_EpisodeCard> {
|
||||
);
|
||||
return Row(
|
||||
children: [
|
||||
if (widget.episode.duration != null) Text(formatDurationTimestamp(Duration(milliseconds: widget.episode.duration!)), style: mutedStyle),
|
||||
if (widget.episode.duration != null)
|
||||
Text(formatDurationTimestamp(Duration(milliseconds: widget.episode.duration!)), style: mutedStyle),
|
||||
if (widget.episode.originallyAvailableAt != null) ...[
|
||||
dot,
|
||||
Text(formatFullDate(widget.episode.originallyAvailableAt!), style: mutedStyle),
|
||||
],
|
||||
if (widget.episode.userRating != null && widget.episode.userRating! > 0) ...[
|
||||
dot,
|
||||
Padding(padding: const EdgeInsets.only(top: 2), child: Icon(Symbols.star_rounded, size: 12, fill: 1, color: Colors.amber)),
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(top: 2),
|
||||
child: Icon(Symbols.star_rounded, size: 12, fill: 1, color: Colors.amber),
|
||||
),
|
||||
const SizedBox(width: 2),
|
||||
Text(
|
||||
(widget.episode.userRating! / 2) == (widget.episode.userRating! / 2).truncateToDouble()
|
||||
|
||||
@@ -2,7 +2,6 @@ import 'dart:convert';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:plezy/widgets/app_icon.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
|
||||
@@ -51,10 +51,9 @@ class PlexAuthService {
|
||||
|
||||
static Future<PlexAuthService> create() async {
|
||||
final storage = await StorageService.getInstance();
|
||||
final dio = Dio(BaseOptions(
|
||||
connectTimeout: ConnectionTimeouts.plexTvConnect,
|
||||
receiveTimeout: ConnectionTimeouts.plexTvReceive,
|
||||
));
|
||||
final dio = Dio(
|
||||
BaseOptions(connectTimeout: ConnectionTimeouts.plexTvConnect, receiveTimeout: ConnectionTimeouts.plexTvReceive),
|
||||
);
|
||||
|
||||
// Get or create client identifier
|
||||
String? clientIdentifier = storage.getClientIdentifier();
|
||||
@@ -276,7 +275,7 @@ class PlexServer {
|
||||
factory PlexServer.fromJson(Map<String, dynamic> json) {
|
||||
// Validate required fields first
|
||||
if (!_isValidServerJson(json)) {
|
||||
throw FormatException(
|
||||
throw const FormatException(
|
||||
'Invalid server data: missing required fields (name, clientIdentifier, accessToken, or connections)',
|
||||
);
|
||||
}
|
||||
@@ -302,7 +301,7 @@ class PlexServer {
|
||||
|
||||
// If no valid connections were parsed, this server is unusable
|
||||
if (connections.isEmpty) {
|
||||
throw FormatException('Server has no valid connections');
|
||||
throw const FormatException('Server has no valid connections');
|
||||
}
|
||||
|
||||
DateTime? lastSeenAt;
|
||||
@@ -863,7 +862,7 @@ class PlexConnection {
|
||||
factory PlexConnection.fromJson(Map<String, dynamic> json) {
|
||||
// Validate required fields
|
||||
if (!_isValidConnectionJson(json)) {
|
||||
throw FormatException('Invalid connection data: missing required fields (protocol, address, port, or uri)');
|
||||
throw const FormatException('Invalid connection data: missing required fields (protocol, address, port, or uri)');
|
||||
}
|
||||
|
||||
return PlexConnection(
|
||||
|
||||
@@ -399,27 +399,27 @@ class SettingsService extends BaseSharedPreferencesService {
|
||||
// HotKey Objects (New implementation)
|
||||
Map<String, HotKey> getDefaultKeyboardHotkeys() {
|
||||
return {
|
||||
'play_pause': HotKey(key: PhysicalKeyboardKey.space),
|
||||
'volume_up': HotKey(key: PhysicalKeyboardKey.arrowUp),
|
||||
'volume_down': HotKey(key: PhysicalKeyboardKey.arrowDown),
|
||||
'seek_forward': HotKey(key: PhysicalKeyboardKey.arrowRight),
|
||||
'seek_backward': HotKey(key: PhysicalKeyboardKey.arrowLeft),
|
||||
'seek_forward_large': HotKey(key: PhysicalKeyboardKey.arrowRight, modifiers: [HotKeyModifier.shift]),
|
||||
'seek_backward_large': HotKey(key: PhysicalKeyboardKey.arrowLeft, modifiers: [HotKeyModifier.shift]),
|
||||
'fullscreen_toggle': HotKey(key: PhysicalKeyboardKey.keyF),
|
||||
'mute_toggle': HotKey(key: PhysicalKeyboardKey.keyM),
|
||||
'subtitle_toggle': HotKey(key: PhysicalKeyboardKey.keyS),
|
||||
'audio_track_next': HotKey(key: PhysicalKeyboardKey.keyA),
|
||||
'subtitle_track_next': HotKey(key: PhysicalKeyboardKey.keyS, modifiers: [HotKeyModifier.shift]),
|
||||
'chapter_next': HotKey(key: PhysicalKeyboardKey.keyN),
|
||||
'chapter_previous': HotKey(key: PhysicalKeyboardKey.keyP),
|
||||
'speed_increase': HotKey(key: PhysicalKeyboardKey.equal),
|
||||
'speed_decrease': HotKey(key: PhysicalKeyboardKey.minus),
|
||||
'speed_reset': HotKey(key: PhysicalKeyboardKey.keyR),
|
||||
'sub_seek_next': HotKey(key: PhysicalKeyboardKey.arrowRight, modifiers: [HotKeyModifier.control]),
|
||||
'sub_seek_prev': HotKey(key: PhysicalKeyboardKey.arrowLeft, modifiers: [HotKeyModifier.control]),
|
||||
'shader_toggle': HotKey(key: PhysicalKeyboardKey.keyG),
|
||||
'skip_marker': HotKey(key: PhysicalKeyboardKey.enter),
|
||||
'play_pause': const HotKey(key: PhysicalKeyboardKey.space),
|
||||
'volume_up': const HotKey(key: PhysicalKeyboardKey.arrowUp),
|
||||
'volume_down': const HotKey(key: PhysicalKeyboardKey.arrowDown),
|
||||
'seek_forward': const HotKey(key: PhysicalKeyboardKey.arrowRight),
|
||||
'seek_backward': const HotKey(key: PhysicalKeyboardKey.arrowLeft),
|
||||
'seek_forward_large': const HotKey(key: PhysicalKeyboardKey.arrowRight, modifiers: [HotKeyModifier.shift]),
|
||||
'seek_backward_large': const HotKey(key: PhysicalKeyboardKey.arrowLeft, modifiers: [HotKeyModifier.shift]),
|
||||
'fullscreen_toggle': const HotKey(key: PhysicalKeyboardKey.keyF),
|
||||
'mute_toggle': const HotKey(key: PhysicalKeyboardKey.keyM),
|
||||
'subtitle_toggle': const HotKey(key: PhysicalKeyboardKey.keyS),
|
||||
'audio_track_next': const HotKey(key: PhysicalKeyboardKey.keyA),
|
||||
'subtitle_track_next': const HotKey(key: PhysicalKeyboardKey.keyS, modifiers: [HotKeyModifier.shift]),
|
||||
'chapter_next': const HotKey(key: PhysicalKeyboardKey.keyN),
|
||||
'chapter_previous': const HotKey(key: PhysicalKeyboardKey.keyP),
|
||||
'speed_increase': const HotKey(key: PhysicalKeyboardKey.equal),
|
||||
'speed_decrease': const HotKey(key: PhysicalKeyboardKey.minus),
|
||||
'speed_reset': const HotKey(key: PhysicalKeyboardKey.keyR),
|
||||
'sub_seek_next': const HotKey(key: PhysicalKeyboardKey.arrowRight, modifiers: [HotKeyModifier.control]),
|
||||
'sub_seek_prev': const HotKey(key: PhysicalKeyboardKey.arrowLeft, modifiers: [HotKeyModifier.control]),
|
||||
'shader_toggle': const HotKey(key: PhysicalKeyboardKey.keyG),
|
||||
'skip_marker': const HotKey(key: PhysicalKeyboardKey.enter),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@ ThemeData monoTheme({required bool dark, bool oled = false}) {
|
||||
color: c.surface,
|
||||
elevation: 0,
|
||||
margin: EdgeInsets.zero,
|
||||
shape: RoundedRectangleBorder(borderRadius: const BorderRadius.all(Radius.circular(14))),
|
||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(14))),
|
||||
),
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
filled: true,
|
||||
|
||||
@@ -453,7 +453,7 @@ class DesktopVideoControlsState extends State<DesktopVideoControls> {
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(color: Colors.red, borderRadius: const BorderRadius.all(Radius.circular(4))),
|
||||
decoration: const BoxDecoration(color: Colors.red, borderRadius: BorderRadius.all(Radius.circular(4))),
|
||||
child: Text(
|
||||
t.liveTv.live,
|
||||
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 12),
|
||||
|
||||
@@ -199,7 +199,7 @@ class MobileVideoControls extends StatelessWidget {
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(color: Colors.red, borderRadius: const BorderRadius.all(Radius.circular(4))),
|
||||
decoration: const BoxDecoration(color: Colors.red, borderRadius: BorderRadius.all(Radius.circular(4))),
|
||||
child: Text(
|
||||
t.liveTv.live,
|
||||
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 12),
|
||||
|
||||
@@ -35,7 +35,6 @@ class ChapterSheet extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _ChapterSheetState extends State<ChapterSheet> {
|
||||
|
||||
/// Get the PlexClient for chapters, or null if unavailable (offline mode)
|
||||
PlexClient? _tryGetClientForChapters(BuildContext context) {
|
||||
if (widget.serverId == null) return null;
|
||||
@@ -49,55 +48,53 @@ class _ChapterSheetState extends State<ChapterSheet> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return StreamBuilder<Duration>(
|
||||
stream: widget.player.streams.position,
|
||||
initialData: widget.player.state.position,
|
||||
builder: (context, positionSnapshot) {
|
||||
final currentPosition = positionSnapshot.data ?? Duration.zero;
|
||||
final currentPositionMs = currentPosition.inMilliseconds;
|
||||
stream: widget.player.streams.position,
|
||||
initialData: widget.player.state.position,
|
||||
builder: (context, positionSnapshot) {
|
||||
final currentPosition = positionSnapshot.data ?? Duration.zero;
|
||||
final currentPositionMs = currentPosition.inMilliseconds;
|
||||
|
||||
// Find the current chapter based on position
|
||||
int? currentChapterIndex;
|
||||
for (int i = 0; i < widget.chapters.length; i++) {
|
||||
final chapter = widget.chapters[i];
|
||||
final startMs = chapter.startTimeOffset ?? 0;
|
||||
final endMs =
|
||||
chapter.endTimeOffset ??
|
||||
(i < widget.chapters.length - 1
|
||||
? widget.chapters[i + 1].startTimeOffset ?? 0
|
||||
: double.maxFinite.toInt());
|
||||
// Find the current chapter based on position
|
||||
int? currentChapterIndex;
|
||||
for (int i = 0; i < widget.chapters.length; i++) {
|
||||
final chapter = widget.chapters[i];
|
||||
final startMs = chapter.startTimeOffset ?? 0;
|
||||
final endMs =
|
||||
chapter.endTimeOffset ??
|
||||
(i < widget.chapters.length - 1 ? widget.chapters[i + 1].startTimeOffset ?? 0 : double.maxFinite.toInt());
|
||||
|
||||
if (currentPositionMs >= startMs && currentPositionMs < endMs) {
|
||||
currentChapterIndex = i;
|
||||
break;
|
||||
}
|
||||
if (currentPositionMs >= startMs && currentPositionMs < endMs) {
|
||||
currentChapterIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Widget content;
|
||||
if (!widget.chaptersLoaded) {
|
||||
content = const Center(child: CircularProgressIndicator());
|
||||
} else if (widget.chapters.isEmpty) {
|
||||
content = Center(
|
||||
child: Text(t.videoControls.noChaptersAvailable, style: TextStyle(color: tokens(context).textMuted)),
|
||||
);
|
||||
} else {
|
||||
content = ListView.builder(
|
||||
itemCount: widget.chapters.length,
|
||||
itemBuilder: (context, index) {
|
||||
final chapter = widget.chapters[index];
|
||||
final isCurrentChapter = currentChapterIndex == index;
|
||||
Widget content;
|
||||
if (!widget.chaptersLoaded) {
|
||||
content = const Center(child: CircularProgressIndicator());
|
||||
} else if (widget.chapters.isEmpty) {
|
||||
content = Center(
|
||||
child: Text(t.videoControls.noChaptersAvailable, style: TextStyle(color: tokens(context).textMuted)),
|
||||
);
|
||||
} else {
|
||||
content = ListView.builder(
|
||||
itemCount: widget.chapters.length,
|
||||
itemBuilder: (context, index) {
|
||||
final chapter = widget.chapters[index];
|
||||
final isCurrentChapter = currentChapterIndex == index;
|
||||
|
||||
// Get local file path for offline chapter thumbnails
|
||||
final localThumbPath = widget.serverId != null && chapter.thumb != null
|
||||
? DownloadStorageService.instance.getArtworkPathSync(widget.serverId!, chapter.thumb!)
|
||||
: null;
|
||||
// Get local file path for offline chapter thumbnails
|
||||
final localThumbPath = widget.serverId != null && chapter.thumb != null
|
||||
? DownloadStorageService.instance.getArtworkPathSync(widget.serverId!, chapter.thumb!)
|
||||
: null;
|
||||
|
||||
return FocusableListTile(
|
||||
leading: chapter.thumb != null
|
||||
? SizedBox(
|
||||
width: 60,
|
||||
height: 34,
|
||||
child: Stack(
|
||||
children: [
|
||||
return FocusableListTile(
|
||||
leading: chapter.thumb != null
|
||||
? SizedBox(
|
||||
width: 60,
|
||||
height: 34,
|
||||
child: Stack(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(4)),
|
||||
child: PlexOptimizedImage.thumb(
|
||||
@@ -114,48 +111,48 @@ class _ChapterSheetState extends State<ChapterSheet> {
|
||||
if (isCurrentChapter)
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(4)),
|
||||
border: const Border.fromBorderSide(BorderSide(color: Colors.blue, width: 2)),
|
||||
decoration: const BoxDecoration(
|
||||
borderRadius: BorderRadius.all(Radius.circular(4)),
|
||||
border: Border.fromBorderSide(BorderSide(color: Colors.blue, width: 2)),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: null,
|
||||
title: Text(
|
||||
chapter.label,
|
||||
style: TextStyle(
|
||||
color: isCurrentChapter ? Colors.blue : null,
|
||||
fontWeight: isCurrentChapter ? FontWeight.bold : FontWeight.normal,
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: null,
|
||||
title: Text(
|
||||
chapter.label,
|
||||
style: TextStyle(
|
||||
color: isCurrentChapter ? Colors.blue : null,
|
||||
fontWeight: isCurrentChapter ? FontWeight.bold : FontWeight.normal,
|
||||
),
|
||||
subtitle: Text(
|
||||
formatDurationTimestamp(chapter.startTime),
|
||||
style: TextStyle(
|
||||
color: isCurrentChapter ? Colors.blue.withValues(alpha: 0.7) : tokens(context).textMuted,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
formatDurationTimestamp(chapter.startTime),
|
||||
style: TextStyle(
|
||||
color: isCurrentChapter ? Colors.blue.withValues(alpha: 0.7) : tokens(context).textMuted,
|
||||
fontSize: 12,
|
||||
),
|
||||
trailing: isCurrentChapter
|
||||
? const AppIcon(Symbols.play_circle_rounded, fill: 1, color: Colors.blue)
|
||||
: null,
|
||||
onTap: () {
|
||||
widget.player.seek(chapter.startTime);
|
||||
OverlaySheetController.of(context).close();
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return BaseVideoControlSheet(
|
||||
title: t.videoControls.chapters,
|
||||
icon: Symbols.video_library_rounded,
|
||||
child: content,
|
||||
),
|
||||
trailing: isCurrentChapter
|
||||
? const AppIcon(Symbols.play_circle_rounded, fill: 1, color: Colors.blue)
|
||||
: null,
|
||||
onTap: () {
|
||||
widget.player.seek(chapter.startTime);
|
||||
OverlaySheetController.of(context).close();
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return BaseVideoControlSheet(
|
||||
title: t.videoControls.chapters,
|
||||
icon: Symbols.video_library_rounded,
|
||||
child: content,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,9 +62,7 @@ class QueueSheet extends StatelessWidget {
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
trailing: isCurrent
|
||||
? const AppIcon(Symbols.play_circle_rounded, fill: 1, color: Colors.blue)
|
||||
: null,
|
||||
trailing: isCurrent ? const AppIcon(Symbols.play_circle_rounded, fill: 1, color: Colors.blue) : null,
|
||||
onTap: () {
|
||||
onItemSelected(item);
|
||||
OverlaySheetController.of(context).close();
|
||||
@@ -74,11 +72,7 @@ class QueueSheet extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
return BaseVideoControlSheet(
|
||||
title: t.videoControls.queue,
|
||||
icon: Symbols.queue_music_rounded,
|
||||
child: content,
|
||||
);
|
||||
return BaseVideoControlSheet(title: t.videoControls.queue, icon: Symbols.queue_music_rounded, child: content);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -109,9 +103,9 @@ class QueueSheet extends StatelessWidget {
|
||||
if (isCurrent)
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(4)),
|
||||
border: const Border.fromBorderSide(BorderSide(color: Colors.blue, width: 2)),
|
||||
decoration: const BoxDecoration(
|
||||
borderRadius: BorderRadius.all(Radius.circular(4)),
|
||||
border: Border.fromBorderSide(BorderSide(color: Colors.blue, width: 2)),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1288,7 +1288,7 @@ class _PlexVideoControlsState extends State<PlexVideoControls> with WindowListen
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
AppIcon(Symbols.fast_forward_rounded, fill: 1, color: Colors.white, size: 16),
|
||||
const AppIcon(Symbols.fast_forward_rounded, fill: 1, color: Colors.white, size: 16),
|
||||
const SizedBox(width: 4),
|
||||
const Text(
|
||||
'2x',
|
||||
@@ -1565,7 +1565,9 @@ class _PlexVideoControlsState extends State<PlexVideoControls> with WindowListen
|
||||
// On Windows/Linux with navigation off, ESC only exits fullscreen —
|
||||
// never exits the player. Consume all back key events and check
|
||||
// actual window state asynchronously.
|
||||
if (!_videoPlayerNavigationEnabled && (Platform.isWindows || Platform.isLinux) && event.logicalKey.isBackKey) {
|
||||
if (!_videoPlayerNavigationEnabled &&
|
||||
(Platform.isWindows || Platform.isLinux) &&
|
||||
event.logicalKey.isBackKey) {
|
||||
if (event is KeyUpEvent) {
|
||||
_exitFullscreenIfNeeded();
|
||||
}
|
||||
|
||||
@@ -242,25 +242,25 @@ class _TimelineSliderState extends State<TimelineSlider> {
|
||||
overlayShape: const RoundSliderOverlayShape(overlayRadius: 12),
|
||||
),
|
||||
child: Semantics(
|
||||
label: t.videoControls.timelineSlider,
|
||||
slider: true,
|
||||
child: Slider(
|
||||
value: widget.duration.inMilliseconds > 0 ? widget.position.inMilliseconds.toDouble() : 0.0,
|
||||
min: 0.0,
|
||||
max: widget.duration.inMilliseconds.toDouble(),
|
||||
onChanged: (value) {
|
||||
setState(() => _dragValue = value);
|
||||
widget.onSeek(Duration(milliseconds: value.toInt()));
|
||||
},
|
||||
onChangeEnd: (value) {
|
||||
setState(() => _dragValue = null);
|
||||
widget.onSeekEnd(Duration(milliseconds: value.toInt()));
|
||||
},
|
||||
activeColor: Colors.white,
|
||||
inactiveColor: Colors.transparent,
|
||||
label: t.videoControls.timelineSlider,
|
||||
slider: true,
|
||||
child: Slider(
|
||||
value: widget.duration.inMilliseconds > 0 ? widget.position.inMilliseconds.toDouble() : 0.0,
|
||||
min: 0.0,
|
||||
max: widget.duration.inMilliseconds.toDouble(),
|
||||
onChanged: (value) {
|
||||
setState(() => _dragValue = value);
|
||||
widget.onSeek(Duration(milliseconds: value.toInt()));
|
||||
},
|
||||
onChangeEnd: (value) {
|
||||
setState(() => _dragValue = null);
|
||||
widget.onSeekEnd(Duration(milliseconds: value.toInt()));
|
||||
},
|
||||
activeColor: Colors.white,
|
||||
inactiveColor: Colors.transparent,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Chapter marker indicators
|
||||
if (widget.chaptersLoaded && widget.chapters.isNotEmpty && widget.duration.inMilliseconds > 0)
|
||||
|
||||
Reference in New Issue
Block a user