fix: resolve all dart analyze warnings

- Migrate RadioListTile to RadioGroup API (Flutter 3.32+)
- Guard BuildContext usage across async gaps
- Use const BorderRadius.all / EdgeInsets.all constructors
- Extract nested ternaries into helpers
- Prefer .first over [0], ??= over if-null assignment
- Prefix unused FocusNode params with _
- Add library directive for dangling doc comments
This commit is contained in:
edde746
2026-02-23 13:50:56 +01:00
parent 8918bdc93e
commit 8f50da4a75
19 changed files with 123 additions and 125 deletions
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -112,7 +112,7 @@ abstract class PlayerBase with PlayerStreamControllersMixin implements Player {
void _handleEvent(dynamic event) {
if (_disposed) return;
if (event is List && event.length == 2) {
final name = _propIdToName[event[0] as int];
final name = _propIdToName[event.first as int];
if (name != null) {
handlePropertyChange(name, event[1]);
}
+1 -1
View File
@@ -382,7 +382,7 @@ class _HubDetailScreenState extends State<HubDetailScreen> with Refreshable, Gri
episodePosterMode == EpisodePosterMode.episodeThumbnail && (isEpisodeOnlyHub || isMixedHub);
return SliverPadding(
padding: const EdgeInsets.fromLTRB(8, 8, 8, 8),
padding: const EdgeInsets.all(8),
sliver: SliverLayoutBuilder(
builder: (context, constraints) {
final maxExtent = GridSizeCalculator.getMaxCrossAxisExtentWithPadding(
+34 -32
View File
@@ -94,40 +94,41 @@ class _SortBottomSheetState extends State<SortBottomSheet> {
action: widget.onClear != null ? TextButton(onPressed: _handleClear, child: Text(t.common.clear)) : null,
),
Expanded(
child: ListView.builder(
padding: const EdgeInsets.symmetric(vertical: 8),
itemCount: widget.sortOptions.length,
itemBuilder: (context, index) {
final sort = widget.sortOptions[index];
final isSelected = _currentSort?.key == sort.key;
child: RadioGroup<PlexSort>(
groupValue: _currentSort,
onChanged: (value) {
if (value != null) _handleSortSelect(value);
},
child: ListView.builder(
padding: const EdgeInsets.symmetric(vertical: 8),
itemCount: widget.sortOptions.length,
itemBuilder: (context, index) {
final sort = widget.sortOptions[index];
final isSelected = _currentSort?.key == sort.key;
return Focus(
canRequestFocus: false,
skipTraversal: true,
onKeyEvent: (node, event) {
if (!event.isActionable) return KeyEventResult.ignored;
if (!isSelected) return KeyEventResult.ignored;
if (event.logicalKey.isLeftKey) {
_handleDirectionChange(sort, false);
return KeyEventResult.handled;
}
if (event.logicalKey.isRightKey) {
_handleDirectionChange(sort, true);
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
},
child: FocusableRadioListTile<PlexSort>(
focusNode: (widget.selectedSort?.key == sort.key || (widget.selectedSort == null && index == 0))
? _initialFocusNode
: null,
title: Text(sort.title),
value: sort,
groupValue: _currentSort,
onChanged: (value) {
if (value != null) _handleSortSelect(value);
return Focus(
canRequestFocus: false,
skipTraversal: true,
onKeyEvent: (node, event) {
if (!event.isActionable) return KeyEventResult.ignored;
if (!isSelected) return KeyEventResult.ignored;
if (event.logicalKey.isLeftKey) {
_handleDirectionChange(sort, false);
return KeyEventResult.handled;
}
if (event.logicalKey.isRightKey) {
_handleDirectionChange(sort, true);
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
},
secondary: isSelected
child: FocusableRadioListTile<PlexSort>(
focusNode: (widget.selectedSort?.key == sort.key || (widget.selectedSort == null && index == 0))
? _initialFocusNode
: null,
title: Text(sort.title),
value: sort,
secondary: isSelected
? SegmentedButton<bool>(
showSelectedIcon: false,
segments: const [
@@ -146,6 +147,7 @@ class _SortBottomSheetState extends State<SortBottomSheet> {
),
);
},
),
),
),
],
@@ -495,23 +495,25 @@ class _LibraryBrowseTabState extends BaseLibraryTabState<PlexMetadata, LibraryBr
final options = _getGroupingOptions();
return StatefulBuilder(
builder: (context, setSheetState) {
return ListView.builder(
shrinkWrap: true,
itemCount: options.length,
itemBuilder: (context, index) {
final grouping = options[index];
return RadioListTile<String>(
title: Text(_getGroupingLabel(grouping)),
value: grouping,
groupValue: pendingGrouping,
onChanged: (value) {
if (value == null) return;
setSheetState(() {
pendingGrouping = value;
});
},
);
return RadioGroup<String>(
groupValue: pendingGrouping,
onChanged: (value) {
if (value == null) return;
setSheetState(() {
pendingGrouping = value;
});
},
child: ListView.builder(
shrinkWrap: true,
itemCount: options.length,
itemBuilder: (context, index) {
final grouping = options[index];
return RadioListTile<String>(
title: Text(_getGroupingLabel(grouping)),
value: grouping,
);
},
),
);
},
);
+14 -14
View File
@@ -851,9 +851,7 @@ class _MediaDetailScreenState extends State<MediaDetailScreen> with WatchStateAw
const SizedBox(width: 4),
Text(
hasRating
? (starValue == starValue.truncateToDouble()
? '${starValue.toInt()}'
: starValue.toStringAsFixed(1))
? formatRating(starValue)
: t.mediaMenu.rate,
style: TextStyle(
color: Theme.of(context).colorScheme.onSecondaryContainer,
@@ -2064,7 +2062,7 @@ class _MediaDetailScreenState extends State<MediaDetailScreen> with WatchStateAw
duration: const Duration(milliseconds: 150),
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
borderRadius: const BorderRadius.all(Radius.circular(8)),
border: Border.all(
color: showFocus
? Theme.of(context).colorScheme.primary.withValues(alpha: 0.5)
@@ -2072,16 +2070,18 @@ class _MediaDetailScreenState extends State<MediaDetailScreen> with WatchStateAw
width: 2,
),
),
child: isTv
? Text(
metadata.summary!,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(height: 1.6),
)
: CollapsibleText(
text: metadata.summary!,
maxLines: isMobile ? 6 : 4,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(height: 1.6),
),
child: () {
final summaryStyle =
Theme.of(context).textTheme.bodyLarge?.copyWith(height: 1.6);
if (isTv) {
return Text(metadata.summary!, style: summaryStyle);
}
return CollapsibleText(
text: metadata.summary!,
maxLines: isMobile ? 6 : 4,
style: summaryStyle,
);
}(),
);
},
),
+17 -15
View File
@@ -242,19 +242,21 @@ class _MetadataEditScreenState extends State<MetadataEditScreen> {
title: Text(title),
content: SizedBox(
width: double.maxFinite,
child: ListView(
shrinkWrap: true,
children: options.map((option) {
return FocusableRadioListTile<String>(
title: Text(option.label),
value: option.value,
groupValue: selected,
onChanged: (val) {
setDialogState(() => selected = val);
Navigator.pop(dialogContext, val);
},
);
}).toList(),
child: RadioGroup<String>(
groupValue: selected,
onChanged: (val) {
setDialogState(() => selected = val);
Navigator.pop(dialogContext, val);
},
child: ListView(
shrinkWrap: true,
children: options.map((option) {
return FocusableRadioListTile<String>(
title: Text(option.label),
value: option.value,
);
}).toList(),
),
),
),
actions: [
@@ -490,7 +492,7 @@ class _MetadataEditScreenState extends State<MetadataEditScreen> {
width: 40,
height: 60,
child: ClipRRect(
borderRadius: BorderRadius.circular(4),
borderRadius: const BorderRadius.all(Radius.circular(4)),
child: PlexOptimizedImage(
client: _client,
imagePath: meta.thumb,
@@ -510,7 +512,7 @@ class _MetadataEditScreenState extends State<MetadataEditScreen> {
width: 80,
height: 45,
child: ClipRRect(
borderRadius: BorderRadius.circular(4),
borderRadius: const BorderRadius.all(Radius.circular(4)),
child: PlexOptimizedImage(
client: _client,
imagePath: meta.art,
+3 -3
View File
@@ -1157,6 +1157,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
/// Restore ambient lighting from persisted setting
Future<void> _restoreAmbientLighting() async {
final shaderProvider = context.read<ShaderProvider>();
final settings = await SettingsService.getInstance();
if (!settings.getAmbientLighting()) return;
@@ -1177,7 +1178,6 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
final outputAspect = playerSize.width / playerSize.height;
// Clear shaders — ambient lighting and shaders are mutually exclusive
final shaderProvider = context.read<ShaderProvider>();
if (shaderProvider.isShaderEnabled) {
await _shaderService!.applyPreset(ShaderPreset.none);
shaderProvider.setCurrentPreset(ShaderPreset.none);
@@ -1212,6 +1212,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
Future<void> _toggleAmbientLighting() async {
final ambientLighting = _ambientLightingService;
if (ambientLighting == null || !ambientLighting.isSupported) return;
final shaderProvider = context.read<ShaderProvider>();
if (ambientLighting.isEnabled) {
await ambientLighting.disable();
@@ -1232,7 +1233,6 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
final outputAspect = playerSize.width / playerSize.height;
// Clear shaders — ambient lighting and shaders are mutually exclusive
final shaderProvider = context.read<ShaderProvider>();
if (shaderProvider.isShaderEnabled) {
await _shaderService!.applyPreset(ShaderPreset.none);
shaderProvider.setCurrentPreset(ShaderPreset.none);
@@ -1248,7 +1248,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
final settings = await SettingsService.getInstance();
settings.setAmbientLighting(ambientLighting.isEnabled);
setState(() {});
if (mounted) setState(() {});
}
/// Toggle between contain and cover modes only (for pinch gesture)
+1 -3
View File
@@ -38,9 +38,7 @@ class AmbientLightingService {
try {
// Write static shader (only needs to happen once)
if (_shaderPath == null) {
_shaderPath = await _writeShaderToTemp(_generateShader());
}
_shaderPath ??= await _writeShaderToTemp(_generateShader());
if (kDebugMode) {
debugPrint('AmbientLightingService: Shader path: $_shaderPath');
+1 -1
View File
@@ -47,7 +47,7 @@ class ExternalPlayerService {
final player = settings.getSelectedExternalPlayer();
// On Android, always use native intent to avoid url_launcher opening in browser
if (Platform.isAndroid) {
if (Platform.isAndroid && context.mounted) {
return _launchAndroidNative(resolvedUrl, player, context);
}
+1 -1
View File
@@ -84,7 +84,7 @@ class LanguageCodes {
// Locale code (e.g. "en-US", "zh-TW")
final parts = code.split('-');
final langName = getLanguageName(parts[0]) ?? parts[0];
final langName = getLanguageName(parts.first) ?? parts.first;
final region = parts.length > 1 ? _regionNames[parts[1]] : null;
return region != null ? '$langName ($region)' : langName;
}
+6 -6
View File
@@ -43,8 +43,10 @@ Future<bool?> navigateToVideoPlayer(
bool isOffline = false,
PlexVideoPlaybackData? playbackData,
}) async {
// Extract navigator before any async operations
// Extract context-dependent values before any async operations
final navigator = Navigator.of(context);
final downloadProvider = context.read<DownloadProvider>();
final client = isOffline ? null : context.getClientForMetadata(metadata);
// Load saved media version preference if not explicitly provided
int mediaIndex = selectedMediaIndex ?? 0;
@@ -75,19 +77,17 @@ Future<bool?> navigateToVideoPlayer(
if (isOffline) {
// Offline mode: resolve local file path for the external player
final downloadProvider = context.read<DownloadProvider>();
final globalKey = '${metadata.serverId}:${metadata.ratingKey}';
final videoPath = await downloadProvider.getVideoFilePath(globalKey);
if (videoPath != null) {
if (videoPath != null && context.mounted) {
final videoUrl = videoPath.contains('://') ? videoPath : 'file://$videoPath';
launched = await ExternalPlayerService.launch(context: context, videoUrl: videoUrl);
}
} else {
final client = context.getClientForMetadata(metadata);
} else if (context.mounted) {
launched = await ExternalPlayerService.launch(
context: context,
metadata: metadata,
client: client,
client: client!,
mediaIndex: mediaIndex,
);
}
+3 -3
View File
@@ -129,7 +129,7 @@ class _ArtworkPickerDialogState extends State<ArtworkPickerDialog> {
height: 400,
child: _isLoading
? const Center(child: CircularProgressIndicator())
: _artworkList == null || _artworkList!.isEmpty
: (_artworkList == null || _artworkList!.isEmpty)
? Center(child: Text(t.metadataEdit.noArtworkAvailable))
: _buildGrid(),
),
@@ -193,10 +193,10 @@ class _ArtworkPickerDialogState extends State<ArtworkPickerDialog> {
Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8),
borderRadius: const BorderRadius.all(Radius.circular(8)),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: PlexOptimizedImage(
client: widget.client,
imagePath: thumbUrl,
+1 -13
View File
@@ -116,7 +116,7 @@ class _FocusableListTileState extends State<FocusableListTile> {
/// A RadioListTile that accepts a FocusNode for keyboard/controller navigation.
///
/// Uses Flutter's native RadioListTile focus support - no custom styling wrapper.
/// Can be used standalone with [groupValue]/[onChanged] or inside a [RadioGroup].
/// Requires a [RadioGroup] ancestor to manage selection state.
class FocusableRadioListTile<T> extends StatelessWidget {
/// The primary content of the list tile.
final Widget? title;
@@ -130,14 +130,6 @@ class FocusableRadioListTile<T> extends StatelessWidget {
/// The value represented by this radio button.
final T value;
/// The currently selected value for the group.
/// When provided, the widget works without a [RadioGroup] ancestor.
final T? groupValue;
/// Called when this radio button is selected.
/// When provided, the widget works without a [RadioGroup] ancestor.
final ValueChanged<T?>? onChanged;
/// Whether this radio button is part of a vertically dense list.
final bool dense;
@@ -156,8 +148,6 @@ class FocusableRadioListTile<T> extends StatelessWidget {
this.subtitle,
this.secondary,
required this.value,
this.groupValue,
this.onChanged,
this.dense = false,
this.focusNode,
this.autofocus = false,
@@ -171,8 +161,6 @@ class FocusableRadioListTile<T> extends StatelessWidget {
subtitle: subtitle,
secondary: secondary,
value: value,
groupValue: groupValue,
onChanged: onChanged,
dense: dense,
focusNode: focusNode,
autofocus: autofocus,
+2 -1
View File
@@ -1066,7 +1066,7 @@ class MediaContextMenuState extends State<MediaContextMenu> {
final globalKey = '${metadata.serverId}:${metadata.ratingKey}';
if (downloadProvider.isDownloaded(globalKey)) {
final videoPath = await downloadProvider.getVideoFilePath(globalKey);
if (videoPath != null) {
if (videoPath != null && context.mounted) {
final videoUrl = videoPath.contains('://') ? videoPath : 'file://$videoPath';
await ExternalPlayerService.launch(context: context, videoUrl: videoUrl);
return;
@@ -1074,6 +1074,7 @@ class MediaContextMenuState extends State<MediaContextMenu> {
}
final client = _getClientForItem();
if (!context.mounted) return;
await ExternalPlayerService.launch(context: context, metadata: metadata, client: client);
}
+1 -1
View File
@@ -392,7 +392,7 @@ class _OverlaySheetHostState extends State<OverlaySheetHost> with SingleTickerPr
}
}
KeyEventResult _handleKeyEvent(FocusNode node, KeyEvent event) {
KeyEventResult _handleKeyEvent(FocusNode _, KeyEvent event) {
// Suppress stale select key-ups
if (SelectKeyUpSuppressor.consumeIfSuppressed(event)) {
return KeyEventResult.handled;
+6 -3
View File
@@ -21,6 +21,9 @@ class RatingBottomSheet extends StatefulWidget {
State<RatingBottomSheet> createState() => _RatingBottomSheetState();
}
String formatRating(double value) =>
value == value.truncateToDouble() ? value.toInt().toString() : value.toStringAsFixed(1);
class _RatingBottomSheetState extends State<RatingBottomSheet> {
late double _selectedRating;
late final FocusNode _starsFocusNode;
@@ -52,13 +55,13 @@ class _RatingBottomSheetState extends State<RatingBottomSheet> {
height: 4,
decoration: BoxDecoration(
color: theme.colorScheme.onSurfaceVariant.withValues(alpha: 0.4),
borderRadius: BorderRadius.circular(2),
borderRadius: const BorderRadius.all(Radius.circular(2)),
),
),
const SizedBox(height: 20),
Text(
_selectedRating > 0
? '${_selectedRating == _selectedRating.truncateToDouble() ? _selectedRating.toInt().toString() : _selectedRating.toStringAsFixed(1)} / 5'
? '${formatRating(_selectedRating)} / 5'
: t.mediaMenu.rate,
style: theme.textTheme.titleMedium,
),
@@ -80,7 +83,7 @@ class _RatingBottomSheetState extends State<RatingBottomSheet> {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
borderRadius: const BorderRadius.all(Radius.circular(12)),
color: hasFocus
? theme.colorScheme.primary.withValues(alpha: 0.12)
: null,
+1 -1
View File
@@ -279,7 +279,7 @@ class SideNavigationRailState extends State<SideNavigationRail> {
}
/// Handle D-pad UP/DOWN by explicitly moving focus to the next/previous item.
KeyEventResult _handleVerticalNavigation(FocusNode node, KeyEvent event, List<String> focusOrder) {
KeyEventResult _handleVerticalNavigation(FocusNode _, KeyEvent event, List<String> focusOrder) {
if (event is! KeyDownEvent) return KeyEventResult.ignored;
final isDown = event.logicalKey == LogicalKeyboardKey.arrowDown;
+10 -9
View File
@@ -4,19 +4,20 @@
/// dart run scripts/generate_ducet_ranks.dart [allkeys.txt] [FractionalUCA.txt]
///
/// Downloads the files automatically if not provided.
library;
import 'dart:io';
// ---------------------------------------------------------------------------
// Weight tuple for multi-level UCA comparison
// ---------------------------------------------------------------------------
class _Weight implements Comparable<_Weight> {
class Weight implements Comparable<Weight> {
final List<(int, int, int)> levels;
final int codepoint; // tiebreaker
const _Weight(this.levels, this.codepoint);
const Weight(this.levels, this.codepoint);
@override
int compareTo(_Weight other) {
int compareTo(Weight other) {
final len = levels.length < other.levels.length ? levels.length : other.levels.length;
// Primary pass
@@ -60,8 +61,8 @@ bool _isKatakana(int cp) =>
// (opposite of raw DUCET). We adjust by subtracting 6 from katakana tertiary
// weights, placing them below hiragana tertiaries (0x000D+).
// ---------------------------------------------------------------------------
Map<int, _Weight> parseAllKeys(String text) {
final result = <int, _Weight>{};
Map<int, Weight> parseAllKeys(String text) {
final result = <int, Weight>{};
final weightRe = RegExp(r'\[([.*])([0-9A-Fa-f]{4})\.([0-9A-Fa-f]{4})\.([0-9A-Fa-f]{4})\]');
for (final line in text.split('\n')) {
@@ -94,7 +95,7 @@ Map<int, _Weight> parseAllKeys(String text) {
if (levels.every((l) => l.$1 == 0 && l.$2 == 0 && l.$3 == 0)) continue;
result[cp] = _Weight(levels, cp);
result[cp] = Weight(levels, cp);
}
return result;
@@ -122,7 +123,7 @@ Map<int, _Weight> parseAllKeys(String text) {
// Extract Kangxi radical → CJK mapping from header
final headerChars = line.substring(eqIdx + 1, colonIdx).runes.toList();
if (headerChars.length >= 2) {
final kangxi = headerChars[0];
final kangxi = headerChars.first;
final cjk = headerChars[1];
// Kangxi Radicals: U+2F00-U+2FD5
if (kangxi >= 0x2F00 && kangxi <= 0x2FD5 && cjk <= 0xFFFF) {
@@ -166,7 +167,7 @@ Map<int, _Weight> parseAllKeys(String text) {
// ---------------------------------------------------------------------------
// Build final ordered list
// ---------------------------------------------------------------------------
List<int> buildOrder(Map<int, _Weight> allKeys, List<int> cjkRadicalOrder) {
List<int> buildOrder(Map<int, Weight> allKeys, List<int> cjkRadicalOrder) {
final entries = allKeys.entries.toList();
entries.sort((a, b) => a.value.compareTo(b.value));
@@ -200,7 +201,7 @@ List<int> buildOrder(Map<int, _Weight> allKeys, List<int> cjkRadicalOrder) {
// Main
// ---------------------------------------------------------------------------
Future<void> main(List<String> args) async {
final allKeysPath = args.length > 0 ? args[0] : '/tmp/allkeys.txt';
final allKeysPath = args.isNotEmpty ? args.first : '/tmp/allkeys.txt';
final fracUcaPath = args.length > 1 ? args[1] : '/tmp/FractionalUCA.txt';
// Download if missing