feat: add search filter bar for extension custom search

- Add SearchFilter struct in Go backend and Dart
- Add filters array to SearchBehaviorConfig manifest
- Add selectedSearchFilter state to TrackProvider
- Add filter bar UI with FilterChips below search bar
- Filter bar only shows when search results exist or loading
- Preserve selectedSearchFilter during customSearch loading
- Pass filter option to extension customSearch
This commit is contained in:
zarzet
2026-01-31 15:10:18 +07:00
parent 831b68b6cc
commit d0bc3b203c
4 changed files with 194 additions and 10 deletions
+25
View File
@@ -146,6 +146,26 @@ class Extension {
bool get hasBrowseCategories => capabilities['browseCategories'] == true;
}
class SearchFilter {
final String id;
final String? label;
final String? icon;
const SearchFilter({
required this.id,
this.label,
this.icon,
});
factory SearchFilter.fromJson(Map<String, dynamic> json) {
return SearchFilter(
id: json['id'] as String? ?? '',
label: json['label'] as String?,
icon: json['icon'] as String?,
);
}
}
class SearchBehavior {
final bool enabled;
final String? placeholder;
@@ -154,6 +174,7 @@ class SearchBehavior {
final String? thumbnailRatio; // "square" (1:1), "wide" (16:9), "portrait" (2:3)
final int? thumbnailWidth;
final int? thumbnailHeight;
final List<SearchFilter> filters; // Available search filters (e.g., track, album, artist, playlist)
const SearchBehavior({
required this.enabled,
@@ -163,6 +184,7 @@ class SearchBehavior {
this.thumbnailRatio,
this.thumbnailWidth,
this.thumbnailHeight,
this.filters = const [],
});
factory SearchBehavior.fromJson(Map<String, dynamic> json) {
@@ -174,6 +196,9 @@ class SearchBehavior {
thumbnailRatio: json['thumbnailRatio'] as String?,
thumbnailWidth: json['thumbnailWidth'] as int?,
thumbnailHeight: json['thumbnailHeight'] as int?,
filters: (json['filters'] as List<dynamic>?)
?.map((f) => SearchFilter.fromJson(f as Map<String, dynamic>))
.toList() ?? [],
);
}
+20 -1
View File
@@ -25,6 +25,7 @@ class TrackState {
final bool hasSearchText; // For back button handling
final bool isShowingRecentAccess; // For recent access mode
final String? searchExtensionId; // Extension ID used for current search results
final String? selectedSearchFilter; // Currently selected search filter (e.g., "track", "album", "artist", "playlist")
const TrackState({
this.tracks = const [],
@@ -44,6 +45,7 @@ class TrackState {
this.hasSearchText = false,
this.isShowingRecentAccess = false,
this.searchExtensionId,
this.selectedSearchFilter,
});
bool get hasContent => tracks.isNotEmpty || artistAlbums != null || (searchArtists != null && searchArtists!.isNotEmpty);
@@ -66,6 +68,8 @@ class TrackState {
bool? hasSearchText,
bool? isShowingRecentAccess,
String? searchExtensionId,
String? selectedSearchFilter,
bool clearSelectedSearchFilter = false,
}) {
return TrackState(
tracks: tracks ?? this.tracks,
@@ -85,6 +89,7 @@ class TrackState {
hasSearchText: hasSearchText ?? this.hasSearchText,
isShowingRecentAccess: isShowingRecentAccess ?? this.isShowingRecentAccess,
searchExtensionId: searchExtensionId,
selectedSearchFilter: clearSelectedSearchFilter ? null : (selectedSearchFilter ?? this.selectedSearchFilter),
);
}
}
@@ -391,7 +396,11 @@ class TrackNotifier extends Notifier<TrackState> {
Future<void> customSearch(String extensionId, String query, {Map<String, dynamic>? options}) async {
final requestId = ++_currentRequestId;
state = TrackState(isLoading: true, hasSearchText: state.hasSearchText);
state = TrackState(
isLoading: true,
hasSearchText: state.hasSearchText,
selectedSearchFilter: state.selectedSearchFilter, // Preserve filter during loading
);
try {
_log.i('Custom search started: extension=$extensionId, query="$query"');
@@ -423,6 +432,7 @@ class TrackNotifier extends Notifier<TrackState> {
isLoading: false,
hasSearchText: state.hasSearchText,
searchExtensionId: extensionId, // Store which extension was used
selectedSearchFilter: state.selectedSearchFilter, // Preserve selected filter
);
} catch (e, stackTrace) {
if (!_isRequestValid(requestId)) return;
@@ -474,6 +484,15 @@ class TrackNotifier extends Notifier<TrackState> {
state = const TrackState();
}
/// Set selected search filter for extension search
void setSearchFilter(String? filter) {
if (state.selectedSearchFilter == filter) return;
state = state.copyWith(
selectedSearchFilter: filter,
clearSelectedSearchFilter: filter == null,
);
}
/// Set search text state for back button handling
void setSearchText(bool hasText) {
if (state.hasSearchText == hasText) {