From 7e95c167efaf44120194e8844dd60cd69360f54b Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Thu, 29 Aug 2024 17:55:35 +0800 Subject: [PATCH] :sparklesS: Able to search siblings tracks --- lib/widgets/player/sibling_tracks.dart | 235 +++++++++++++++++++------ 1 file changed, 178 insertions(+), 57 deletions(-) diff --git a/lib/widgets/player/sibling_tracks.dart b/lib/widgets/player/sibling_tracks.dart index fff5024..9c51a61 100644 --- a/lib/widgets/player/sibling_tracks.dart +++ b/lib/widgets/player/sibling_tracks.dart @@ -1,15 +1,24 @@ +import 'dart:async'; + +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:rhythm_box/providers/audio_player.dart'; +import 'package:rhythm_box/providers/user_preferences.dart'; +import 'package:rhythm_box/services/database/database.dart'; import 'package:rhythm_box/services/duration.dart'; import 'package:rhythm_box/services/server/active_sourced_track.dart'; import 'package:rhythm_box/services/sourced_track/models/source_info.dart'; +import 'package:rhythm_box/services/sourced_track/models/video_info.dart'; import 'package:rhythm_box/services/sourced_track/sourced_track.dart'; import 'package:rhythm_box/services/sourced_track/sources/piped.dart'; import 'package:rhythm_box/services/sourced_track/sources/youtube.dart'; +import 'package:rhythm_box/services/artist.dart'; +import 'package:rhythm_box/services/utils.dart'; import 'package:rhythm_box/widgets/auto_cache_image.dart'; import 'package:rhythm_box/widgets/tracks/querying_track_info.dart'; +import 'package:spotify/spotify.dart'; class SiblingTracks extends StatefulWidget { const SiblingTracks({super.key}); @@ -23,77 +32,189 @@ class _SiblingTracksState extends State { late final ActiveSourcedTrackProvider _activeSource = Get.find(); late final AudioPlayerProvider _playback = Get.find(); - get _activeTrack => + final TextEditingController _searchTermController = TextEditingController(); + + Track? get _activeTrack => _activeSource.state.value ?? _playback.state.value.activeTrack; - List get _siblings => !_query.isQueryingTrackInfo.value - ? [ - (_activeTrack as SourcedTrack).sourceInfo, - ..._activeSource.state.value!.siblings, - ] - : []; + List _siblings = List.empty(growable: true); final sourceInfoToLabelMap = { YoutubeSourceInfo: 'YouTube', PipedSourceInfo: 'Piped', }; + List? _subscriptions; + + String? _lastActiveTrackId; + + void _updateSiblings() { + _siblings = List.from( + !_query.isQueryingTrackInfo.value + ? [ + (_activeTrack as SourcedTrack).sourceInfo, + ..._activeSource.state.value!.siblings, + ] + : [], + growable: true, + ); + } + + void _updateSearchTerm() { + if (_lastActiveTrackId == _activeTrack?.id) return; + + final title = ServiceUtils.getTitle( + _activeTrack?.name ?? '', + artists: _activeTrack?.artists?.map((e) => e.name!).toList() ?? [], + onlyCleanArtist: true, + ).trim(); + + final defaultSearchTerm = + '$title - ${_activeTrack?.artists?.asString() ?? ''}'; + + _searchTermController.text = defaultSearchTerm; + } + + bool _isSearching = false; + + Future _searchSiblings() async { + if (_isSearching) return; + if (_searchTermController.text.trim().isEmpty) return; + + _siblings.clear(); + setState(() => _isSearching = true); + + final preferences = Get.find().state.value; + final searchTerm = _searchTermController.text.trim(); + + if (preferences.audioSource == AudioSource.youtube || + preferences.audioSource == AudioSource.piped) { + final resultsYt = await youtubeClient.search.search(searchTerm.trim()); + + final searchResults = await Future.wait( + resultsYt.map(YoutubeVideoInfo.fromVideo).mapIndexed((i, video) async { + final siblingType = await YoutubeSourcedTrack.toSiblingType(i, video); + return siblingType.info; + }), + ); + final activeSourceInfo = (_activeTrack! as SourcedTrack).sourceInfo; + _siblings = List.from( + searchResults + ..removeWhere((element) => element.id == activeSourceInfo.id) + ..insert( + 0, + activeSourceInfo, + ), + growable: true, + ); + } + + setState(() => _isSearching = false); + } + + @override + void initState() { + super.initState(); + _updateSearchTerm(); + _updateSiblings(); + _subscriptions = [ + _playback.state.listen((value) async { + if (value.activeTrack != null) { + _updateSearchTerm(); + _updateSiblings(); + setState(() {}); + } + }), + ]; + } + + @override + void dispose() { + _searchTermController.dispose(); + if (_subscriptions != null) { + for (final subscription in _subscriptions!) { + subscription.cancel(); + } + } + super.dispose(); + } + @override Widget build(BuildContext context) { - return Obx( - () => ListView.builder( - itemCount: _siblings.length, - itemBuilder: (context, idx) { - final item = _siblings[idx]; - final src = sourceInfoToLabelMap[item.runtimeType]; - return ListTile( - title: Text( - item.title, - overflow: TextOverflow.ellipsis, - maxLines: 1, + return Column( + children: [ + Container( + color: Theme.of(context).colorScheme.surfaceContainerHigh, + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), + child: TextField( + controller: _searchTermController, + decoration: InputDecoration( + isCollapsed: true, + border: InputBorder.none, + hintText: 'search'.tr, ), - leading: Padding( - padding: const EdgeInsets.all(8.0), - child: AutoCacheImage( - item.thumbnail, - height: 64, - width: 64, - ), - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(5), - ), - trailing: Text( - item.duration.toHumanReadableString(), - style: GoogleFonts.robotoMono(), - ), - subtitle: Row( - children: [ - if (src != null) Text(src), - Expanded( - child: Text( - ' · ${item.artist}', - maxLines: 1, - overflow: TextOverflow.ellipsis, + onSubmitted: (_) { + _searchSiblings(); + }, + ), + ), + if (_isSearching) const LinearProgressIndicator(minHeight: 3), + Expanded( + child: ListView.builder( + itemCount: _siblings.length, + itemBuilder: (context, idx) { + final item = _siblings[idx]; + final src = sourceInfoToLabelMap[item.runtimeType]; + return ListTile( + title: Text( + item.title, + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + leading: Padding( + padding: const EdgeInsets.all(8.0), + child: AutoCacheImage( + item.thumbnail, + height: 64, + width: 64, ), ), - ], - ), - enabled: !_query.isQueryingTrackInfo.value, - tileColor: !_query.isQueryingTrackInfo.value && - item.id == (_activeTrack as SourcedTrack).sourceInfo.id - ? Theme.of(context).colorScheme.secondaryContainer - : null, - onTap: () { - if (!_query.isQueryingTrackInfo.value && - item.id != (_activeTrack as SourcedTrack).sourceInfo.id) { - _activeSource.swapSibling(item); - Navigator.of(context).pop(); - } + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5), + ), + trailing: Text( + item.duration.toHumanReadableString(), + style: GoogleFonts.robotoMono(), + ), + subtitle: Row( + children: [ + if (src != null) Text(src), + Expanded( + child: Text( + ' · ${item.artist}', + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + enabled: !_query.isQueryingTrackInfo.value, + tileColor: !_query.isQueryingTrackInfo.value && + item.id == (_activeTrack as SourcedTrack).sourceInfo.id + ? Theme.of(context).colorScheme.secondaryContainer + : null, + onTap: () { + if (!_query.isQueryingTrackInfo.value && + item.id != (_activeTrack as SourcedTrack).sourceInfo.id) { + _activeSource.swapSibling(item); + Navigator.of(context).pop(); + } + }, + ); }, - ); - }, - ), + ), + ), + ], ); } }