Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions database/src/actions/mixes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -724,6 +724,8 @@ pub async fn query_mix_media_files(
cursor: usize,
page_size: usize,
) -> Result<Vec<media_files::Model>> {
log::info!("query_mix_media_files: cursor={}, page_size={}, queries={:?}", cursor, page_size, queries);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Info-level logging for every query may be excessive in production.

Consider lowering the log level or adding log sampling to prevent excessive logging if this function is called often.

Suggested implementation:

    log::debug!("query_mix_media_files: cursor={}, page_size={}, queries={:?}", cursor, page_size, queries);
    log::debug!("query_mix_media_files: returning {} media files", media_files.len());

If you want to implement log sampling (e.g., only log every Nth call), you would need to introduce a counter or use a sampling library. If you want this, let me know and I can provide a code example.


let mut all: bool = false;

let mut artist_ids: Vec<i32> = vec![];
Expand Down Expand Up @@ -1144,6 +1146,8 @@ pub async fn query_mix_media_files(
.await
.unwrap();

log::info!("query_mix_media_files: returning {} media files", media_files.len());

let sorted_files = sort_media_files(media_files, &track_ids);

Ok(sorted_files)
Expand Down
49 changes: 41 additions & 8 deletions lib/screens/query_tracks/query_tracks_list.dart
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,45 @@ class QueryTrackListViewState extends State<QueryTrackListView> {
}

Future<void> _initializeData() async {
// Pre-load first page before marking as initialized
await _loadPageForInitialization(0);
setState(() {
_isInitialized = true;
});
// Pre-load first page
_loadPage(0);
}
Comment on lines 50 to +56
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (performance): Blocking initialization on data load may impact perceived performance.

Initializing the UI before loading data and displaying a loading indicator can enhance responsiveness, especially on slow networks.

Suggested change
Future<void> _initializeData() async {
// Pre-load first page before marking as initialized
await _loadPageForInitialization(0);
setState(() {
_isInitialized = true;
});
// Pre-load first page
_loadPage(0);
}
Future<void> _initializeData() async {
// Mark as initialized before loading data
setState(() {
_isInitialized = true;
});
// Start loading first page, but don't block initialization
_loadPageForInitialization(0);
}


Future<void> _loadPageForInitialization(int cursor) async {
if (_loadingIndices.contains(cursor) || _reachedEnd) return;

_loadingIndices.add(cursor);

try {
final newItems = await queryMixTracks(widget.queries, cursor, _pageSize);

if (!mounted) return;

for (var i = 0; i < newItems.length; i++) {
_loadedItems[cursor + i] = newItems[i];
}
_loadingIndices.remove(cursor);

// Check if we've reached the end
if (newItems.length < _pageSize) {
_reachedEnd = true;
_totalCount = cursor + newItems.length;
} else {
final loadedCount = cursor + newItems.length;
_totalCount = loadedCount + _pageSize;
}

Timer(
Duration(milliseconds: gridAnimationDelay),
() => widget.layoutManager.playAnimations(),
);
} catch (error) {
if (!mounted) return;
_loadingIndices.remove(cursor);
}
}

Future<void> _loadPage(int cursor) async {
Expand Down Expand Up @@ -80,16 +114,15 @@ class QueryTrackListViewState extends State<QueryTrackListView> {
}
_loadingIndices.remove(cursor);

// Update total count
final newTotal = cursor + newItems.length;
if (newTotal > _totalCount) {
_totalCount = newTotal;
}

// Check if we've reached the end
if (newItems.length < _pageSize) {
_reachedEnd = true;
_totalCount = cursor + newItems.length;
} else {
// If we haven't reached the end, assume there's at least one more page
// This allows the UI to scroll and trigger loading the next page
final loadedCount = cursor + newItems.length;
_totalCount = loadedCount + _pageSize;
}
});
});
Expand Down
5 changes: 4 additions & 1 deletion lib/utils/api/search_collection_summary.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ import '../../bindings/bindings.dart';

Future<List<(int, String)>> fetchCollectionSummary(
CollectionType collectionType) async {
SearchCollectionSummaryRequest(n: 50).sendSignalToRust();
SearchCollectionSummaryRequest(
collectionType: collectionType,
n: 50,
).sendSignalToRust();

return (await SearchCollectionSummaryResponse.rustSignalStream.first)
.message
Expand Down
116 changes: 101 additions & 15 deletions lib/utils/dialogs/mix/mix_studio_dialog.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'dart:async';

import 'package:provider/provider.dart';
import 'package:fluent_ui/fluent_ui.dart';

Expand All @@ -22,7 +24,6 @@ import '../../api/fetch_mix_queries_by_mix_id.dart';
import '../../dialogs/mix/widgets/mix_editor.dart';
import '../../dialogs/mix/utils/mix_editor_data.dart';
import '../../dialogs/mix/widgets/mix_editor_controller.dart';
import '../../chip_input/search_task.dart';

import '../unavailable_dialog_on_band.dart';

Expand Down Expand Up @@ -69,34 +70,113 @@ class _MixStudioDialogImplementationState
extends State<MixStudioDialogImplementation> {
late final _controller = MixEditorController();
final _layoutManager = StartScreenLayoutManager();
final _searchManager = SearchTask<InternalMediaFile, List<(String, String)>>(
notifyWhenStateChange: false,
searchDelegate: (x) => queryMixTracks(QueryList(x)),
);
final _scrollController = ScrollController();

bool isLoading = false;
String _query = '';
int _cursor = 0;
bool _isLoadingMore = false;
bool _hasMore = true;
static const int _pageSize = 100;
List<InternalMediaFile> _allTracks = [];
Timer? _debounceTimer;
bool _isInitialLoad = true;

@override
void initState() {
super.initState();

_scrollController.addListener(_onScroll);

_controller.addListener(() {
// Skip the first listener call during loadMix
if (_isInitialLoad) {
_isInitialLoad = false;
return;
}
_debouncedResetAndLoad();
});

if (widget.mixId != null) {
loadMix(widget.mixId!);
}
}

_controller.addListener(() {
_layoutManager.resetAnimations();
_searchManager.search(mixEditorDataToQuery(_controller.getData()));
void _debouncedResetAndLoad() {
_debounceTimer?.cancel();
_debounceTimer = Timer(const Duration(milliseconds: 300), () {
_resetAndLoadTracks();
});
_searchManager.addListener(() {
setState(() {
final query = mixEditorDataToQuery(_controller.getData());
}

_query = query.map((x) => '$x').join(';');
void _onScroll() {
if (_scrollController.position.pixels >=
_scrollController.position.maxScrollExtent - 200 &&
!_isLoadingMore &&
_hasMore) {
_loadMoreTracks();
}
}

Future<void> _resetAndLoadTracks() async {
_layoutManager.resetAnimations();

// Don't clear the tracks immediately to avoid flickering
// Instead, load new data first, then replace
final query = mixEditorDataToQuery(_controller.getData());
final newQuery = query.map((x) => '$x').join(';');

try {
final newTracks = await queryMixTracks(QueryList(query), 0, _pageSize);

setState(() {
_query = newQuery;
_cursor = newTracks.length;
_hasMore = newTracks.length >= _pageSize;
_allTracks = newTracks;
_isLoadingMore = false;
});

// Reset scroll position to top for new results
if (_scrollController.hasClients) {
_scrollController.jumpTo(0);
}

_layoutManager.playAnimations();
} catch (e) {
setState(() {
_query = newQuery;
_cursor = 0;
_hasMore = false;
_allTracks = [];
_isLoadingMore = false;
});
}
}

Future<void> _loadMoreTracks() async {
if (_isLoadingMore || !_hasMore) return;

setState(() {
_isLoadingMore = true;
});

try {
final query = mixEditorDataToQuery(_controller.getData());
final newTracks = await queryMixTracks(QueryList(query), _cursor, _pageSize);

setState(() {
_allTracks.addAll(newTracks);
_cursor += newTracks.length;
_hasMore = newTracks.length >= _pageSize;
_isLoadingMore = false;
});
} catch (e) {
setState(() {
_isLoadingMore = false;
_hasMore = false;
});
}
}

Future<void> loadMix(int mixId) async {
Expand All @@ -106,11 +186,17 @@ class _MixStudioDialogImplementationState
final queryData = await queryToMixEditorData(mix.name, mix.group, queries);

_controller.setData(queryData);

// Load initial tracks after setting the controller data
await _resetAndLoadTracks();

setState(() {});
}

@override
void dispose() {
_debounceTimer?.cancel();
_scrollController.dispose();
_controller.dispose();
_layoutManager.dispose();
super.dispose();
Expand Down Expand Up @@ -218,20 +304,20 @@ class _MixStudioDialogImplementationState
(cellSize + gapSize))
.floor();

final trackIds = _searchManager.searchResults
final trackIds = _allTracks
.map((x) => x.id)
.toList();

return GridView(
key: Key(_query),
controller: _scrollController,
gridDelegate:
SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: rows,
mainAxisSpacing: gapSize,
crossAxisSpacing: gapSize,
childAspectRatio: ratio,
),
children: _searchManager.searchResults
children: _allTracks
.map(
(a) => TrackSearchItem(
index: 0,
Expand Down
5 changes: 4 additions & 1 deletion native/hub/src/backends/local/gui_request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,10 @@ macro_rules! handle_single_gui_event {
handle_response!(_response, $response_type);
}
Err(e) => {
error!("{e:?}");
error!("Request {} failed: {:?}", stringify!($request), e);
error!("Full error chain for {}: {:#}", stringify!($request), e);
let backtrace = e.backtrace();
error!("Backtrace for {}: {:?}", stringify!($request), backtrace);
CrashResponse {
detail: format!("{e:#?}"),
}
Expand Down
18 changes: 17 additions & 1 deletion native/hub/src/handlers/collection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -544,6 +544,12 @@ impl Signal for SearchCollectionSummaryRequest {
_session: Option<Session>,
dart_signal: &Self,
) -> Result<Option<Self::Response>> {
log::debug!(
"SearchCollectionSummaryRequest: collection_type={:?}, n={}",
dart_signal.collection_type,
dart_signal.n
);

let params = CollectionActionParams {
n: Some(dart_signal.n.try_into()?),
..Default::default()
Expand All @@ -556,7 +562,17 @@ impl Signal for SearchCollectionSummaryRequest {
handle_search::<playlists::Model>(&main_db, params).await
}
Some(CollectionType::Mix) => handle_search::<mixes::Model>(&main_db, params).await,
_ => Err(anyhow::anyhow!("Invalid collection type")),
Some(CollectionType::Genre) => handle_search::<genres::Model>(&main_db, params).await,
_ => {
log::error!(
"SearchCollectionSummaryRequest: Invalid or unsupported collection_type={:?}",
dart_signal.collection_type
);
Err(anyhow::anyhow!(
"Invalid collection type: {:?}",
dart_signal.collection_type
))
}
}
}
}
21 changes: 18 additions & 3 deletions native/hub/src/server/client/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,12 @@ pub async fn build_query(
id: i32,
connection: &WSConnection,
) -> Result<Vec<(String, String)>> {
log::debug!("build_query: collection_type={:?}, id={}", collection_type, id);

if collection_type == CollectionType::Mix {
log::debug!("build_query: Fetching mix queries for mix_id={}", id);
let queries = fetch_mix_queries_by_mix_id(id, connection).await?;
log::debug!("build_query: Got {} mix queries", queries.len());
Ok(queries
.into_iter()
.map(|q| (q.operator, q.parameter))
Expand Down Expand Up @@ -55,15 +59,26 @@ pub fn build_collection_query(
}

pub fn path_to_collection_type(path: &Path) -> Option<CollectionType> {
match path.components().nth(1)?.as_os_str().to_str()? {
let component = path.components().nth(1)?;
let component_str = component.as_os_str().to_str()?;

log::debug!("path_to_collection_type: path={:?}, component={}", path, component_str);

let result = match component_str {
"Albums" => Some(CollectionType::Album),
"Artists" => Some(CollectionType::Artist),
"Playlists" => Some(CollectionType::Playlist),
"Mixes" => Some(CollectionType::Mix),
"Tracks" => Some(CollectionType::Track),
"Genres" => Some(CollectionType::Genre),
_ => None,
}
_ => {
log::warn!("path_to_collection_type: Unknown collection type '{}' from path {:?}", component_str, path);
None
}
};

log::debug!("path_to_collection_type: result={:?}", result);
result
}

pub async fn fetch_collection_group_summary(
Expand Down
Loading
Loading