Files
plezy/lib/screens/libraries/tabs/base_library_tab.dart
T

171 lines
4.6 KiB
Dart

import 'dart:async';
import 'package:flutter/material.dart';
import '../../../models/plex_library.dart';
import '../../../utils/app_logger.dart';
import '../../../mixins/library_tab_state.dart';
import '../../../mixins/refreshable.dart';
import '../content_state_builder.dart';
/// Base class for library tab screens that provides common state management
/// and lifecycle handling for tabs that display library content.
///
/// Type parameter T: The type of items this tab displays
///
/// Subclasses must implement:
/// - [loadData]: Load data from the Plex API
/// - [buildContent]: Build the UI for displaying loaded items
///
/// Optional overrides:
/// - [emptyIcon]: Icon to show when there are no items
/// - [emptyMessage]: Message to show when there are no items
/// - [errorContext]: Context for error messages (defaults to "content")
/// - [getRefreshStream]: Stream to listen for refresh events
abstract class BaseLibraryTab<T> extends StatefulWidget {
final PlexLibrary library;
final String? viewMode;
final String? density;
const BaseLibraryTab({
super.key,
required this.library,
this.viewMode,
this.density,
});
}
/// State mixin that provides the common implementation for library tabs
/// This preserves AutomaticKeepAliveClientMixin functionality
abstract class BaseLibraryTabState<T, W extends BaseLibraryTab<T>>
extends State<W>
with AutomaticKeepAliveClientMixin, Refreshable, LibraryTabStateMixin {
@override
bool get wantKeepAlive => true;
@override
PlexLibrary get library => widget.library;
@override
void refresh() {
loadItems();
}
// State management
List<T> _items = [];
bool _isLoading = false;
String? _errorMessage;
StreamSubscription<void>? _refreshSubscription;
// Getters for subclasses
List<T> get items => _items;
bool get isLoading => _isLoading;
String? get errorMessage => _errorMessage;
@override
void initState() {
super.initState();
loadItems();
// Subscribe to refresh stream if provided
final refreshStream = getRefreshStream();
if (refreshStream != null) {
_refreshSubscription = refreshStream.listen((_) {
if (mounted) {
loadItems();
}
});
}
}
@override
void dispose() {
_refreshSubscription?.cancel();
super.dispose();
}
@override
void didUpdateWidget(W oldWidget) {
super.didUpdateWidget(oldWidget);
// Reload if library changed
if (oldWidget.library.globalKey != widget.library.globalKey) {
loadItems();
}
}
/// Load items from the API
/// This is the main data loading function that subclasses must implement
Future<List<T>> loadData();
/// Build the content widget given the loaded items
/// This is called by ContentStateBuilder when items are available
Widget buildContent(List<T> items);
/// Icon to display when there are no items (empty state)
IconData get emptyIcon;
/// Message to display when there are no items (empty state)
String get emptyMessage;
/// Context string for error messages (e.g., "playlists", "collections")
String get errorContext;
/// Optional refresh stream to listen for external refresh events
/// Return null if no refresh stream is needed
Stream<void>? getRefreshStream() => null;
/// Load items with error handling and state management
Future<void> loadItems() async {
setState(() {
_isLoading = true;
_errorMessage = null;
});
try {
final loadedItems = await loadData();
if (!mounted) return;
setState(() {
_items = loadedItems;
_isLoading = false;
});
} catch (e) {
if (!mounted) return;
appLogger.e('Error loading $errorContext', error: e);
setState(() {
_errorMessage = 'Failed to load $errorContext: ${e.toString()}';
_isLoading = false;
});
}
}
/// Focus the first item in the tab content
/// Subclasses can override this for custom focus behavior
void focusFirstItem() {
// Default implementation: try to focus the first focusable item
if (_items.isNotEmpty && mounted) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
FocusScope.of(context).nextFocus();
}
});
}
}
@override
Widget build(BuildContext context) {
super.build(context); // Required for AutomaticKeepAliveClientMixin
return ContentStateBuilder<T>(
isLoading: _isLoading,
errorMessage: _errorMessage,
items: _items,
emptyIcon: emptyIcon,
emptyMessage: emptyMessage,
onRetry: loadItems,
builder: (items) =>
RefreshIndicator(onRefresh: loadItems, child: buildContent(items)),
);
}
}