Files
plezy/lib/screens/libraries/alpha_jump_helper.dart
2026-08-10 20:28:41 +02:00

107 lines
3.8 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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, {bool descending = false}) {
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;
}
}
entries.sort((a, b) => ducetCompare(a.letter, b.letter));
if (descending) {
entries.setAll(0, entries.reversed.toList());
}
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.01.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.01.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);
}
}