Files
plezy/lib/screens/libraries/library_alpha_bar_strategy.dart
T

216 lines
7.3 KiB
Dart
Raw 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 '../../media/library_first_character.dart';
import '../../media/media_backend.dart';
import '../../services/plex_client.dart';
import 'alpha_jump_helper.dart';
/// Backend-specific alpha-jump-bar behaviour.
///
/// Plex libraries use real per-letter counts from
/// `/library/sections/{id}/firstCharacter`, so the bar is scroll-position
/// driven — tapping a letter scrolls to that letter's cumulative offset and
/// the highlighted letter follows the visible row.
///
/// Jellyfin libraries have no per-letter count endpoint. The bar synthesises
/// the 27-letter alphabet (`#`, `A``Z`) and acts as a name-prefix filter
/// that refetches the page when the user picks a letter (matches the JF web
/// client's UX).
abstract class LibraryAlphaBarStrategy {
/// Whether the bar should be rendered at all. Implementations consider
/// total item count, sort key, and current filter state.
bool shouldShow({
required int totalItemCount,
required int loadedCharacterCount,
required String? sortKey,
required bool isFolderGrouping,
required String? jellyfinAlphaPrefix,
required bool isPhone,
});
/// Load the first-character buckets for the current filter state.
/// Returns the new helper plus the synthesised character list — the caller
/// stores both in widget state.
Future<({List<LibraryFirstCharacter> chars, AlphaJumpHelper helper})> loadCharacters({
required Map<String, String> filters,
required int? typeId,
required bool descending,
});
/// Letter to highlight given the current scroll-derived index. Plex maps
/// the index back through the cumulative offsets; Jellyfin echoes back
/// whatever filter is active.
String currentLetter(int index, AlphaJumpHelper helper, {String? jellyfinAlphaPrefix});
/// Handle a tap on the letter at [targetIndex]. Plex strategies invoke
/// [onPlexJump] with the cumulative item index for in-grid scrolling;
/// Jellyfin strategies invoke [onJellyfinPrefixChange] with the next
/// `NameStartsWith` prefix (or `null` to clear the filter when the user
/// re-taps the active letter). Each strategy ignores the callback that
/// doesn't apply to its UX, so callers can wire both unconditionally.
void onLetterPressed(
int targetIndex,
AlphaJumpHelper helper, {
required String? currentJellyfinPrefix,
required void Function(int index) onPlexJump,
required void Function(String? nextPrefix) onJellyfinPrefixChange,
});
/// Construct the right strategy for [backend].
factory LibraryAlphaBarStrategy.forBackend(
MediaBackend backend, {
required PlexClient Function() plexClientProvider,
required String libraryKey,
required bool isShared,
}) {
return switch (backend) {
MediaBackend.plex => PlexAlphaBarStrategy(
plexClientProvider: plexClientProvider,
libraryKey: libraryKey,
isShared: isShared,
),
MediaBackend.jellyfin => const JellyfinAlphaBarStrategy(),
};
}
}
/// Plex strategy — calls `/library/sections/{id}/firstCharacter` for real
/// per-letter counts and uses the cumulative offsets to drive scroll
/// position.
class PlexAlphaBarStrategy implements LibraryAlphaBarStrategy {
final PlexClient Function() plexClientProvider;
final String libraryKey;
final bool isShared;
PlexAlphaBarStrategy({required this.plexClientProvider, required this.libraryKey, required this.isShared});
@override
bool shouldShow({
required int totalItemCount,
required int loadedCharacterCount,
required String? sortKey,
required bool isFolderGrouping,
required String? jellyfinAlphaPrefix,
required bool isPhone,
}) {
if (isFolderGrouping) return false;
if (loadedCharacterCount < 6 || totalItemCount < 80) return false;
final s = sortKey ?? '';
return s.isEmpty || s.startsWith('titleSort');
}
@override
Future<({List<LibraryFirstCharacter> chars, AlphaJumpHelper helper})> loadCharacters({
required Map<String, String> filters,
required int? typeId,
required bool descending,
}) async {
if (isShared) {
// Shared libraries don't support first-characters.
return (chars: const <LibraryFirstCharacter>[], helper: AlphaJumpHelper(const []));
}
final client = plexClientProvider();
final params = Map<String, String>.from(filters);
params['includeCollections'] = '1';
final chars = await client.getFirstCharacters(libraryKey, type: typeId, filters: params.isNotEmpty ? params : null);
return (chars: chars, helper: AlphaJumpHelper(chars, descending: descending));
}
@override
String currentLetter(int index, AlphaJumpHelper helper, {String? jellyfinAlphaPrefix}) => helper.currentLetter(index);
/// Plex jumps the grid to the cumulative offset for the tapped letter —
/// the helper's letter list already encodes the per-letter ranges from
/// the server's `/firstCharacter` counts.
@override
void onLetterPressed(
int targetIndex,
AlphaJumpHelper helper, {
required String? currentJellyfinPrefix,
required void Function(int index) onPlexJump,
required void Function(String? nextPrefix) onJellyfinPrefixChange,
}) {
onPlexJump(targetIndex);
}
}
/// Jellyfin strategy — synthesises the 27-letter alphabet locally and uses
/// the bar as a `NameStartsWith` filter.
class JellyfinAlphaBarStrategy implements LibraryAlphaBarStrategy {
static const _letters = [
'#',
'A',
'B',
'C',
'D',
'E',
'F',
'G',
'H',
'I',
'J',
'K',
'L',
'M',
'N',
'O',
'P',
'Q',
'R',
'S',
'T',
'U',
'V',
'W',
'X',
'Y',
'Z',
];
const JellyfinAlphaBarStrategy();
@override
bool shouldShow({
required int totalItemCount,
required int loadedCharacterCount,
required String? sortKey,
required bool isFolderGrouping,
required String? jellyfinAlphaPrefix,
required bool isPhone,
}) {
if (isPhone) return false;
if (isFolderGrouping) return false;
if (loadedCharacterCount == 0) return false;
return totalItemCount >= 80 || jellyfinAlphaPrefix != null;
}
@override
Future<({List<LibraryFirstCharacter> chars, AlphaJumpHelper helper})> loadCharacters({
required Map<String, String> filters,
required int? typeId,
required bool descending,
}) async {
final synthetic = [for (final l in _letters) LibraryFirstCharacter(key: l, title: l, size: 1)];
return (chars: synthetic, helper: AlphaJumpHelper(synthetic, descending: descending));
}
@override
String currentLetter(int index, AlphaJumpHelper helper, {String? jellyfinAlphaPrefix}) => jellyfinAlphaPrefix ?? '';
/// Jellyfin reuses the alpha bar as a `NameStartsWith` filter. We map the
/// bar offset back to a letter (the synthesised `size: 1` entries make
/// offset == position in [helper.letters]) and toggle the filter — re-tap
/// the active letter to clear, otherwise set the new prefix.
@override
void onLetterPressed(
int targetIndex,
AlphaJumpHelper helper, {
required String? currentJellyfinPrefix,
required void Function(int index) onPlexJump,
required void Function(String? nextPrefix) onJellyfinPrefixChange,
}) {
if (targetIndex < 0 || targetIndex >= helper.letters.length) return;
final letter = helper.letters[targetIndex];
final next = (currentJellyfinPrefix == letter) ? null : letter;
onJellyfinPrefixChange(next);
}
}