chore: pre-commit ci hook, dart format

This commit is contained in:
edde746
2026-04-18 12:40:35 +02:00
parent 2bd9e6655f
commit 3da51f9d64
128 changed files with 2056 additions and 2048 deletions
@@ -20,7 +20,12 @@ class GridItemContext {
/// Callback to navigate to the sidebar (for first-column items).
final VoidCallback? navigateToSidebar;
const GridItemContext({required this.isFirstRow, required this.isFirstColumn, this.isListMode = false, this.navigateToSidebar});
const GridItemContext({
required this.isFirstRow,
required this.isFirstColumn,
this.isListMode = false,
this.navigateToSidebar,
});
}
/// A widget that automatically switches between grid and list view
+5 -16
View File
@@ -123,12 +123,7 @@ class FolderTreeItem extends StatelessWidget {
: AppIcon(expandIcon, fill: 1, size: 20),
),
const SizedBox(width: 8),
AppIcon(
_getIcon(),
fill: 1,
size: 20,
color: Theme.of(context).colorScheme.primary,
),
AppIcon(_getIcon(), fill: 1, size: 20, color: Theme.of(context).colorScheme.primary),
const SizedBox(width: 12),
Expanded(
child: Text(
@@ -234,7 +229,8 @@ class FolderTreeItem extends StatelessWidget {
) {
final posterUrl = item.posterThumb(mode: episodePosterMode);
final client = serverId != null ? context.getClientForServer(serverId!) : null;
final shouldBlur = hideSpoilers && item.shouldHideSpoiler && episodePosterMode == EpisodePosterMode.episodeThumbnail;
final shouldBlur =
hideSpoilers && item.shouldHideSpoiler && episodePosterMode == EpisodePosterMode.episodeThumbnail;
Widget image;
if (item.usesWideAspectRatio(episodePosterMode)) {
@@ -265,10 +261,7 @@ class FolderTreeItem extends StatelessWidget {
Widget _buildWatchOverlay(BuildContext context, bool showUnwatchedCount) {
final hasActiveProgress =
item.viewOffset != null &&
item.duration != null &&
item.viewOffset! > 0 &&
item.viewOffset! < item.duration!;
item.viewOffset != null && item.duration != null && item.viewOffset! > 0 && item.viewOffset! < item.duration!;
return Stack(
children: [
@@ -356,11 +349,7 @@ class FolderTreeItem extends StatelessWidget {
useBackgroundFocus: true,
disableScale: true,
descendantsAreFocusable: false,
child: GestureDetector(
onTap: _handleTap,
behavior: HitTestBehavior.opaque,
child: rowContent,
),
child: GestureDetector(onTap: _handleTap, behavior: HitTestBehavior.opaque, child: rowContent),
),
),
+5 -5
View File
@@ -159,7 +159,10 @@ class _FolderTreeViewState extends State<FolderTreeView> {
bool _isFolder(PlexMetadata item) {
// Folders typically don't have a specific type or might have special indicators
// Check for common folder indicators
return item.key?.contains('/folder') == true || item.type == null || item.type!.isEmpty || item.mediaType == PlexMediaType.unknown;
return item.key?.contains('/folder') == true ||
item.type == null ||
item.type!.isEmpty ||
item.mediaType == PlexMediaType.unknown;
}
List<Widget> _buildTreeItems(List<PlexMetadata> items, int depth, [String parentPath = '']) {
@@ -227,10 +230,7 @@ class _FolderTreeViewState extends State<FolderTreeView> {
return RefreshIndicator(
onRefresh: _loadRootFolders,
child: ListView(
padding: const EdgeInsets.symmetric(horizontal: 8),
children: _buildTreeItems(_rootFolders, 0),
),
child: ListView(padding: const EdgeInsets.symmetric(horizontal: 8), children: _buildTreeItems(_rootFolders, 0)),
);
}
}
+101 -107
View File
@@ -100,7 +100,6 @@ class _LibrariesScreenState extends State<LibrariesScreen>
String? _selectedLibraryGlobalKey;
bool _isInitialLoad = true;
/// Flag to prevent onTabChanged from focusing when we're programmatically changing tabs
bool _isRestoringTab = false;
@@ -317,7 +316,6 @@ class _LibrariesScreenState extends State<LibrariesScreen>
_focusCurrentTab();
}
@override
void dispose() {
_outerScrollController.dispose();
@@ -338,9 +336,7 @@ class _LibrariesScreenState extends State<LibrariesScreen>
if (listEquals(_visibleTabs, newTabs)) return;
// Save current tab type before changing
final currentTabType = _visibleTabs.length > tabController.index
? _visibleTabs[tabController.index]
: null;
final currentTabType = _visibleTabs.length > tabController.index ? _visibleTabs[tabController.index] : null;
// Dispose old focus nodes and controller
for (final node in _tabFocusNodes) {
@@ -350,10 +346,7 @@ class _LibrariesScreenState extends State<LibrariesScreen>
// Build new
_visibleTabs = newTabs;
_tabFocusNodes = List.generate(
newTabs.length,
(i) => FocusNode(debugLabel: 'tab_chip_${newTabs[i].name}'),
);
_tabFocusNodes = List.generate(newTabs.length, (i) => FocusNode(debugLabel: 'tab_chip_${newTabs[i].name}'));
initTabNavigation();
// Restore tab position: find current tab type in new set, default to first
@@ -370,7 +363,12 @@ class _LibrariesScreenState extends State<LibrariesScreen>
LibraryTabType.playlists => t.libraries.tabs.playlists,
};
Widget _buildTabContent(LibraryTabType type, {required PlexLibrary library, required bool isActive, required int tabIndex}) {
Widget _buildTabContent(
LibraryTabType type, {
required PlexLibrary library,
required bool isActive,
required int tabIndex,
}) {
return switch (type) {
LibraryTabType.recommended => LibraryRecommendedTab(
key: _recommendedTabKey,
@@ -481,7 +479,6 @@ class _LibrariesScreenState extends State<LibrariesScreen>
_focusCurrentTab();
}
});
}
@override
@@ -605,10 +602,7 @@ class _LibrariesScreenState extends State<LibrariesScreen>
FocusableButton(
autofocus: true,
onPressed: () => Navigator.pop(context, false),
child: TextButton(
onPressed: () => Navigator.pop(context, false),
child: Text(t.common.cancel),
),
child: TextButton(onPressed: () => Navigator.pop(context, false), child: Text(t.common.cancel)),
),
FocusableButton(
onPressed: () => Navigator.pop(context, true),
@@ -834,9 +828,8 @@ class _LibrariesScreenState extends State<LibrariesScreen>
}
Widget _buildLibraryDropdownTitle(List<PlexLibrary> visibleLibraries) {
final selectedLibrary = visibleLibraries
.where((lib) => lib.globalKey == _selectedLibraryGlobalKey)
.firstOrNull ??
final selectedLibrary =
visibleLibraries.where((lib) => lib.globalKey == _selectedLibraryGlobalKey).firstOrNull ??
visibleLibraries.firstOrNull;
if (selectedLibrary == null) return Text(t.libraries.title);
@@ -900,115 +893,116 @@ class _LibrariesScreenState extends State<LibrariesScreen>
return Scaffold(
body: ScrollConfiguration(
behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false),
child: CustomScrollView(
controller: _outerScrollController,
slivers: [
DesktopSliverAppBar(
title: _buildAppBarTitle(visibleLibraries),
pinned: true,
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
surfaceTintColor: Colors.transparent,
shadowColor: Colors.transparent,
scrolledUnderElevation: 0,
actions: [
FocusableActionBar(
key: _actionBarKey,
onNavigateLeft: () => getTabChipFocusNode(_visibleTabs.length - 1).requestFocus(),
onNavigateDown: _focusCurrentTab,
actions: [
if (allLibraries.isNotEmpty)
FocusableAction(
icon: Symbols.edit_rounded,
tooltip: t.libraries.manageLibraries,
onPressed: _showLibraryManagementSheet,
),
behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false),
child: CustomScrollView(
controller: _outerScrollController,
slivers: [
DesktopSliverAppBar(
title: _buildAppBarTitle(visibleLibraries),
pinned: true,
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
surfaceTintColor: Colors.transparent,
shadowColor: Colors.transparent,
scrolledUnderElevation: 0,
actions: [
FocusableActionBar(
key: _actionBarKey,
onNavigateLeft: () => getTabChipFocusNode(_visibleTabs.length - 1).requestFocus(),
onNavigateDown: _focusCurrentTab,
actions: [
if (allLibraries.isNotEmpty)
FocusableAction(
icon: Symbols.refresh_rounded,
tooltip: t.common.refresh,
onPressed: _refreshCurrentTab,
icon: Symbols.edit_rounded,
tooltip: t.libraries.manageLibraries,
onPressed: _showLibraryManagementSheet,
),
],
),
],
),
if (isLoadingLibraries)
const SliverFillRemaining(child: Center(child: CircularProgressIndicator()))
else if (_errorMessage != null && visibleLibraries.isEmpty)
SliverFillRemaining(
child: ErrorStateWidget(
message: _errorMessage!,
icon: Symbols.error_outline_rounded,
onRetry: () {
final librariesProvider = context.read<LibrariesProvider>();
librariesProvider.refresh();
},
),
)
else if (visibleLibraries.isEmpty)
SliverFillRemaining(
child: EmptyStateWidget(message: t.libraries.noLibrariesFound, icon: Symbols.video_library_rounded),
)
else ...[
// Tab selector chips (only on mobile - desktop has them in app bar)
if (selectedLibrary != null && !PlatformDetector.shouldUseSideNavigation(context))
SliverToBoxAdapter(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
for (int i = 0; i < _visibleTabs.length; i++) ...[
if (i > 0) const SizedBox(width: 8),
buildTabChip(
_getTabLabel(_visibleTabs[i]),
i,
onSelectWhenActive: _focusCurrentTab,
onNavigateDown: _focusCurrentTabFromTabBar,
onNavigateRightFromLast: () => _actionBarKey.currentState?.requestFocusOnFirst(),
FocusableAction(
icon: Symbols.refresh_rounded,
tooltip: t.common.refresh,
onPressed: _refreshCurrentTab,
),
],
),
],
),
],
if (isLoadingLibraries)
const SliverFillRemaining(child: Center(child: CircularProgressIndicator()))
else if (_errorMessage != null && visibleLibraries.isEmpty)
SliverFillRemaining(
child: ErrorStateWidget(
message: _errorMessage!,
icon: Symbols.error_outline_rounded,
onRetry: () {
final librariesProvider = context.read<LibrariesProvider>();
librariesProvider.refresh();
},
),
)
else if (visibleLibraries.isEmpty)
SliverFillRemaining(
child: EmptyStateWidget(message: t.libraries.noLibrariesFound, icon: Symbols.video_library_rounded),
)
else ...[
// Tab selector chips (only on mobile - desktop has them in app bar)
if (selectedLibrary != null && !PlatformDetector.shouldUseSideNavigation(context))
SliverToBoxAdapter(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
for (int i = 0; i < _visibleTabs.length; i++) ...[
if (i > 0) const SizedBox(width: 8),
buildTabChip(
_getTabLabel(_visibleTabs[i]),
i,
onSelectWhenActive: _focusCurrentTab,
onNavigateDown: _focusCurrentTabFromTabBar,
onNavigateRightFromLast: () => _actionBarKey.currentState?.requestFocusOnFirst(),
),
],
),
],
),
),
),
),
// Tab content
if (selectedLibrary != null)
SliverFillRemaining(
child: TabBarView(
key: ValueKey(_selectedLibraryGlobalKey),
controller: tabController,
// Disable swipe on desktop - trackpad scrolling triggers accidental tab switches
// See: https://github.com/flutter/flutter/issues/11132
physics: PlatformDetector.isDesktop(context) ? const NeverScrollableScrollPhysics() : null,
// Wrap each tab in ClipRect so horizontal overflow (e.g. hub rows
// with Clip.none) doesn't bleed into adjacent tabs during swipe transitions.
// The TabBarView's own clipBehavior only clips at the viewport level,
// not per-page, so we need per-child clipping.
children: [
for (int i = 0; i < _visibleTabs.length; i++)
ClipRect(child: _buildTabContent(
// Tab content
if (selectedLibrary != null)
SliverFillRemaining(
child: TabBarView(
key: ValueKey(_selectedLibraryGlobalKey),
controller: tabController,
// Disable swipe on desktop - trackpad scrolling triggers accidental tab switches
// See: https://github.com/flutter/flutter/issues/11132
physics: PlatformDetector.isDesktop(context) ? const NeverScrollableScrollPhysics() : null,
// Wrap each tab in ClipRect so horizontal overflow (e.g. hub rows
// with Clip.none) doesn't bleed into adjacent tabs during swipe transitions.
// The TabBarView's own clipBehavior only clips at the viewport level,
// not per-page, so we need per-child clipping.
children: [
for (int i = 0; i < _visibleTabs.length; i++)
ClipRect(
child: _buildTabContent(
_visibleTabs[i],
library: selectedLibrary,
isActive: tabController.index == i,
tabIndex: i,
)),
],
),
),
),
],
),
],
),
],
),
],
),
),
);
}
}
class _LibraryManagementSheet extends StatefulWidget {
final bool isDialog;
final List<PlexLibrary> allLibraries;
final Set<String> hiddenLibraryKeys;
+21 -18
View File
@@ -146,24 +146,27 @@ class _SortBottomSheetState extends State<SortBottomSheet> {
title: Text(sort.title),
value: sort,
secondary: isSelected
? SegmentedButton<bool>(
showSelectedIcon: false,
segments: const [
ButtonSegment(value: false, icon: AppIcon(Symbols.arrow_upward_rounded, fill: 1, size: 16)),
ButtonSegment(
value: true,
icon: AppIcon(Symbols.arrow_downward_rounded, fill: 1, size: 16),
),
],
selected: {_currentDescending},
onSelectionChanged: (Set<bool> newSelection) {
_handleDirectionChange(sort, newSelection.first);
},
)
: null,
),
);
},
? SegmentedButton<bool>(
showSelectedIcon: false,
segments: const [
ButtonSegment(
value: false,
icon: AppIcon(Symbols.arrow_upward_rounded, fill: 1, size: 16),
),
ButtonSegment(
value: true,
icon: AppIcon(Symbols.arrow_downward_rounded, fill: 1, size: 16),
),
],
selected: {_currentDescending},
onSelectionChanged: (Set<bool> newSelection) {
_handleDirectionChange(sort, newSelection.first);
},
)
: null,
),
);
},
),
),
),
@@ -643,52 +643,54 @@ class _LibraryBrowseTabState extends BaseLibraryTabState<PlexMetadata, LibraryBr
SelectKeyUpSuppressor.suppressSelectUntilKeyUp();
final options = _getGroupingOptions();
final controller = OverlaySheetController.of(context);
controller.show<String>(
showDragHandle: true,
builder: (sheetContext) => Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(16, 4, 16, 8),
child: Text(
t.libraries.groupings.title,
style: Theme.of(sheetContext).textTheme.titleMedium,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
Flexible(
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: options.map((grouping) {
final isSelected = _selectedGrouping == grouping;
return FocusableListTile(
dense: true,
leading: AppIcon(
isSelected ? Symbols.radio_button_checked_rounded : Symbols.radio_button_unchecked_rounded,
fill: 1,
),
title: Text(_getGroupingLabel(grouping)),
onTap: () => controller.close(grouping),
);
}).toList(),
controller
.show<String>(
showDragHandle: true,
builder: (sheetContext) => Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(16, 4, 16, 8),
child: Text(
t.libraries.groupings.title,
style: Theme.of(sheetContext).textTheme.titleMedium,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
),
Flexible(
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: options.map((grouping) {
final isSelected = _selectedGrouping == grouping;
return FocusableListTile(
dense: true,
leading: AppIcon(
isSelected ? Symbols.radio_button_checked_rounded : Symbols.radio_button_unchecked_rounded,
fill: 1,
),
title: Text(_getGroupingLabel(grouping)),
onTap: () => controller.close(grouping),
);
}).toList(),
),
),
),
],
),
],
),
).then((value) {
if (!mounted || value == null || value == _selectedGrouping) return;
setState(() {
_selectedGrouping = value;
});
StorageService.getInstance().then((storage) {
storage.saveLibraryGrouping(widget.library.globalKey, value);
});
_loadItems();
_loadFirstCharacters();
});
)
.then((value) {
if (!mounted || value == null || value == _selectedGrouping) return;
setState(() {
_selectedGrouping = value;
});
StorageService.getInstance().then((storage) {
storage.saveLibraryGrouping(widget.library.globalKey, value);
});
_loadItems();
_loadFirstCharacters();
});
}
void _showFiltersBottomSheet() {
@@ -776,7 +778,10 @@ class _LibraryBrowseTabState extends BaseLibraryTabState<PlexMetadata, LibraryBr
if (_totalSize == 0) return;
final targetIndex = shouldRestoreGridFocus && lastFocusedGridIndex! < _totalSize && _loadedItems.containsKey(lastFocusedGridIndex!) ? lastFocusedGridIndex! : 0;
final targetIndex =
shouldRestoreGridFocus && lastFocusedGridIndex! < _totalSize && _loadedItems.containsKey(lastFocusedGridIndex!)
? lastFocusedGridIndex!
: 0;
// Use firstItemFocusNode for index 0 (matches _buildMediaCardItem)
if (targetIndex == 0) {
@@ -1227,8 +1232,7 @@ class _LibraryBrowseTabState extends BaseLibraryTabState<PlexMetadata, LibraryBr
final settingsProvider = context.read<SettingsProvider>();
final maxExtent = GridSizeCalculator.getMaxCrossAxisExtent(context, settingsProvider.libraryDensity);
final crossAxisSpacing = GridLayoutConstants.crossAxisSpacing;
final columnCount =
((screenSize.width + crossAxisSpacing) / (maxExtent + crossAxisSpacing)).ceil().clamp(1, 100);
final columnCount = ((screenSize.width + crossAxisSpacing) / (maxExtent + crossAxisSpacing)).ceil().clamp(1, 100);
final itemWidth = screenSize.width / columnCount;
final itemHeight = itemWidth / GridLayoutConstants.posterAspectRatio;
final rowHeight = itemHeight + GridLayoutConstants.mainAxisSpacing;
@@ -1551,10 +1555,7 @@ class _SkeletonCard extends StatelessWidget {
FractionallySizedBox(
alignment: Alignment.centerLeft,
widthFactor: 0.6,
child: SkeletonLoader(
borderRadius: BorderRadius.all(Radius.circular(4)),
child: SizedBox(height: 11),
),
child: SkeletonLoader(borderRadius: BorderRadius.all(Radius.circular(4)), child: SizedBox(height: 11)),
),
],
),