Keep compact music status clear of seek and playback controls

This commit is contained in:
liu weikai committed 2026-10-16 12:47:43 +08:00
1 parent 2d2a9e8973
commit e73fa53bee
6 files changed
+46 -5

No files matched your search

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

+8 -2
View File
@@ -8,7 +8,9 @@ are never deleted by a playlist operation.
Now Playing includes embedded artwork, title, artist, album, elapsed/total time,
seek, previous/pause/next and repeat-all/repeat-one/shuffle. The artwork frame and
seek knob have disjoint bounds on every supported screen. The track-information
seek knob have disjoint bounds on every supported screen. Compact screens place
a single-line paused/interrupted status between the time labels, outside the
slider knob and playback controls. Longer status text elides. The track-information
view pages through six ID3 fields at a time and keeps the cover visible. Lyrics
use three lines with synchronized LRC selection or manual scrolling.
@@ -76,7 +78,7 @@ write flash. A slider owns its whole drag and cannot page the dashboard.
| Control | Owner and transition |
| --- | --- |
| Wi-Fi | Preserve saved profiles and priority. Coordinate transport clients before stopping the driver. Reject changes during OTA, calls, non-preemptible operations or external-storage suspension. Roll back a rejected preference; preserve initialization state after failed driver cleanup so retry is possible. |
| LoRa | A shared RF-permission policy is separate from mesh-protocol TX/RX configuration and temporary exclusive sessions. Reject a switch during an exclusive session. Reject new TX while disabled, finish current work and enter checked standby before acknowledging Off. Re-enable through the radio task; an uninitialized disabled radio can still be enabled. Preserve shared power rails. |
| LoRa | A shared RF-permission policy is separate from mesh-protocol TX/RX configuration and temporary exclusive sessions. Reject a switch during an exclusive session. Reject new TX while disabled, finish current work and enter checked standby before acknowledging Off. The control accepts an enable request while offline; the radio task reports transition failures instead of claiming hardware success. Preserve shared power rails. |
| GPS | Use the existing configuration edit lease, GPS runtime enable operation and position projection. The user preference is separate from normal duty-cycle sleep and temporary power leases. |
| Volume | Use the shared music/speaker volume. An idle adjustment updates the cached preference without creating a decoder, opening SD or starting a worker. Active playback defers flash persistence until audio is idle. |
| Brightness | Map the percentage to the target's actual brightness range, apply through its device port and persist that native level. |
@@ -133,6 +135,10 @@ preview services. Top-bar memory values and media metadata are fixture data.
![Pager Now Playing with cover, seek and playback modes](../../images/music/pager-now-playing.png)
![Pager paused status clear of the progress knob](../../images/music/pager-paused.png)
![T-Deck interrupted status stays on one line above the controls](../../images/music/tdeck-interrupted.png)
![Pager Quick controls without the empty footer](../../images/music/pager-quick-controls.png)
![P4 Music widget in the right dashboard](../../images/music/p4-music-widget.png)
@@ -139,6 +139,7 @@ void now_playing(lv_coord_t width, lv_coord_t height)
self.volume = label(panel, volume, w - 54, title_y + 46, 40, &lv_font_montserrat_12, true);
self.playback_status = label(panel, "", text_x, title_y + 64, text_w, &lv_font_montserrat_12, true);
lv_label_set_long_mode(self.playback_status, LV_LABEL_LONG_DOT);
lv_obj_set_height(self.playback_status, lv_font_get_line_height(&lv_font_montserrat_12));
const lv_coord_t controls_y = h - 44;
const lv_coord_t progress_y = controls_y - 34;
self.progress = lv_slider_create(panel);
@@ -154,6 +155,14 @@ void now_playing(lv_coord_t width, lv_coord_t height)
self.elapsed = label(panel, "0:00", 14, progress_y + 12, 70, &lv_font_montserrat_12, true);
self.duration = label(panel, "--:--", w - 84, progress_y + 12, 70, &lv_font_montserrat_12, true);
lv_obj_set_style_text_align(self.duration, LV_TEXT_ALIGN_RIGHT, 0);
if (!self.wide)
{
// Status shares the time row on compact screens, leaving the cover,
// metadata and the slider's painted knob area independent.
lv_obj_set_pos(self.playback_status, 92, progress_y + 12);
lv_obj_set_width(self.playback_status, w - 184);
lv_obj_set_style_text_align(self.playback_status, LV_TEXT_ALIGN_CENTER, 0);
}
auto* mode = button(panel, "Repeat all", PlayMode, 92);
self.mode = lv_obj_get_child(mode, 0);
auto* previous = button(panel, LV_SYMBOL_PREV, Previous, 36);
+8
View File
@@ -21,6 +21,14 @@ for(const [device,width,height] of boards.filter(([name])=>!process.env.MUSIC_UI
click('Repeat one');assert.ok(snapshot().some(item=>item.text==='Shuffle'));
click('Shuffle');assert.ok(snapshot().some(item=>item.text==='Repeat all'));
await capture('now-playing');
click('Pause');assert.ok(snapshot().some(item=>item.text==='Paused'));
assert.equal(runtime._native_music_geometry(),0,`${device}: paused status clears the progress knob and time labels`);
await capture('paused');
runtime._native_preview_music_playing(2);tick();tick();
assert.ok(snapshot().some(item=>item.text.startsWith('Paused for')),'interrupted status remains readable when elided');
assert.equal(runtime._native_music_geometry(),0,`${device}: interrupted status clears the progress knob and time labels`);
await capture('interrupted');
runtime._native_preview_music_playing(1);tick();tick();
click('Lyrics');assert.ok(snapshot().some(item=>item.text==='We take the quiet way home'));await capture('lyrics');
click('\uf053');click('Menu');click('Track info');await capture('metadata');assert.ok(snapshot().some(item=>item.text==='Aster'));
click('\uf053');click('Library');click('Browse');click('Playlists');await capture('playlists');
+21 -3
View File
@@ -306,9 +306,13 @@ class Source final : public music::IService
++out.revision;
return true;
}
void set_playing(bool value)
void set_playing(int value)
{
if (value) play(1);
if (value)
{
play(1);
if (value == 2) out.playback = music::Playback::Interrupted;
}
else out.playback = music::Playback::Stopped;
}
} source;
@@ -319,7 +323,7 @@ void preview_bind_music()
ui::menu::dashboard::bindMusicSource(&source);
}
music::IService* preview_music_service() { return &source; }
extern "C" EMSCRIPTEN_KEEPALIVE void native_preview_music_playing(int value) { source.set_playing(value != 0); }
extern "C" EMSCRIPTEN_KEEPALIVE void native_preview_music_playing(int value) { source.set_playing(value); }
// QA only: inspect the actual shared LVGL objects, including slider knob clearance.
extern "C" EMSCRIPTEN_KEEPALIVE int native_music_geometry()
{
@@ -335,5 +339,19 @@ extern "C" EMSCRIPTEN_KEEPALIVE int native_music_geometry()
const auto knob = lv_obj_get_style_pad_top(self->progress, LV_PART_KNOB) + lv_obj_get_height(self->progress);
if (cover.y2 + knob >= progress.y1) return 1;
if (top.y2 >= body.y1 || body.y2 >= footer.y1) return 2;
if (self->playback_status && lv_label_get_text(self->playback_status)[0])
{
lv_area_t status{}, elapsed{}, duration{}, controls{};
lv_obj_get_coords(self->playback_status, &status);
lv_obj_get_coords(self->elapsed, &elapsed);
lv_obj_get_coords(self->duration, &duration);
lv_obj_get_coords(lv_obj_get_parent(self->play_label), &controls);
auto painted = progress;
painted.y1 -= lv_obj_get_style_pad_top(self->progress, LV_PART_KNOB) + 1;
painted.y2 += lv_obj_get_style_pad_bottom(self->progress, LV_PART_KNOB) + 1;
const auto overlaps = [](const lv_area_t& a, const lv_area_t& b)
{ return a.x1 <= b.x2 && a.x2 >= b.x1 && a.y1 <= b.y2 && a.y2 >= b.y1; };
if (overlaps(status, painted) || overlaps(status, elapsed) || overlaps(status, duration) || overlaps(status, controls)) return 3;
}
return 0;
}