first commit
This commit is contained in:
@@ -0,0 +1,614 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:finamp/components/MusicScreen/artist_item_list_tile.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||
|
||||
import '../../models/finamp_models.dart';
|
||||
import '../../models/jellyfin_models.dart';
|
||||
import '../../services/downloads_helper.dart';
|
||||
import '../../services/finamp_settings_helper.dart';
|
||||
import '../../services/finamp_user_helper.dart';
|
||||
import '../../services/jellyfin_api_helper.dart';
|
||||
import '../AlbumScreen/song_list_tile.dart';
|
||||
import '../error_snackbar.dart';
|
||||
import '../first_page_progress_indicator.dart';
|
||||
import '../new_page_progress_indicator.dart';
|
||||
import 'album_item.dart';
|
||||
import 'alphabet_item_list.dart';
|
||||
|
||||
class MusicScreenTabView extends StatefulWidget {
|
||||
const MusicScreenTabView({
|
||||
Key? key,
|
||||
required this.tabContentType,
|
||||
this.parentItem,
|
||||
this.searchTerm,
|
||||
required this.isFavourite,
|
||||
this.sortBy,
|
||||
this.sortOrder,
|
||||
this.view,
|
||||
this.albumArtist,
|
||||
}) : super(key: key);
|
||||
|
||||
final TabContentType tabContentType;
|
||||
final BaseItemDto? parentItem;
|
||||
final String? searchTerm;
|
||||
final bool isFavourite;
|
||||
final SortBy? sortBy;
|
||||
final SortOrder? sortOrder;
|
||||
final BaseItemDto? view;
|
||||
final String? albumArtist;
|
||||
|
||||
@override
|
||||
State<MusicScreenTabView> createState() => _MusicScreenTabViewState();
|
||||
}
|
||||
|
||||
// We use AutomaticKeepAliveClientMixin so that the view keeps its position after the tab is changed.
|
||||
// https://stackoverflow.com/questions/49439047/how-to-preserve-widget-states-in-flutter-when-navigating-using-bottomnavigation
|
||||
class _MusicScreenTabViewState extends State<MusicScreenTabView>
|
||||
with AutomaticKeepAliveClientMixin<MusicScreenTabView> {
|
||||
// If parentItem is null, we assume that this view is actually in a tab.
|
||||
// If it isn't null, this view is being used as an artist detail screen and shouldn't be kept alive.
|
||||
@override
|
||||
bool get wantKeepAlive => widget.parentItem == null;
|
||||
|
||||
static const _pageSize = 100;
|
||||
|
||||
final PagingController<int, BaseItemDto> _pagingController =
|
||||
PagingController(firstPageKey: 0);
|
||||
|
||||
List<BaseItemDto>? offlineSortedItems;
|
||||
|
||||
final _jellyfinApiHelper = GetIt.instance<JellyfinApiHelper>();
|
||||
final _finampUserHelper = GetIt.instance<FinampUserHelper>();
|
||||
|
||||
String? _lastSearch;
|
||||
bool? _oldIsFavourite;
|
||||
SortBy? _oldSortBy;
|
||||
SortOrder? _oldSortOrder;
|
||||
BaseItemDto? _oldView;
|
||||
ScrollController? controller;
|
||||
String? letterToSearch;
|
||||
String lastSortOrder = SortOrder.ascending.toString();
|
||||
Timer? timer;
|
||||
|
||||
// This function just lets us easily set stuff to the getItems call we want.
|
||||
Future<void> _getPage(int pageKey) async {
|
||||
try {
|
||||
final sortOrder =
|
||||
widget.sortOrder?.toString() ?? SortOrder.ascending.toString();
|
||||
final newItems = await _jellyfinApiHelper.getItems(
|
||||
// starting with Jellyfin 10.9, only automatically created playlists will have a specific library as parent. user-created playlists will not be returned anymore
|
||||
// this condition fixes this by not providing a parentId when fetching playlists
|
||||
parentItem: widget.tabContentType == TabContentType.playlists
|
||||
? null
|
||||
: widget.parentItem ??
|
||||
widget.view ??
|
||||
_finampUserHelper.currentUser?.currentView,
|
||||
includeItemTypes: _includeItemTypes(widget.tabContentType),
|
||||
|
||||
// If we're on the songs tab, sort by "Album,SortName". This is what the
|
||||
// Jellyfin web client does. If this isn't the case, check if parentItem
|
||||
// is null. parentItem will be null when this widget is not used in an
|
||||
// artist view. If it's null, sort by "SortName". If it isn't null, check
|
||||
// if the parentItem is a MusicArtist. If it is, sort by year. Otherwise,
|
||||
// sort by SortName. If widget.sortBy is set, it is used instead.
|
||||
sortBy: widget.sortBy?.jellyfinName(widget.tabContentType) ??
|
||||
(widget.tabContentType == TabContentType.songs
|
||||
? "Album,SortName"
|
||||
: widget.parentItem == null
|
||||
? "SortName"
|
||||
: widget.parentItem?.type == "MusicArtist"
|
||||
? "ProductionYear,PremiereDate"
|
||||
: "SortName"),
|
||||
sortOrder: sortOrder,
|
||||
searchTerm: widget.searchTerm?.trim(),
|
||||
// If this is the genres tab, tell getItems to get genres.
|
||||
isGenres: widget.tabContentType == TabContentType.genres,
|
||||
filters: widget.isFavourite ? "IsFavorite" : null,
|
||||
startIndex: pageKey,
|
||||
limit: _pageSize,
|
||||
);
|
||||
|
||||
if (newItems!.length < _pageSize) {
|
||||
_pagingController.appendLastPage(newItems);
|
||||
} else {
|
||||
_pagingController.appendPage(newItems, pageKey + newItems.length);
|
||||
}
|
||||
if (letterToSearch != null) {
|
||||
scrollToLetter(letterToSearch);
|
||||
timer?.cancel();
|
||||
timer = Timer(const Duration(seconds: 2, milliseconds: 500), () {
|
||||
scrollToNearbyLetter();
|
||||
});
|
||||
}
|
||||
setState(() {
|
||||
lastSortOrder = sortOrder;
|
||||
});
|
||||
} catch (e) {
|
||||
errorSnackbar(e, context);
|
||||
}
|
||||
}
|
||||
|
||||
String _getParentType() =>
|
||||
widget.parentItem?.type! ??
|
||||
_finampUserHelper.currentUser!.currentView!.type!;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_pagingController.addPageRequestListener((pageKey) {
|
||||
_getPage(pageKey);
|
||||
});
|
||||
lastSortOrder =
|
||||
widget.sortOrder?.toString() ?? SortOrder.ascending.toString();
|
||||
controller = ScrollController();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(oldWidget) {
|
||||
setState(() {
|
||||
lastSortOrder =
|
||||
widget.sortOrder?.toString() ?? SortOrder.ascending.toString();
|
||||
});
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
|
||||
// Scrolls the list to the first occurrence of the letter in the list
|
||||
// If clicked in the # element, it goes to the first one ( pixels = 0 )
|
||||
void scrollToLetter(String? clickedLetter) async {
|
||||
String? letter = clickedLetter ?? letterToSearch;
|
||||
if (letter == null) return;
|
||||
|
||||
letterToSearch = letter;
|
||||
|
||||
if (letter == '#') {
|
||||
double targetScroll = lastSortOrder == SortOrder.ascending.toString()
|
||||
? -(controller!.position.maxScrollExtent * 10)
|
||||
: controller!.position.maxScrollExtent * 10;
|
||||
|
||||
await controller?.animateTo(targetScroll,
|
||||
duration: const Duration(milliseconds: 200), curve: Curves.ease);
|
||||
} else {
|
||||
final indexWhere = _pagingController.itemList!.indexWhere((element) {
|
||||
final name = element.name!;
|
||||
final firstLetter =
|
||||
name.startsWith(RegExp(r'^the', caseSensitive: false))
|
||||
? name.split(RegExp(r'^the', caseSensitive: false))[1].trim()[0]
|
||||
: name[0].toUpperCase();
|
||||
return firstLetter == letter;
|
||||
});
|
||||
|
||||
if (indexWhere >= 0) {
|
||||
final scrollTo = (indexWhere * 72).toDouble();
|
||||
await controller?.animateTo(scrollTo,
|
||||
duration: const Duration(milliseconds: 200), curve: Curves.ease);
|
||||
letterToSearch = null;
|
||||
} else {
|
||||
await controller?.animateTo(controller!.position.maxScrollExtent * 100,
|
||||
duration: const Duration(milliseconds: 200), curve: Curves.ease);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void scrollToNearbyLetter() {
|
||||
if (letterToSearch != null) {
|
||||
const standardAlphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
final closestLetterIndex = standardAlphabet.indexOf(letterToSearch!);
|
||||
if (closestLetterIndex != -1) {
|
||||
for (int offset = 0; offset <= standardAlphabet.length; offset++) {
|
||||
for (final direction in [1, -1]) {
|
||||
final nextIndex = closestLetterIndex + offset * direction;
|
||||
if (nextIndex >= 0 && nextIndex < standardAlphabet.length) {
|
||||
final nextLetter = standardAlphabet[nextIndex];
|
||||
final nextLetterIndex =
|
||||
_pagingController.itemList!.indexWhere((element) {
|
||||
final firstLetter = element.name![0].toUpperCase();
|
||||
return firstLetter == nextLetter;
|
||||
});
|
||||
|
||||
if (nextLetterIndex >= 0) {
|
||||
final scrollTo = (nextLetterIndex * 72).toDouble();
|
||||
controller?.animateTo(scrollTo,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.ease);
|
||||
letterToSearch = null;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pagingController.dispose();
|
||||
timer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
|
||||
return ValueListenableBuilder<Box<FinampSettings>>(
|
||||
valueListenable: FinampSettingsHelper.finampSettingsListener,
|
||||
builder: (context, box, _) {
|
||||
final isOffline = box.get("FinampSettings")?.isOffline ?? false;
|
||||
|
||||
if (isOffline) {
|
||||
// We do the same checks we do when online to ensure that the list is
|
||||
// not resorted when it doesn't have to be.
|
||||
if (widget.searchTerm != _lastSearch ||
|
||||
offlineSortedItems == null ||
|
||||
widget.isFavourite != _oldIsFavourite ||
|
||||
widget.sortBy != _oldSortBy ||
|
||||
widget.sortOrder != _oldSortOrder ||
|
||||
widget.view != _oldView) {
|
||||
_lastSearch = widget.searchTerm;
|
||||
_oldIsFavourite = widget.isFavourite;
|
||||
_oldSortBy = widget.sortBy;
|
||||
_oldSortOrder = widget.sortOrder;
|
||||
_oldView = widget.view;
|
||||
|
||||
DownloadsHelper downloadsHelper = GetIt.instance<DownloadsHelper>();
|
||||
|
||||
if (widget.tabContentType == TabContentType.artists) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.cloud_off,
|
||||
size: 64,
|
||||
color: Colors.white.withOpacity(0.5),
|
||||
),
|
||||
const Padding(padding: EdgeInsets.all(8.0)),
|
||||
const Text("Offline artists view hasn't been implemented")
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (widget.searchTerm == null) {
|
||||
if (widget.tabContentType == TabContentType.songs) {
|
||||
// If we're on the songs tab, just get all of the downloaded items
|
||||
offlineSortedItems = downloadsHelper.downloadedItems
|
||||
.where((element) =>
|
||||
element.viewId ==
|
||||
_finampUserHelper.currentUser!.currentViewId)
|
||||
.map((e) => e.song)
|
||||
.toList();
|
||||
} else {
|
||||
String? albumArtist = widget.albumArtist;
|
||||
offlineSortedItems = downloadsHelper.downloadedParents
|
||||
.where((element) =>
|
||||
element.item.type ==
|
||||
_includeItemTypes(widget.tabContentType) &&
|
||||
element.viewId ==
|
||||
_finampUserHelper.currentUser!.currentViewId &&
|
||||
(albumArtist == null ||
|
||||
element.item.albumArtist?.toLowerCase() ==
|
||||
albumArtist.toLowerCase()))
|
||||
.map((e) => e.item)
|
||||
.toList();
|
||||
}
|
||||
} else {
|
||||
if (widget.tabContentType == TabContentType.songs) {
|
||||
offlineSortedItems = downloadsHelper.downloadedItems
|
||||
.where(
|
||||
(element) {
|
||||
return _offlineSearch(
|
||||
item: element.song,
|
||||
searchTerm: widget.searchTerm!,
|
||||
tabContentType: widget.tabContentType);
|
||||
},
|
||||
)
|
||||
.map((e) => e.song)
|
||||
.toList();
|
||||
} else {
|
||||
offlineSortedItems = downloadsHelper.downloadedParents
|
||||
.where(
|
||||
(element) {
|
||||
return _offlineSearch(
|
||||
item: element.item,
|
||||
searchTerm: widget.searchTerm!,
|
||||
tabContentType: widget.tabContentType);
|
||||
},
|
||||
)
|
||||
.map((e) => e.item)
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
||||
offlineSortedItems!.sort((a, b) {
|
||||
// if (a.name == null || b.name == null) {
|
||||
// // Returning 0 is the same as both being the same
|
||||
// return 0;
|
||||
// } else {
|
||||
// return a.name!.compareTo(b.name!);
|
||||
// }
|
||||
if (a.nameForSorting == null || b.nameForSorting == null) {
|
||||
// Returning 0 is the same as both being the same
|
||||
return 0;
|
||||
} else {
|
||||
switch (widget.sortBy) {
|
||||
case SortBy.sortName:
|
||||
if (a.nameForSorting == null || b.nameForSorting == null) {
|
||||
// Returning 0 is the same as both being the same
|
||||
return 0;
|
||||
} else {
|
||||
return a.nameForSorting!.compareTo(b.nameForSorting!);
|
||||
}
|
||||
case SortBy.albumArtist:
|
||||
if (a.albumArtist == null || b.albumArtist == null) {
|
||||
return 0;
|
||||
} else {
|
||||
return a.albumArtist!.compareTo(b.albumArtist!);
|
||||
}
|
||||
case SortBy.communityRating:
|
||||
if (a.communityRating == null ||
|
||||
b.communityRating == null) {
|
||||
return 0;
|
||||
} else {
|
||||
return a.communityRating!.compareTo(b.communityRating!);
|
||||
}
|
||||
case SortBy.criticRating:
|
||||
if (a.criticRating == null || b.criticRating == null) {
|
||||
return 0;
|
||||
} else {
|
||||
return a.criticRating!.compareTo(b.criticRating!);
|
||||
}
|
||||
case SortBy.dateCreated:
|
||||
if (a.dateCreated == null || b.dateCreated == null) {
|
||||
return 0;
|
||||
} else {
|
||||
return a.dateCreated!.compareTo(b.dateCreated!);
|
||||
}
|
||||
case SortBy.premiereDate:
|
||||
if (a.premiereDate == null || b.premiereDate == null) {
|
||||
return 0;
|
||||
} else {
|
||||
return a.premiereDate!.compareTo(b.premiereDate!);
|
||||
}
|
||||
case SortBy.random:
|
||||
// We subtract the result by one so that we can get -1 values
|
||||
// (see comareTo documentation)
|
||||
return Random().nextInt(2) - 1;
|
||||
default:
|
||||
throw UnimplementedError(
|
||||
"Unimplemented offline sort mode ${widget.sortBy}");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (widget.sortOrder == SortOrder.descending) {
|
||||
// The above sort functions sort in ascending order, so we swap them
|
||||
// when sorting in descending order.
|
||||
offlineSortedItems = offlineSortedItems!.reversed.toList();
|
||||
}
|
||||
}
|
||||
|
||||
return Scrollbar(
|
||||
controller: controller,
|
||||
child: Stack(
|
||||
children: [
|
||||
box.get("FinampSettings")!.contentViewType ==
|
||||
ContentViewType.list
|
||||
? ListView.builder(
|
||||
keyboardDismissBehavior:
|
||||
ScrollViewKeyboardDismissBehavior.onDrag,
|
||||
itemCount: offlineSortedItems!.length,
|
||||
key: UniqueKey(),
|
||||
controller: controller,
|
||||
itemBuilder: (context, index) {
|
||||
if (widget.tabContentType == TabContentType.songs) {
|
||||
return SongListTile(
|
||||
item: offlineSortedItems![index],
|
||||
isSong: true,
|
||||
);
|
||||
} else {
|
||||
return AlbumItem(
|
||||
album: offlineSortedItems![index],
|
||||
parentType: _getParentType(),
|
||||
);
|
||||
}
|
||||
},
|
||||
)
|
||||
: GridView.builder(
|
||||
itemCount: offlineSortedItems!.length,
|
||||
keyboardDismissBehavior:
|
||||
ScrollViewKeyboardDismissBehavior.onDrag,
|
||||
controller: controller,
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: MediaQuery.of(context).size.width >
|
||||
MediaQuery.of(context).size.height
|
||||
? box
|
||||
.get("FinampSettings")!
|
||||
.contentGridViewCrossAxisCountLandscape
|
||||
: box
|
||||
.get("FinampSettings")!
|
||||
.contentGridViewCrossAxisCountPortrait,
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
if (widget.tabContentType == TabContentType.songs) {
|
||||
return SongListTile(
|
||||
item: offlineSortedItems![index],
|
||||
isSong: true,
|
||||
);
|
||||
} else {
|
||||
return AlbumItem(
|
||||
album: offlineSortedItems![index],
|
||||
parentType: _getParentType(),
|
||||
isGrid: true,
|
||||
gridAddSettingsListener: false,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
box.get("FinampSettings")!.showFastScroller &&
|
||||
widget.sortBy == SortBy.sortName
|
||||
? AlphabetList(
|
||||
callback: scrollToLetter, sortOrder: lastSortOrder)
|
||||
: const SizedBox.shrink(),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
// If the searchTerm argument is different to lastSearch, the user has changed their search input.
|
||||
// This makes albumViewFuture search again so that results with the search are shown.
|
||||
// This also means we don't redo a search unless we actaully need to.
|
||||
if (widget.searchTerm != _lastSearch ||
|
||||
_pagingController.itemList == null ||
|
||||
widget.isFavourite != _oldIsFavourite ||
|
||||
widget.sortBy != _oldSortBy ||
|
||||
widget.sortOrder != _oldSortOrder ||
|
||||
widget.view != _oldView) {
|
||||
_lastSearch = widget.searchTerm;
|
||||
_oldIsFavourite = widget.isFavourite;
|
||||
_oldSortBy = widget.sortBy;
|
||||
_oldSortOrder = widget.sortOrder;
|
||||
_oldView = widget.view;
|
||||
_pagingController.refresh();
|
||||
}
|
||||
|
||||
return RefreshIndicator(
|
||||
// RefreshIndicator wants an async function, so we use Future.sync()
|
||||
// to run refresh() inside an async function
|
||||
onRefresh: () => Future.sync(() => _pagingController.refresh()),
|
||||
child: Scrollbar(
|
||||
controller: controller,
|
||||
child: Stack(
|
||||
children: [
|
||||
box.get("FinampSettings")!.contentViewType ==
|
||||
ContentViewType.list
|
||||
? PagedListView<int, BaseItemDto>.separated(
|
||||
pagingController: _pagingController,
|
||||
scrollController: controller,
|
||||
keyboardDismissBehavior:
|
||||
ScrollViewKeyboardDismissBehavior.onDrag,
|
||||
builderDelegate:
|
||||
PagedChildBuilderDelegate<BaseItemDto>(
|
||||
itemBuilder: (context, item, index) {
|
||||
if (widget.tabContentType ==
|
||||
TabContentType.songs) {
|
||||
return SongListTile(
|
||||
item: item,
|
||||
isSong: true,
|
||||
);
|
||||
} else if (widget.tabContentType ==
|
||||
TabContentType.artists) {
|
||||
return ArtistListTile(item: item);
|
||||
} else {
|
||||
return AlbumItem(
|
||||
album: item,
|
||||
parentType: _getParentType(),
|
||||
);
|
||||
}
|
||||
},
|
||||
firstPageProgressIndicatorBuilder: (_) =>
|
||||
const FirstPageProgressIndicator(),
|
||||
newPageProgressIndicatorBuilder: (_) =>
|
||||
const NewPageProgressIndicator(),
|
||||
),
|
||||
separatorBuilder: (context, index) => SizedBox(
|
||||
height: widget.tabContentType ==
|
||||
TabContentType.artists ||
|
||||
widget.tabContentType ==
|
||||
TabContentType.genres
|
||||
? 16.0
|
||||
: 0.0,
|
||||
),
|
||||
)
|
||||
: PagedGridView(
|
||||
pagingController: _pagingController,
|
||||
keyboardDismissBehavior:
|
||||
ScrollViewKeyboardDismissBehavior.onDrag,
|
||||
scrollController: controller,
|
||||
builderDelegate:
|
||||
PagedChildBuilderDelegate<BaseItemDto>(
|
||||
itemBuilder: (context, item, index) {
|
||||
if (widget.tabContentType ==
|
||||
TabContentType.songs) {
|
||||
return SongListTile(
|
||||
item: item,
|
||||
isSong: true,
|
||||
);
|
||||
} else {
|
||||
return AlbumItem(
|
||||
album: item,
|
||||
parentType: _getParentType(),
|
||||
isGrid: true,
|
||||
gridAddSettingsListener: false,
|
||||
);
|
||||
}
|
||||
},
|
||||
firstPageProgressIndicatorBuilder: (_) =>
|
||||
const FirstPageProgressIndicator(),
|
||||
newPageProgressIndicatorBuilder: (_) =>
|
||||
const NewPageProgressIndicator(),
|
||||
),
|
||||
gridDelegate:
|
||||
SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: MediaQuery.of(context).size.width >
|
||||
MediaQuery.of(context).size.height
|
||||
? box
|
||||
.get("FinampSettings")!
|
||||
.contentGridViewCrossAxisCountLandscape
|
||||
: box
|
||||
.get("FinampSettings")!
|
||||
.contentGridViewCrossAxisCountPortrait,
|
||||
),
|
||||
),
|
||||
box.get("FinampSettings")!.showFastScroller &&
|
||||
widget.sortBy == SortBy.sortName
|
||||
? AlphabetList(
|
||||
callback: scrollToLetter, sortOrder: lastSortOrder)
|
||||
: const SizedBox.shrink(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _includeItemTypes(TabContentType tabContentType) {
|
||||
switch (tabContentType) {
|
||||
case TabContentType.songs:
|
||||
return "Audio";
|
||||
case TabContentType.albums:
|
||||
return "MusicAlbum";
|
||||
case TabContentType.artists:
|
||||
return "MusicArtist";
|
||||
case TabContentType.genres:
|
||||
return "MusicGenre";
|
||||
case TabContentType.playlists:
|
||||
return "Playlist";
|
||||
default:
|
||||
throw const FormatException("Unsupported TabContentType");
|
||||
}
|
||||
}
|
||||
|
||||
bool _offlineSearch(
|
||||
{required BaseItemDto item,
|
||||
required String searchTerm,
|
||||
required TabContentType tabContentType}) {
|
||||
late bool containsName;
|
||||
|
||||
// This horrible thing is for null safety
|
||||
if (item.name == null) {
|
||||
containsName = false;
|
||||
} else {
|
||||
containsName = item.name!.toLowerCase().contains(searchTerm.toLowerCase());
|
||||
}
|
||||
|
||||
return item.type == _includeItemTypes(tabContentType) && containsName;
|
||||
}
|
||||
Reference in New Issue
Block a user