107 lines
3.9 KiB
Dart
107 lines
3.9 KiB
Dart
import '../../data/ducet_order.dart';
|
||
import '../../media/library_first_character.dart';
|
||
|
||
/// Shared letter-index mapping logic used by both [AlphaJumpBar] (desktop/tablet/TV)
|
||
/// and [AlphaScrollHandle] (phone).
|
||
///
|
||
/// Builds a dynamic letter list and cumulative index map from [LibraryFirstCharacter]
|
||
/// data returned by the server's first-character endpoint. Only letters that
|
||
/// have items are included, supporting non-Latin scripts (Korean, Japanese,
|
||
/// Cyrillic, etc.).
|
||
///
|
||
/// Plex's `/firstCharacter` endpoint returns characters in Unicode codepoint
|
||
/// order, which doesn't match the content endpoint's ICU locale-aware sort.
|
||
/// We re-sort using ICU collation so cumulative indices are correct.
|
||
class AlphaJumpHelper {
|
||
/// Dynamic letter list derived from the API's firstCharacter data,
|
||
/// re-sorted to match ICU collation order.
|
||
final List<String> letters;
|
||
|
||
/// Maps each letter to its cumulative start index in the full item list.
|
||
final Map<String, int> letterToIndex;
|
||
|
||
/// Maps each letter to its item count.
|
||
final Map<String, int> letterSizes;
|
||
|
||
/// Total number of items across all letters.
|
||
final int totalItemCount;
|
||
|
||
AlphaJumpHelper._(this.letters, this.letterToIndex, this.letterSizes, this.totalItemCount);
|
||
|
||
factory AlphaJumpHelper(List<LibraryFirstCharacter> firstCharacters) {
|
||
// Collect characters with their sizes.
|
||
final entries = <({String letter, int size})>[];
|
||
final letterSizes = <String, int>{};
|
||
|
||
for (final fc in firstCharacters) {
|
||
final letter = fc.title.toUpperCase();
|
||
if (fc.size > 0) {
|
||
entries.add((letter: letter, size: fc.size));
|
||
letterSizes[letter] = fc.size;
|
||
}
|
||
}
|
||
|
||
// Re-sort by DUCET collation to match the content endpoint's ICU sort order.
|
||
entries.sort((a, b) => ducetCompare(a.letter, b.letter));
|
||
|
||
// Build cumulative index map in the corrected order.
|
||
final letters = <String>[];
|
||
final letterToIndex = <String, int>{};
|
||
int cumulative = 0;
|
||
|
||
for (final e in entries) {
|
||
letters.add(e.letter);
|
||
letterToIndex[e.letter] = cumulative;
|
||
cumulative += e.size;
|
||
}
|
||
|
||
return AlphaJumpHelper._(letters, letterToIndex, letterSizes, cumulative);
|
||
}
|
||
|
||
/// Returns at most [maxCount] letters, prioritizing those with the most
|
||
/// items. The returned letters maintain their original order.
|
||
List<String> displayLetters(int maxCount) {
|
||
if (maxCount >= letters.length) return letters;
|
||
if (maxCount <= 0) return const [];
|
||
|
||
final indices = List.generate(letters.length, (i) => i);
|
||
indices.sort((a, b) => (letterSizes[letters[b]] ?? 0).compareTo(letterSizes[letters[a]] ?? 0));
|
||
final kept = indices.take(maxCount).toList()..sort();
|
||
return [for (final i in kept) letters[i]];
|
||
}
|
||
|
||
/// Returns the letter that the given item index falls within.
|
||
String currentLetter(int itemIndex) {
|
||
if (letters.isEmpty) return '#';
|
||
String current = letters.first;
|
||
for (final letter in letters) {
|
||
final startIndex = letterToIndex[letter];
|
||
if (startIndex != null && startIndex <= itemIndex) {
|
||
current = letter;
|
||
}
|
||
}
|
||
return current;
|
||
}
|
||
|
||
/// Returns the cumulative start index for a letter, or null if not present.
|
||
int? indexForLetter(String letter) => letterToIndex[letter];
|
||
|
||
/// Returns the fractional position (0.0–1.0) for a letter, proportional to
|
||
/// item count. Letters with more items occupy a larger segment.
|
||
double fractionForLetter(String letter) {
|
||
if (totalItemCount == 0) return 0.0;
|
||
final index = letterToIndex[letter];
|
||
if (index == null) return 0.0;
|
||
return index / totalItemCount;
|
||
}
|
||
|
||
/// Returns the letter at a given fractional position (0.0–1.0), proportional
|
||
/// to item count.
|
||
String letterAtFraction(double fraction) {
|
||
if (letters.isEmpty) return '#';
|
||
if (totalItemCount == 0) return letters.first;
|
||
final targetIndex = (fraction * totalItemCount).round().clamp(0, totalItemCount - 1);
|
||
return currentLetter(targetIndex);
|
||
}
|
||
}
|