@@ -1783,7 +1783,11 @@ impl App {
17831783 // ── Remote control ──────────────────────────────────────
17841784 if self . http_remote_running {
17851785 self . update_remote_state ( ) ;
1786- self . poll_remote_commands ( ) ;
1786+ let t = self . poll_remote_commands ( ) ;
1787+ // A returned task means the remote enqueued an op
1788+ // that needs to re-enter update() (Surprise / load /
1789+ // restore). Fire it before we return.
1790+ return t;
17871791 }
17881792 }
17891793
@@ -3928,6 +3932,23 @@ impl App {
39283932 fn update_remote_state ( & self ) {
39293933 if let Ok ( mut rs) = self . remote_state . try_lock ( ) {
39303934 let info = self . status . track_info . as_ref ( ) ;
3935+
3936+ let current_md5 = self . playlist . current_entry ( ) . and_then ( |e| e. md5 . as_deref ( ) ) ;
3937+ let is_favorite_current = current_md5
3938+ . map ( |m| self . favorites . hashes . contains ( m) )
3939+ . unwrap_or ( false ) ;
3940+
3941+ // Sleep timer countdown — seconds until deadline. `None` when
3942+ // no timer is armed.
3943+ let sleep_remaining_secs = self
3944+ . sleep_deadline
3945+ . map ( |dl| dl. saturating_duration_since ( Instant :: now ( ) ) . as_secs ( ) as u32 ) ;
3946+
3947+ let active_published_playlist = match & self . session_mode {
3948+ SessionMode :: PublishedReadOnly { file } => Some ( file. clone ( ) ) ,
3949+ _ => None ,
3950+ } ;
3951+
39313952 rs. status = remote:: RemoteStatus {
39323953 state : match self . status . state {
39333954 PlayState :: Playing => "playing" ,
@@ -3951,11 +3972,33 @@ impl App {
39513972 sid_type : info. map ( |i| i. sid_type . clone ( ) ) . unwrap_or_default ( ) ,
39523973 is_pal : info. map ( |i| i. is_pal ) . unwrap_or ( true ) ,
39533974 engine : self . config . output_engine . clone ( ) ,
3975+ is_favorite : is_favorite_current,
3976+ master_volume : self . config . master_volume ,
3977+ shuffle : self . playlist . shuffle ,
3978+ repeat : match self . playlist . repeat {
3979+ playlist:: RepeatMode :: Off => "off" ,
3980+ playlist:: RepeatMode :: All => "all" ,
3981+ playlist:: RepeatMode :: Single => "one" ,
3982+ }
3983+ . to_string ( ) ,
3984+ sleep_selected_mins : self . sleep_selected_mins ,
3985+ sleep_remaining_secs,
3986+ hvsc_sync_active : self . hvsc_sync . is_some ( ) ,
3987+ hvsc_sync_progress : self . hvsc_sync_progress . map ( |( done, total) | [ done, total] ) ,
3988+ active_published_playlist,
39543989 } ;
39553990
3956- // Only rebuild playlist snapshot when entries change.
3957- let version = self . playlist . len ( ) as u64 ;
3991+ // Snapshot hvsc_root + published manifest for the library
3992+ // browse endpoints on the HTTP thread.
3993+ rs. hvsc_root = self . config . hvsc_root . clone ( ) . map ( PathBuf :: from) ;
3994+ rs. published_manifest = self . published_playlists_browser . manifest ( ) . cloned ( ) ;
3995+
3996+ // Rebuild playlist snapshot when entries OR favourites change.
3997+ // We stuff both into one epoch so the version check catches both.
3998+ let favs_epoch = self . favorites . hashes . len ( ) as u64 ;
3999+ let version = ( ( self . playlist . len ( ) as u64 ) << 32 ) | favs_epoch;
39584000 if rs. playlist_version != version {
4001+ let fav_set = & self . favorites . hashes ;
39594002 rs. playlist = self
39604003 . playlist
39614004 . entries
@@ -3968,6 +4011,11 @@ impl App {
39684011 duration : e. duration_secs ,
39694012 num_sids : e. num_sids ,
39704013 is_rsid : e. is_rsid ,
4014+ is_favorite : e
4015+ . md5
4016+ . as_deref ( )
4017+ . map ( |m| fav_set. contains ( m) )
4018+ . unwrap_or ( false ) ,
39714019 } )
39724020 . collect ( ) ;
39734021 rs. playlist_version = version;
@@ -3976,7 +4024,14 @@ impl App {
39764024 }
39774025
39784026 /// Process commands from the HTTP remote control server.
3979- fn poll_remote_commands ( & mut self ) {
4027+ ///
4028+ /// Returns a `Task<Message>` batch so commands that map to a full
4029+ /// `Message::…` handler (Surprise / LoadPublishedPlaylist / etc.)
4030+ /// can trigger the same code path the desktop UI uses. Simple ops
4031+ /// (favourites toggle, shuffle, volume) are executed in-line and
4032+ /// contribute `Task::none()` to the batch.
4033+ fn poll_remote_commands ( & mut self ) -> Task < Message > {
4034+ let mut tasks: Vec < Task < Message > > = Vec :: new ( ) ;
39804035 while let Ok ( cmd) = self . remote_cmd_rx . try_recv ( ) {
39814036 match cmd {
39824037 remote:: RemoteCmd :: PlayTrack ( idx) => {
@@ -4006,6 +4061,115 @@ impl App {
40064061 let _ = self . cmd_tx . send ( PlayerCmd :: SetSubtune ( n) ) ;
40074062 self . clear_advance_status ( ) ;
40084063 }
4064+
4065+ // ── Playback QOL (in-line, no Task needed) ───────────
4066+ remote:: RemoteCmd :: ToggleFavorite ( idx) => {
4067+ if let Some ( md5) = self . playlist . entries . get ( idx) . and_then ( |e| e. md5 . clone ( ) ) {
4068+ let _ = self . favorites . toggle ( & md5) ;
4069+ self . favorites . save ( ) ;
4070+ }
4071+ }
4072+ remote:: RemoteCmd :: ToggleFavoriteCurrent => {
4073+ let md5 = self . playlist . current_entry ( ) . and_then ( |e| e. md5 . clone ( ) ) ;
4074+ if let Some ( md5) = md5 {
4075+ let _ = self . favorites . toggle ( & md5) ;
4076+ self . favorites . save ( ) ;
4077+ }
4078+ }
4079+ remote:: RemoteCmd :: ToggleShuffle => {
4080+ self . playlist . toggle_shuffle ( ) ;
4081+ }
4082+ remote:: RemoteCmd :: CycleRepeat => {
4083+ self . playlist . repeat = self . playlist . repeat . cycle ( ) ;
4084+ }
4085+ remote:: RemoteCmd :: SetSleepTimer ( mins) => match mins {
4086+ Some ( m) if m > 0 => {
4087+ self . sleep_deadline =
4088+ Some ( Instant :: now ( ) + Duration :: from_secs ( ( m as u64 ) * 60 ) ) ;
4089+ self . sleep_selected_mins = Some ( m) ;
4090+ }
4091+ _ => {
4092+ self . sleep_deadline = None ;
4093+ self . sleep_selected_mins = None ;
4094+ }
4095+ } ,
4096+ remote:: RemoteCmd :: SetVolume ( v) => {
4097+ self . config . master_volume = v. clamp ( 0.0 , 1.0 ) ;
4098+ self . config . save ( ) ;
4099+ }
4100+
4101+ // ── Ops that reuse full Message handlers via Task::done
4102+ remote:: RemoteCmd :: Surprise => {
4103+ tasks. push ( Task :: done ( Message :: HvscBrowserSurpriseMe ) ) ;
4104+ }
4105+ remote:: RemoteCmd :: LoadPublishedPlaylist ( file) => {
4106+ tasks. push ( Task :: done ( Message :: PublishedPlaylistsLoad ( file) ) ) ;
4107+ }
4108+ remote:: RemoteCmd :: RestoreDefaultPlaylist => {
4109+ tasks. push ( Task :: done ( Message :: PublishedPlaylistsRestoreDefault ) ) ;
4110+ }
4111+
4112+ // ── HVSC direct play / add by absolute path ──────────
4113+ // Reuses the same PlaylistEntry::from_path + add_entries +
4114+ // songlength chain as the HvscBrowserPlayTune handler.
4115+ remote:: RemoteCmd :: HvscPlay ( path) => {
4116+ self . direct_hvsc_action ( path, /*play=*/ true ) ;
4117+ }
4118+ remote:: RemoteCmd :: HvscAdd ( path) => {
4119+ self . direct_hvsc_action ( path, /*play=*/ false ) ;
4120+ }
4121+ }
4122+ }
4123+ if tasks. is_empty ( ) {
4124+ Task :: none ( )
4125+ } else {
4126+ Task :: batch ( tasks)
4127+ }
4128+ }
4129+
4130+ /// Realise a single SID at an absolute path (typically inside the
4131+ /// HVSC tree, but any path works), add it to the playlist, apply
4132+ /// songlengths, and optionally start playback. Shared by the two
4133+ /// remote `HvscPlay` / `HvscAdd` commands.
4134+ fn direct_hvsc_action ( & mut self , path : PathBuf , play : bool ) {
4135+ let entry = match playlist:: PlaylistEntry :: from_path ( & path) {
4136+ Ok ( e) => e,
4137+ Err ( e) => {
4138+ eprintln ! ( "[remote] HVSC direct action failed: {e}" ) ;
4139+ return ;
4140+ }
4141+ } ;
4142+ let entry_path = entry. path . clone ( ) ;
4143+ let song = entry. selected_song . max ( 1 ) ;
4144+ self . playlist . add_entries ( vec ! [ entry] ) ;
4145+ if let Some ( db) = self . songlength_db . as_ref ( ) {
4146+ db. apply_to_playlist (
4147+ & mut self . playlist ,
4148+ self . config . hvsc_root . as_deref ( ) . map ( std:: path:: Path :: new) ,
4149+ ) ;
4150+ }
4151+ self . rebuild_filter ( ) ;
4152+ if let Some ( abs_i) = self
4153+ . playlist
4154+ . entries
4155+ . iter ( )
4156+ . position ( |e| e. path == entry_path)
4157+ {
4158+ self . selected = Some ( abs_i) ;
4159+ if play {
4160+ let _ = self . cmd_tx . send ( player:: PlayerCmd :: Play {
4161+ path : entry_path,
4162+ song,
4163+ force_stereo : self . config . force_stereo_2sid
4164+ || std:: env:: args ( ) . any ( |a| a == "--stereo" ) ,
4165+ sid4_addr : 0xd420 ,
4166+ audio_port : if self . config . u64_audio_enabled {
4167+ Some ( self . config . u64_audio_port )
4168+ } else {
4169+ None
4170+ } ,
4171+ restart_usb_on_load : self . config . restart_usb_on_load ,
4172+ } ) ;
40094173 }
40104174 }
40114175 }
0 commit comments