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:
File diff suppressed because one or more lines are too long
@@ -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]);
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}(),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user