diff --git a/analysis_options.yaml b/analysis_options.yaml index 3de9f1c9..f6b14e01 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -8,8 +8,62 @@ analyzer: - dart_code_linter dart_code_linter: + extends: + - package:dart_code_linter/presets/recommended.yaml + rules: - - avoid-unused-parameters + # --- Flutter rules (on top of recommended) --- + - avoid-border-all + - avoid-shrink-wrap-in-lists + - avoid-expanded-as-spacer + - avoid-wrapping-in-padding + - prefer-const-border-radius + - prefer-correct-edge-insets-constructor + - prefer-define-hero-tag + - use-setstate-synchronously + + # --- Additional useful Dart rules --- + - avoid-cascade-after-if-null + - avoid-collection-methods-with-unrelated-types + - avoid-unnecessary-type-assertions + - avoid-unrelated-type-assertions + - double-literal-format + - prefer-first + - prefer-last + - prefer-enums-by-name + - prefer-commenting-analyzer-ignores + + # --- Disable noisy rules from recommended preset --- + - no-magic-number: false + - avoid-dynamic: false + - format-comment: false + - newline-before-return: false + - prefer-moving-to-variable: false + - member-ordering: false + - prefer-extracting-callbacks: false + - avoid-returning-widgets: false + - no-equal-arguments: false + - avoid-passing-async-when-sync-expected: false + - avoid-redundant-async: false + - no-empty-block: false + - prefer-trailing-comma: false + - avoid-non-null-assertion: false + - prefer-conditional-expressions: false + + metrics: + cyclomatic-complexity: 70 + halstead-volume: 16000 + maintainability-index: 8 + maximum-nesting-level: 7 + number-of-parameters: 25 + number-of-methods: 150 + source-lines-of-code: 500 + + anti-patterns: + - long-method: + lines-of-code: 500 + - long-parameter-list: + number-of-parameters: 25 formatter: page_width: 120 diff --git a/lib/database/app_database.dart b/lib/database/app_database.dart index ed635ff1..e97610d5 100644 --- a/lib/database/app_database.dart +++ b/lib/database/app_database.dart @@ -200,12 +200,9 @@ class AppDatabase extends _$AppDatabase { LazyDatabase _openConnection() { return LazyDatabase(() async { - final Directory dbFolder; - if (Platform.isAndroid || Platform.isIOS) { - dbFolder = await getApplicationDocumentsDirectory(); - } else { - dbFolder = await getApplicationSupportDirectory(); - } + final dbFolder = (Platform.isAndroid || Platform.isIOS) + ? await getApplicationDocumentsDirectory() + : await getApplicationSupportDirectory(); final file = File(p.join(dbFolder.path, 'plezy_downloads.db')); diff --git a/lib/focus/focusable_chip_mixin.dart b/lib/focus/focusable_chip_mixin.dart index 0cbbff85..f28e0a86 100644 --- a/lib/focus/focusable_chip_mixin.dart +++ b/lib/focus/focusable_chip_mixin.dart @@ -102,7 +102,7 @@ mixin FocusableChipStateMixin on State { /// /// Returns [KeyEventResult.handled] if the event was consumed, /// [KeyEventResult.ignored] otherwise. - KeyEventResult handleChipKeyEvent(FocusNode node, KeyEvent event, ChipKeyCallbacks callbacks) { + KeyEventResult handleChipKeyEvent(FocusNode _, KeyEvent event, ChipKeyCallbacks callbacks) { final key = event.logicalKey; if (callbacks.onBack != null) { diff --git a/lib/focus/input_mode_tracker.dart b/lib/focus/input_mode_tracker.dart index c1752ae9..fe57c942 100644 --- a/lib/focus/input_mode_tracker.dart +++ b/lib/focus/input_mode_tracker.dart @@ -119,9 +119,7 @@ class _InputModeTrackerState extends State { onPointerHover: (_) => _setMode(InputMode.pointer), behavior: HitTestBehavior.translucent, child: MouseRegion( - cursor: _mode == InputMode.keyboard - ? SystemMouseCursors.none - : MouseCursor.defer, + cursor: _mode == InputMode.keyboard ? SystemMouseCursors.none : MouseCursor.defer, child: IgnorePointer( ignoring: _mode == InputMode.keyboard, child: _InputModeProvider(mode: _mode, child: widget.child), diff --git a/lib/main.dart b/lib/main.dart index 8dadf4f9..3a92545c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -90,7 +90,7 @@ void main() async { // Initialize TV detection and PiP service for Android if (Platform.isAndroid) { - futures.add(TvDetectionService.getInstance().then((_) {})); + futures.add(TvDetectionService.getInstance()); // Initialize PiP service to listen for PiP state changes PipService(); } @@ -99,7 +99,7 @@ void main() async { futures.add(MacOSTitlebarService.setupCustomTitlebar()); // Initialize storage service - futures.add(StorageService.getInstance().then((_) {})); + futures.add(StorageService.getInstance()); // Initialize language codes for track selection futures.add(LanguageCodes.initialize()); diff --git a/lib/mixins/tab_navigation_mixin.dart b/lib/mixins/tab_navigation_mixin.dart index 59df4516..555dfe66 100644 --- a/lib/mixins/tab_navigation_mixin.dart +++ b/lib/mixins/tab_navigation_mixin.dart @@ -65,6 +65,7 @@ mixin TabNavigationMixin on State, SingleTickerProv /// Called when the tab index changes. Override to add custom behaviour /// (e.g. persisting the tab index), then call `super.onTabChanged()`. void onTabChanged() { + // ignore: no-empty-block - setState triggers rebuild to reflect new tab setState(() {}); } diff --git a/lib/models/companion_remote/recent_remote_session.dart b/lib/models/companion_remote/recent_remote_session.dart index 18a72877..43e0ecc9 100644 --- a/lib/models/companion_remote/recent_remote_session.dart +++ b/lib/models/companion_remote/recent_remote_session.dart @@ -32,7 +32,7 @@ class RecentRemoteSession { throw FormatException('Invalid QR code format - expected ip|port|sessionId|pin'); } - final ip = parts[0]; + final ip = parts.first; final port = parts[1]; final sessionId = parts[2]; final pin = parts[3]; diff --git a/lib/models/companion_remote/remote_command.dart b/lib/models/companion_remote/remote_command.dart index d301da2c..233c63fd 100644 --- a/lib/models/companion_remote/remote_command.dart +++ b/lib/models/companion_remote/remote_command.dart @@ -15,10 +15,7 @@ class RemoteCommand { } Map toJson() { - return { - 't': type.index, - if (data != null) 'd': data, - }; + return {'t': type.index, if (data != null) 'd': data}; } @override diff --git a/lib/models/external_player_models.dart b/lib/models/external_player_models.dart index 9d6f59d9..c2ae6a9f 100644 --- a/lib/models/external_player_models.dart +++ b/lib/models/external_player_models.dart @@ -81,7 +81,7 @@ class ExternalPlayer { // --- Launch helpers --- -Future _launchWithUrl(String url) async { +Future _launchWithUrl(String url) { return launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication); } @@ -158,7 +158,7 @@ class KnownPlayers { name: 'VLC', iconAsset: 'assets/player_icons/vlc.svg', isAvailable: Platform.isAndroid || Platform.isIOS || Platform.isMacOS || Platform.isLinux || Platform.isWindows, - launch: (url) async { + launch: (url) { if (Platform.isAndroid) return _launchAndroidIntent(url, package: 'org.videolan.vlc'); if (Platform.isIOS) return _launchUrlScheme('vlc://', url); if (Platform.isMacOS) return _launchMacApp('VLC', url); @@ -170,7 +170,7 @@ class KnownPlayers { name: 'mpv', iconAsset: 'assets/player_icons/mpv.svg', isAvailable: Platform.isAndroid || Platform.isMacOS || Platform.isLinux || Platform.isWindows, - launch: (url) async { + launch: (url) { if (Platform.isAndroid) return _launchAndroidIntent(url, package: 'is.xyz.mpv'); return _launchCommand('mpv', url); }, diff --git a/lib/models/livetv_dvr.dart b/lib/models/livetv_dvr.dart index f087e0fb..07cc8280 100644 --- a/lib/models/livetv_dvr.dart +++ b/lib/models/livetv_dvr.dart @@ -68,12 +68,7 @@ class ChannelMapping { final bool? enabled; final String? lineupIdentifier; - ChannelMapping({ - this.channelKey, - this.deviceIdentifier, - this.enabled, - this.lineupIdentifier, - }); + ChannelMapping({this.channelKey, this.deviceIdentifier, this.enabled, this.lineupIdentifier}); factory ChannelMapping.fromJson(Map json) { return ChannelMapping( diff --git a/lib/models/livetv_hub_result.dart b/lib/models/livetv_hub_result.dart index a5093feb..642e1d62 100644 --- a/lib/models/livetv_hub_result.dart +++ b/lib/models/livetv_hub_result.dart @@ -7,11 +7,7 @@ class LiveTvHubResult { final String hubKey; final List entries; - LiveTvHubResult({ - required this.title, - required this.hubKey, - required this.entries, - }); + LiveTvHubResult({required this.title, required this.hubKey, required this.entries}); } /// A single item in a live TV hub, holding both display metadata and EPG timing. diff --git a/lib/models/livetv_program.dart b/lib/models/livetv_program.dart index 9ef7275c..1114bfa7 100644 --- a/lib/models/livetv_program.dart +++ b/lib/models/livetv_program.dart @@ -63,11 +63,9 @@ class LiveTvProgram { parentIndex: (json['parentIndex'] as num?)?.toInt(), thumb: json['thumb'] as String? ?? json['grandparentThumb'] as String?, art: json['art'] as String?, - channelIdentifier: json['channelIdentifier'] as String? - ?? media?['channelIdentifier']?.toString() - ?? channel?['id']?.toString(), - channelCallSign: json['channelCallSign'] as String? - ?? media?['channelCallSign'] as String?, + channelIdentifier: + json['channelIdentifier'] as String? ?? media?['channelIdentifier']?.toString() ?? channel?['id']?.toString(), + channelCallSign: json['channelCallSign'] as String? ?? media?['channelCallSign'] as String?, live: json['live'] == true || json['live'] == 1 || json['live'] == '1', premiere: json['premiere'] == true || json['premiere'] == 1 || json['premiere'] == '1', ); diff --git a/lib/models/livetv_subscription.dart b/lib/models/livetv_subscription.dart index 707b3cfb..a0fa5b2d 100644 --- a/lib/models/livetv_subscription.dart +++ b/lib/models/livetv_subscription.dart @@ -59,8 +59,7 @@ class LiveTvSubscription { } /// Creation time as DateTime - DateTime? get createdAtTime => - createdAt != null ? DateTime.fromMillisecondsSinceEpoch(createdAt! * 1000) : null; + DateTime? get createdAtTime => createdAt != null ? DateTime.fromMillisecondsSinceEpoch(createdAt! * 1000) : null; } /// Represents a setting within a DVR subscription @@ -93,10 +92,7 @@ class SubscriptionSetting { final parts = (json['enumValues'] as String).split('|'); options = parts.map((part) { final kv = part.split(':'); - return SubscriptionSettingOption( - value: kv[0], - label: kv.length > 1 ? kv[1] : kv[0], - ); + return SubscriptionSettingOption(value: kv.first, label: kv.length > 1 ? kv[1] : kv.first); }).toList(); } diff --git a/lib/models/plex_file_info.dart b/lib/models/plex_file_info.dart index 567206c3..54676308 100644 --- a/lib/models/plex_file_info.dart +++ b/lib/models/plex_file_info.dart @@ -75,11 +75,7 @@ class PlexFileInfo { final minutes = (seconds % 3600) ~/ 60; final secs = seconds % 60; - if (hours > 0) { - return '${hours}h ${minutes}m ${secs}s'; - } else { - return '${minutes}m ${secs}s'; - } + return hours > 0 ? '${hours}h ${minutes}m ${secs}s' : '${minutes}m ${secs}s'; } /// Format bitrate in Mbps or Kbps diff --git a/lib/models/plex_media_version.dart b/lib/models/plex_media_version.dart index 85676b05..a03466ed 100644 --- a/lib/models/plex_media_version.dart +++ b/lib/models/plex_media_version.dart @@ -26,7 +26,7 @@ class PlexMediaVersion { factory PlexMediaVersion.fromJson(Map json) { // Get the first Part key for playback final parts = json['Part'] as List?; - final partKey = parts != null && parts.isNotEmpty ? parts[0]['key'] as String? ?? '' : ''; + final partKey = parts != null && parts.isNotEmpty ? parts.first['key'] as String? ?? '' : ''; return PlexMediaVersion( id: json['id'] as int? ?? 0, diff --git a/lib/models/shader_preset.dart b/lib/models/shader_preset.dart index 1d4a4b10..95dcb254 100644 --- a/lib/models/shader_preset.dart +++ b/lib/models/shader_preset.dart @@ -51,8 +51,8 @@ class Anime4KConfig { factory Anime4KConfig.fromJson(Map json) { return Anime4KConfig( - quality: Anime4KQuality.values.firstWhere((e) => e.name == json['quality'], orElse: () => Anime4KQuality.fast), - mode: Anime4KMode.values.firstWhere((e) => e.name == json['mode'], orElse: () => Anime4KMode.modeA), + quality: Anime4KQuality.values.asNameMap()[json['quality']] ?? Anime4KQuality.fast, + mode: Anime4KMode.values.asNameMap()[json['mode']] ?? Anime4KMode.modeA, ); } } @@ -207,7 +207,7 @@ class ShaderPreset { return ShaderPreset( id: id ?? 'custom', name: json['name'] as String? ?? 'Custom', - type: ShaderPresetType.values.firstWhere((e) => e.name == json['type'], orElse: () => ShaderPresetType.none), + type: ShaderPresetType.values.asNameMap()[json['type']] ?? ShaderPresetType.none, anime4kConfig: json['anime4kConfig'] != null ? Anime4KConfig.fromJson(json['anime4kConfig']) : null, nvscalerConfig: json['nvscalerConfig'] != null ? NVScalerConfig.fromJson(json['nvscalerConfig']) : null, ); diff --git a/lib/mpv/player/player_android.dart b/lib/mpv/player/player_android.dart index 07f5d7c4..7850bc21 100644 --- a/lib/mpv/player/player_android.dart +++ b/lib/mpv/player/player_android.dart @@ -232,7 +232,7 @@ class PlayerAndroid extends PlayerBase { // Handle MPV commands by translating to ExoPlayer equivalents if (args.isEmpty) return; - switch (args[0]) { + switch (args.first) { case 'loadfile': if (args.length > 1) { await open(Media(args[1])); diff --git a/lib/mpv/player/player_base.dart b/lib/mpv/player/player_base.dart index e47098c3..3604788f 100644 --- a/lib/mpv/player/player_base.dart +++ b/lib/mpv/player/player_base.dart @@ -298,11 +298,9 @@ abstract class PlayerBase with PlayerStreamControllersMixin implements Player { final id = trackId?.toString(); SubtitleTrack? selectedTrack; - if (id == null || id == 'no') { - selectedTrack = SubtitleTrack.off; - } else { - selectedTrack = _state.tracks.subtitle.cast().firstWhere((t) => t?.id == id, orElse: () => null); - } + selectedTrack = (id == null || id == 'no') + ? SubtitleTrack.off + : _state.tracks.subtitle.cast().firstWhere((t) => t?.id == id, orElse: () => null); _state = _state.copyWith(track: _state.track.copyWith(subtitle: selectedTrack)); trackController.add(_state.track); @@ -347,19 +345,16 @@ abstract class PlayerBase with PlayerStreamControllersMixin implements Player { } @override - Future updateFrame() async { - // Default no-op, overridden by platforms that need it - } + // ignore: no-empty-block - base no-op, overridden by platform subclasses + Future updateFrame() async {} @override - Future setVideoFrameRate(double fps, int durationMs) async { - // Default no-op, overridden by platforms that support it - } + // ignore: no-empty-block - base no-op, overridden by platform subclasses + Future setVideoFrameRate(double fps, int durationMs) async {} @override - Future clearVideoFrameRate() async { - // Default no-op, overridden by platforms that support it - } + // ignore: no-empty-block - base no-op, overridden by platform subclasses + Future clearVideoFrameRate() async {} @override Future requestAudioFocus() async { @@ -368,19 +363,16 @@ abstract class PlayerBase with PlayerStreamControllersMixin implements Player { } @override - Future abandonAudioFocus() async { - // Default no-op, overridden by Android - } + // ignore: no-empty-block - base no-op, overridden by platform subclasses + Future abandonAudioFocus() async {} @override - Future setAudioDevice(AudioDevice device) async { - // Default no-op, overridden by platforms that support it - } + // ignore: no-empty-block - base no-op, overridden by platform subclasses + Future setAudioDevice(AudioDevice device) async {} @override - Future setAudioPassthrough(bool enabled) async { - // Default no-op, overridden by platforms that support it - } + // ignore: no-empty-block - base no-op, overridden by platform subclasses + Future setAudioPassthrough(bool enabled) async {} // ============================================ // Lifecycle diff --git a/lib/mpv/player/player_native.dart b/lib/mpv/player/player_native.dart index 7f3acd7b..a45c9539 100644 --- a/lib/mpv/player/player_native.dart +++ b/lib/mpv/player/player_native.dart @@ -88,8 +88,7 @@ class PlayerNative extends PlayerBase { /// Returns null if the call fails. Future _openContentFd(String contentUri) async { try { - final fd = await methodChannel.invokeMethod('openContentFd', {'uri': contentUri}); - return fd; + return await methodChannel.invokeMethod('openContentFd', {'uri': contentUri}); } catch (e) { return null; } diff --git a/lib/mpv/player/video_rect_support.dart b/lib/mpv/player/video_rect_support.dart index 17770faa..1fab6574 100644 --- a/lib/mpv/player/video_rect_support.dart +++ b/lib/mpv/player/video_rect_support.dart @@ -1,9 +1,11 @@ +import 'player.dart'; + /// Mixin for players that support video rect positioning. /// /// Players that render video behind the Flutter view (e.g., using /// native window embedding or GtkGLArea) implement this mixin to /// receive layout updates from the [Video] widget. -mixin VideoRectSupport { +mixin VideoRectSupport on Player { /// Updates the video rendering area. /// /// Called by the [Video] widget when the layout changes. diff --git a/lib/mpv/video.dart b/lib/mpv/video.dart index 00589eb7..5389f9a0 100644 --- a/lib/mpv/video.dart +++ b/lib/mpv/video.dart @@ -89,7 +89,7 @@ class _VideoState extends State