Compare commits
10 Commits
67fc6f584f
...
5b259a1ac1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5b259a1ac1 | ||
|
|
1eda52e4c5 | ||
|
|
3be7a7b194 | ||
|
|
bd80ad1ea0 | ||
|
|
c30fd9fe78 | ||
|
|
605d21a3e2 | ||
|
|
cc4fb0d5c9 | ||
|
|
af38affdbe | ||
|
|
034b3df7ae | ||
|
|
d4497b0c05 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -38,3 +38,5 @@ legacy/releases/
|
||||
|
||||
# python caches from scripts/lyrics
|
||||
__pycache__/
|
||||
plans/codex-shots*/
|
||||
plans/codex-report*.md
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
---
|
||||
target_identity: "file:/home/josh/development/personal/ytplayer/frontend/index.html"
|
||||
target_fingerprint: "sha256:5f16cc1d66caa47d5d1ae35afe92b5206825e66772d974d72fd90effac161d27"
|
||||
target_path: /home/josh/development/personal/ytplayer/frontend/index.html
|
||||
timestamp: 2026-10-05T20-14-49Z
|
||||
slug: frontend-index-html
|
||||
---
|
||||
Method: dual-agent (A: /root/critique_design · B: /root/critique_evidence)
|
||||
|
||||
# Design Health Score
|
||||
|
||||
| # | Heuristic | Score | Key issue |
|
||||
|---|---|---:|---|
|
||||
| 1 | Visibility of System Status | 3/4 | Active tab and playback state are clear; row actions depend on an undisclosed swipe. |
|
||||
| 2 | Match System / Real World | 3/4 | Grouped lists, bottom tabs and sheet use familiar iOS patterns. |
|
||||
| 3 | User Control and Freedom | 3/4 | Player and sheets can be collapsed or closed; long swipe removal remains a high consequence gesture. |
|
||||
| 4 | Consistency and Standards | 3/4 | F7 components now share the same token-based visual treatment; some underlying CSS layers overlap. |
|
||||
| 5 | Error Prevention | 2/4 | Long-left removes a row without a confirmation step. |
|
||||
| 6 | Recognition Rather Than Recall | 2/4 | Swipe row actions are not apparent before discovery. |
|
||||
| 7 | Flexibility and Efficiency | 3/4 | Gestures and bottom tabs offer quick navigation for touch users. |
|
||||
| 8 | Aesthetic and Minimalist Design | 3/4 | Light, dark and contrast treatments stay restrained; row actions can obscure content while open. |
|
||||
| 9 | Error Recovery | 2/4 | No explicit undo is provided for a swipe removal. |
|
||||
| 10 | Help and Documentation | 1/4 | Only the mini-player has a visual gesture affordance; row and tab gestures lack a persistent hint. |
|
||||
| **Total** | | **25/40** | **Acceptable; interaction discovery is the main gap.** |
|
||||
|
||||
# Design Specificity Verdict
|
||||
|
||||
**LLM assessment:** The F7 layout now reads as a media player: native line icons, an inset media list, accent-led controls, glass navigation and a sheet for Up Next. The large title hierarchy and player/list relationship fit the product. It is no longer just the mini-player that changes. The remaining weakest point is that row actions and tab movement rely on gesture knowledge users may not have.
|
||||
|
||||
**Deterministic scan:** The scoped Impeccable detector scan across `frontend/index.html`, `frontend/app.js`, `frontend/f7-layout.js`, `frontend/fullscreen-orientation.css`, `frontend/fullscreen-orientation.js` and `frontend/theme-f7.css` reported 129 findings: 60 warnings and 69 advisories. Categories: low contrast 22, design-system-radius 35, undersized-ui-text 22, design-system-font-size 20, thin-border/wide-shadow 10, dark-glow 4, design-system-color 4, clipped overflow 3, gradient text 2, layout transition 2, tiny text 2, marquee 2, pulsing dot 1. It found no issues in the two gesture/fullscreen logic modules. Most radius/type findings refer to the intended F7 control scale or circular/icon controls; many remaining findings point to shared prototype content and pre-existing theme variants in the large single HTML document. The contrast findings around bright accent fills and dim secondary text merit future contextual review, especially against the detector's assumed body-text size.
|
||||
|
||||
**Browser visualization:** The detector overlay could not be injected into the selected live page. The available browser inspection loaded the default Classic layout and returned `Unexpected token '<'` from API/script requests served as HTML, so no reliable user-visible overlay is available. Separate Playwright captures were made on the static frontend with the F7 layout enabled and inspected directly.
|
||||
|
||||
# Cognitive Load
|
||||
|
||||
The tab bar reduces destination recall to six visible choices. The list header, filter and grouped items scan predictably. Hidden row actions and swipe-to-change-tab are not discoverable from the screen itself; the small grabber only hints at mini-player movement and does not label what the movement does. No screen presents more than four primary decision points at once.
|
||||
|
||||
# Emotional Journey
|
||||
|
||||
Search and queue selection are direct, and the full-player sheet preserves the sense of continuity while browsing. Filling the screen with a real video makes fullscreen feel intentional. A long swipe that removes a row can create a negative surprise because the outcome is not previewed or recoverable.
|
||||
|
||||
# What's Working
|
||||
|
||||
- The F7 treatment now spans search, sidebar, home, list, settings, player, mini-player, tabs, sheets and auxiliary panels.
|
||||
- Fill/Fit is explicit, persisted, and also toggles by center double-tap or multi-touch.
|
||||
- The classic regression pair is pixel-identical at 390×844.
|
||||
|
||||
# Priority Issues
|
||||
|
||||
1. **[P2] Row gestures are hard to discover.** Users see an ellipsis, but do not know that left/right reveals Playlist, Remove and Queue. Add a one-time, dismissible hint or a small visible interaction cue. Suggested command: `/impeccable clarify`.
|
||||
2. **[P2] Swipe removal has weak recovery.** A long left swipe removes immediately, with no Undo affordance. Give the toast a short Undo action or reduce the destructive gesture to a normal revealed button. Suggested command: `/impeccable harden`.
|
||||
|
||||
# Persona Red Flags
|
||||
|
||||
- **Jordan (First-Timer):** Can find Search and Queue from the bottom bar, but cannot infer that a song row can be swiped to add it to a playlist or remove it.
|
||||
- **Alex (Power User):** Can move quickly through tabs and the mini-player, but a long-left removal has no quick undo if the wrong row is removed.
|
||||
- **Morgan (Accessibility-focused listener):** Touch actions have generous targets, but gestures have no equivalent hint and depend on a screen reader user discovering the menu path.
|
||||
|
||||
# Minor Observations
|
||||
|
||||
- The critique found overlapping F7 declarations in `theme-f7.css`; current precedence is predictable but the file would be easier to maintain if these layers were consolidated.
|
||||
- The queue/settings duplicate heading and the redundant settings list filter were fixed during this pass; Queue's Play action is now the filled primary action.
|
||||
- The section rail remains usable as a compact floating control above the tab bar.
|
||||
- The detector's font-size advisories in compact badges and section labels are intentional density choices; check any future increase in text scale against them.
|
||||
|
||||
# Questions to Consider
|
||||
|
||||
- Would a single dismissible swipe hint be enough, or should Queue/Playlist/Remove stay visible behind each row menu?
|
||||
- Should long-left remove remain as a shortcut if the toast gains Undo, or should removal require tapping the revealed action?
|
||||
904
docs/design/f7-swipe.json
Normal file
904
docs/design/f7-swipe.json
Normal file
@@ -0,0 +1,904 @@
|
||||
{
|
||||
"title": "YT Player \u2014 Framework7 Swipe",
|
||||
"brief": "Ad-free YouTube/worship player PWA. Framework7 iOS-style layout with swipe gestures, built on the app's own tokens.",
|
||||
"frame": "phone",
|
||||
"platform": "web",
|
||||
"paletteKey": "coral",
|
||||
"theme": {
|
||||
"dark": true,
|
||||
"bothModes": true,
|
||||
"contrast": "standard",
|
||||
"shape": "rounded",
|
||||
"font": "system",
|
||||
"emphasized": false,
|
||||
"motion": "expressive"
|
||||
},
|
||||
"frames": [
|
||||
{
|
||||
"id": "search",
|
||||
"name": "Search",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"note": "Home + search results. Big title, rounded search field, results as an inset grouped list.",
|
||||
"swipe": {
|
||||
"left": "queue"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "queue",
|
||||
"name": "Queue",
|
||||
"x": 492,
|
||||
"y": 0,
|
||||
"note": "Queued songs. Swipe a row left for Playlist/Remove (long swipe removes), right for Queue. Swipe sideways on empty space to change tab.",
|
||||
"swipe": {
|
||||
"right": "search"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "player",
|
||||
"name": "Now playing",
|
||||
"x": 984,
|
||||
"y": 0,
|
||||
"note": "Video on top, Apple Music\u2013style transport, icon action row. Swipe down to collapse to the mini player.",
|
||||
"swipe": {
|
||||
"down": "queue"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "upnext",
|
||||
"name": "Up next",
|
||||
"x": 1476,
|
||||
"y": 0,
|
||||
"note": "Bottom sheet opened by swiping up on the mini player. Half height, drag up for full, down to close."
|
||||
},
|
||||
{
|
||||
"id": "fullscreen",
|
||||
"name": "Fullscreen",
|
||||
"x": 1968,
|
||||
"y": 0,
|
||||
"note": "Edge-to-edge video (Fill by default). Minimal icon-only controls over a soft bottom gradient; auto-hide.",
|
||||
"bg": "inverseSurface"
|
||||
}
|
||||
],
|
||||
"groups": [
|
||||
{
|
||||
"id": "g2",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"axis": "x",
|
||||
"items": [
|
||||
{
|
||||
"id": "i1",
|
||||
"kind": "topAppBar",
|
||||
"label": "Search",
|
||||
"icon": "menu",
|
||||
"variant": "filled",
|
||||
"icon2": "directions_car",
|
||||
"note": "Translucent navbar; menu opens the sidebar (also left-edge swipe)."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "g4",
|
||||
"x": 16,
|
||||
"y": 96,
|
||||
"axis": "x",
|
||||
"items": [
|
||||
{
|
||||
"id": "i3",
|
||||
"kind": "searchBar",
|
||||
"label": "Search YouTube",
|
||||
"icon": null,
|
||||
"variant": "filled",
|
||||
"icon2": "mic",
|
||||
"note": "Rounded F7 searchbar with inline mic; Enter searches."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "g8",
|
||||
"x": 16,
|
||||
"y": 164,
|
||||
"axis": "x",
|
||||
"items": [
|
||||
{
|
||||
"id": "i5",
|
||||
"kind": "chip",
|
||||
"label": "lofi beats",
|
||||
"icon": null,
|
||||
"variant": "filled"
|
||||
},
|
||||
{
|
||||
"id": "i6",
|
||||
"kind": "chip",
|
||||
"label": "live news",
|
||||
"icon": null,
|
||||
"variant": "filled"
|
||||
},
|
||||
{
|
||||
"id": "i7",
|
||||
"kind": "chip",
|
||||
"label": "worship",
|
||||
"icon": null,
|
||||
"variant": "filled"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "g10",
|
||||
"x": 16,
|
||||
"y": 212,
|
||||
"axis": "x",
|
||||
"items": [
|
||||
{
|
||||
"id": "i9",
|
||||
"kind": "text",
|
||||
"label": "Results",
|
||||
"icon": null,
|
||||
"variant": "filled",
|
||||
"size": 28,
|
||||
"bold": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "g15",
|
||||
"x": 16,
|
||||
"y": 260,
|
||||
"axis": "y",
|
||||
"items": [
|
||||
{
|
||||
"id": "i11",
|
||||
"kind": "listItem",
|
||||
"label": "Goodness of God",
|
||||
"icon": "smart_display",
|
||||
"variant": "filled",
|
||||
"supporting": "Bethel Music \u00b7 5:12",
|
||||
"icon2": "more_horiz",
|
||||
"action": {
|
||||
"to": "player",
|
||||
"transition": "slide"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "i12",
|
||||
"kind": "listItem",
|
||||
"label": "Way Maker",
|
||||
"icon": "smart_display",
|
||||
"variant": "filled",
|
||||
"supporting": "Leeland \u00b7 3:12",
|
||||
"icon2": "more_horiz",
|
||||
"action": {
|
||||
"to": "player",
|
||||
"transition": "slide"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "i13",
|
||||
"kind": "listItem",
|
||||
"label": "Build My Life",
|
||||
"icon": "smart_display",
|
||||
"variant": "filled",
|
||||
"supporting": "Housefires \u00b7 6:04",
|
||||
"icon2": "more_horiz",
|
||||
"action": {
|
||||
"to": "player",
|
||||
"transition": "slide"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "i14",
|
||||
"kind": "listItem",
|
||||
"label": "Graves Into Gardens",
|
||||
"icon": "smart_display",
|
||||
"variant": "filled",
|
||||
"supporting": "Elevation \u00b7 6:41",
|
||||
"icon2": "more_horiz",
|
||||
"action": {
|
||||
"to": "player",
|
||||
"transition": "slide"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "g17",
|
||||
"x": 16,
|
||||
"y": 700,
|
||||
"axis": "x",
|
||||
"items": [
|
||||
{
|
||||
"id": "i16",
|
||||
"kind": "listItem",
|
||||
"label": "Way Maker",
|
||||
"icon": "music_note",
|
||||
"variant": "tonal",
|
||||
"supporting": "Leeland \u00b7 3:12",
|
||||
"icon2": "play_arrow",
|
||||
"action": {
|
||||
"to": "player",
|
||||
"transition": "slideUp"
|
||||
},
|
||||
"note": "Floating glass mini player. Tap opens the player, swipe left/right = next/previous, swipe up = Up next sheet."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "g19",
|
||||
"x": 0,
|
||||
"y": 788,
|
||||
"axis": "x",
|
||||
"items": [
|
||||
{
|
||||
"id": "i18",
|
||||
"kind": "bottomNav",
|
||||
"label": "",
|
||||
"icon": null,
|
||||
"variant": "filled",
|
||||
"tabs": [
|
||||
{
|
||||
"icon": "search",
|
||||
"label": "Search"
|
||||
},
|
||||
{
|
||||
"icon": "queue_music",
|
||||
"label": "Queue"
|
||||
},
|
||||
{
|
||||
"icon": "history",
|
||||
"label": "History"
|
||||
},
|
||||
{
|
||||
"icon": "download_done",
|
||||
"label": "Saved"
|
||||
},
|
||||
{
|
||||
"icon": "more_horiz",
|
||||
"label": "More"
|
||||
}
|
||||
],
|
||||
"selected": 0,
|
||||
"actions": {
|
||||
"tab:0": {
|
||||
"to": "search",
|
||||
"transition": "fade"
|
||||
},
|
||||
"tab:1": {
|
||||
"to": "queue",
|
||||
"transition": "fade"
|
||||
}
|
||||
},
|
||||
"note": "Translucent blurred tab bar; badge on Queue."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "g21",
|
||||
"x": 492,
|
||||
"y": 0,
|
||||
"axis": "x",
|
||||
"items": [
|
||||
{
|
||||
"id": "i20",
|
||||
"kind": "topAppBar",
|
||||
"label": "Queue",
|
||||
"icon": "menu",
|
||||
"variant": "filled",
|
||||
"icon2": "checklist"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "g24",
|
||||
"x": 508,
|
||||
"y": 96,
|
||||
"axis": "x",
|
||||
"items": [
|
||||
{
|
||||
"id": "i22",
|
||||
"kind": "button",
|
||||
"label": "Play queue",
|
||||
"icon": "play_arrow",
|
||||
"variant": "filled",
|
||||
"size": 182
|
||||
},
|
||||
{
|
||||
"id": "i23",
|
||||
"kind": "button",
|
||||
"label": "Clear",
|
||||
"icon": "clear_all",
|
||||
"variant": "tonal",
|
||||
"size": 182
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "g29",
|
||||
"x": 508,
|
||||
"y": 168,
|
||||
"axis": "y",
|
||||
"items": [
|
||||
{
|
||||
"id": "i25",
|
||||
"kind": "listItem",
|
||||
"label": "Way Maker",
|
||||
"icon": "smart_display",
|
||||
"variant": "filled",
|
||||
"supporting": "Leeland \u00b7 3:12",
|
||||
"icon2": "drag_handle",
|
||||
"note": "Swipe right \u2192 Queue (accent action)."
|
||||
},
|
||||
{
|
||||
"id": "i26",
|
||||
"kind": "listItem",
|
||||
"label": "Build My Life",
|
||||
"icon": "smart_display",
|
||||
"variant": "filled",
|
||||
"supporting": "Housefires \u00b7 6:04",
|
||||
"icon2": "drag_handle",
|
||||
"note": "Swipe left \u2192 Playlist + Remove pills."
|
||||
},
|
||||
{
|
||||
"id": "i27",
|
||||
"kind": "listItem",
|
||||
"label": "Graves Into Gardens",
|
||||
"icon": "smart_display",
|
||||
"variant": "filled",
|
||||
"supporting": "Elevation \u00b7 6:41",
|
||||
"icon2": "drag_handle",
|
||||
"note": "Long swipe left removes the row."
|
||||
},
|
||||
{
|
||||
"id": "i28",
|
||||
"kind": "listItem",
|
||||
"label": "Goodness of God",
|
||||
"icon": "smart_display",
|
||||
"variant": "filled",
|
||||
"supporting": "Bethel Music \u00b7 5:12",
|
||||
"icon2": "drag_handle",
|
||||
"note": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "g32",
|
||||
"x": 508,
|
||||
"y": 480,
|
||||
"axis": "x",
|
||||
"items": [
|
||||
{
|
||||
"id": "i30",
|
||||
"kind": "button",
|
||||
"label": "Playlist",
|
||||
"icon": "playlist_add",
|
||||
"variant": "tonal"
|
||||
},
|
||||
{
|
||||
"id": "i31",
|
||||
"kind": "button",
|
||||
"label": "Remove",
|
||||
"icon": "delete",
|
||||
"variant": "filled"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "g34",
|
||||
"x": 508,
|
||||
"y": 548,
|
||||
"axis": "x",
|
||||
"items": [
|
||||
{
|
||||
"id": "i33",
|
||||
"kind": "text",
|
||||
"label": "Swipe actions revealed (example row)",
|
||||
"icon": null,
|
||||
"variant": "filled",
|
||||
"size": 14
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "g36",
|
||||
"x": 508,
|
||||
"y": 700,
|
||||
"axis": "x",
|
||||
"items": [
|
||||
{
|
||||
"id": "i35",
|
||||
"kind": "listItem",
|
||||
"label": "Way Maker",
|
||||
"icon": "music_note",
|
||||
"variant": "tonal",
|
||||
"supporting": "Leeland \u00b7 3:12",
|
||||
"icon2": "play_arrow",
|
||||
"action": {
|
||||
"to": "player",
|
||||
"transition": "slideUp"
|
||||
},
|
||||
"note": "Floating glass mini player. Tap opens the player, swipe left/right = next/previous, swipe up = Up next sheet."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "g38",
|
||||
"x": 492,
|
||||
"y": 788,
|
||||
"axis": "x",
|
||||
"items": [
|
||||
{
|
||||
"id": "i37",
|
||||
"kind": "bottomNav",
|
||||
"label": "",
|
||||
"icon": null,
|
||||
"variant": "filled",
|
||||
"tabs": [
|
||||
{
|
||||
"icon": "search",
|
||||
"label": "Search"
|
||||
},
|
||||
{
|
||||
"icon": "queue_music",
|
||||
"label": "Queue"
|
||||
},
|
||||
{
|
||||
"icon": "history",
|
||||
"label": "History"
|
||||
},
|
||||
{
|
||||
"icon": "download_done",
|
||||
"label": "Saved"
|
||||
},
|
||||
{
|
||||
"icon": "more_horiz",
|
||||
"label": "More"
|
||||
}
|
||||
],
|
||||
"selected": 1,
|
||||
"actions": {
|
||||
"tab:0": {
|
||||
"to": "search",
|
||||
"transition": "fade"
|
||||
},
|
||||
"tab:1": {
|
||||
"to": "queue",
|
||||
"transition": "fade"
|
||||
}
|
||||
},
|
||||
"note": "Translucent blurred tab bar; badge on Queue."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "g40",
|
||||
"x": 984,
|
||||
"y": 0,
|
||||
"axis": "x",
|
||||
"items": [
|
||||
{
|
||||
"id": "i39",
|
||||
"kind": "topAppBar",
|
||||
"label": "Now playing",
|
||||
"icon": "keyboard_arrow_down",
|
||||
"variant": "filled",
|
||||
"icon2": "more_horiz",
|
||||
"actions": {
|
||||
"icon": {
|
||||
"to": "back",
|
||||
"transition": "slideDown"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "g42",
|
||||
"x": 984,
|
||||
"y": 88,
|
||||
"axis": "x",
|
||||
"items": [
|
||||
{
|
||||
"id": "i41",
|
||||
"kind": "image",
|
||||
"label": "",
|
||||
"icon": null,
|
||||
"variant": "filled",
|
||||
"size": 412,
|
||||
"note": "16:9 video stage; the fullscreen button opens Fullscreen."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "g44",
|
||||
"x": 1000,
|
||||
"y": 320,
|
||||
"axis": "x",
|
||||
"items": [
|
||||
{
|
||||
"id": "i43",
|
||||
"kind": "text",
|
||||
"label": "Way Maker",
|
||||
"icon": null,
|
||||
"variant": "filled",
|
||||
"size": 24,
|
||||
"bold": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "g46",
|
||||
"x": 1000,
|
||||
"y": 356,
|
||||
"axis": "x",
|
||||
"items": [
|
||||
{
|
||||
"id": "i45",
|
||||
"kind": "text",
|
||||
"label": "Leeland",
|
||||
"icon": null,
|
||||
"variant": "filled",
|
||||
"size": 16
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "g48",
|
||||
"x": 1000,
|
||||
"y": 396,
|
||||
"axis": "x",
|
||||
"items": [
|
||||
{
|
||||
"id": "i47",
|
||||
"kind": "slider",
|
||||
"label": "",
|
||||
"icon": null,
|
||||
"variant": "filled",
|
||||
"value": 35,
|
||||
"note": "Thin seek track, accent fill."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "g54",
|
||||
"x": 1048,
|
||||
"y": 456,
|
||||
"axis": "x",
|
||||
"items": [
|
||||
{
|
||||
"id": "i49",
|
||||
"kind": "iconButton",
|
||||
"label": "",
|
||||
"icon": "skip_previous",
|
||||
"variant": "text"
|
||||
},
|
||||
{
|
||||
"id": "i50",
|
||||
"kind": "iconButton",
|
||||
"label": "",
|
||||
"icon": "replay_10",
|
||||
"variant": "text"
|
||||
},
|
||||
{
|
||||
"id": "i51",
|
||||
"kind": "iconButton",
|
||||
"label": "",
|
||||
"icon": "play_arrow",
|
||||
"variant": "filled"
|
||||
},
|
||||
{
|
||||
"id": "i52",
|
||||
"kind": "iconButton",
|
||||
"label": "",
|
||||
"icon": "forward_10",
|
||||
"variant": "text"
|
||||
},
|
||||
{
|
||||
"id": "i53",
|
||||
"kind": "iconButton",
|
||||
"label": "",
|
||||
"icon": "skip_next",
|
||||
"variant": "text"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "g61",
|
||||
"x": 1024,
|
||||
"y": 540,
|
||||
"axis": "x",
|
||||
"items": [
|
||||
{
|
||||
"id": "i55",
|
||||
"kind": "iconButton",
|
||||
"label": "",
|
||||
"icon": "playlist_add",
|
||||
"variant": "text"
|
||||
},
|
||||
{
|
||||
"id": "i56",
|
||||
"kind": "iconButton",
|
||||
"label": "",
|
||||
"icon": "download",
|
||||
"variant": "text"
|
||||
},
|
||||
{
|
||||
"id": "i57",
|
||||
"kind": "iconButton",
|
||||
"label": "",
|
||||
"icon": "lyrics",
|
||||
"variant": "text"
|
||||
},
|
||||
{
|
||||
"id": "i58",
|
||||
"kind": "iconButton",
|
||||
"label": "",
|
||||
"icon": "share",
|
||||
"variant": "text"
|
||||
},
|
||||
{
|
||||
"id": "i59",
|
||||
"kind": "iconButton",
|
||||
"label": "",
|
||||
"icon": "fullscreen",
|
||||
"variant": "text",
|
||||
"action": {
|
||||
"to": "fullscreen",
|
||||
"transition": "expand"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "i60",
|
||||
"kind": "iconButton",
|
||||
"label": "",
|
||||
"icon": "more_horiz",
|
||||
"variant": "text"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "g63",
|
||||
"x": 984,
|
||||
"y": 788,
|
||||
"axis": "x",
|
||||
"items": [
|
||||
{
|
||||
"id": "i62",
|
||||
"kind": "bottomNav",
|
||||
"label": "",
|
||||
"icon": null,
|
||||
"variant": "filled",
|
||||
"tabs": [
|
||||
{
|
||||
"icon": "search",
|
||||
"label": "Search"
|
||||
},
|
||||
{
|
||||
"icon": "queue_music",
|
||||
"label": "Queue"
|
||||
},
|
||||
{
|
||||
"icon": "history",
|
||||
"label": "History"
|
||||
},
|
||||
{
|
||||
"icon": "download_done",
|
||||
"label": "Saved"
|
||||
},
|
||||
{
|
||||
"icon": "more_horiz",
|
||||
"label": "More"
|
||||
}
|
||||
],
|
||||
"selected": 1,
|
||||
"actions": {
|
||||
"tab:0": {
|
||||
"to": "search",
|
||||
"transition": "fade"
|
||||
},
|
||||
"tab:1": {
|
||||
"to": "queue",
|
||||
"transition": "fade"
|
||||
}
|
||||
},
|
||||
"note": "Translucent blurred tab bar; badge on Queue."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "g65",
|
||||
"x": 1476,
|
||||
"y": 0,
|
||||
"axis": "x",
|
||||
"items": [
|
||||
{
|
||||
"id": "i64",
|
||||
"kind": "topAppBar",
|
||||
"label": "Queue",
|
||||
"icon": "menu",
|
||||
"variant": "filled"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "g67",
|
||||
"x": 1476,
|
||||
"y": 380,
|
||||
"axis": "x",
|
||||
"items": [
|
||||
{
|
||||
"id": "i66",
|
||||
"kind": "box",
|
||||
"label": "",
|
||||
"icon": null,
|
||||
"variant": "filled",
|
||||
"size": 412,
|
||||
"size2": 512,
|
||||
"checked": true,
|
||||
"fill": "surfaceContainerHigh",
|
||||
"note": "F7 sheet with grab handle; backdrop tap closes."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "g69",
|
||||
"x": 1492,
|
||||
"y": 404,
|
||||
"axis": "x",
|
||||
"items": [
|
||||
{
|
||||
"id": "i68",
|
||||
"kind": "text",
|
||||
"label": "Up next",
|
||||
"icon": null,
|
||||
"variant": "filled",
|
||||
"size": 20,
|
||||
"bold": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "g73",
|
||||
"x": 1492,
|
||||
"y": 448,
|
||||
"axis": "y",
|
||||
"items": [
|
||||
{
|
||||
"id": "i70",
|
||||
"kind": "listItem",
|
||||
"label": "Build My Life",
|
||||
"icon": "smart_display",
|
||||
"variant": "filled",
|
||||
"supporting": "Housefires \u00b7 6:04",
|
||||
"action": {
|
||||
"to": "player",
|
||||
"transition": "fade"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "i71",
|
||||
"kind": "listItem",
|
||||
"label": "Graves Into Gardens",
|
||||
"icon": "smart_display",
|
||||
"variant": "filled",
|
||||
"supporting": "Elevation \u00b7 6:41",
|
||||
"action": {
|
||||
"to": "player",
|
||||
"transition": "fade"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "i72",
|
||||
"kind": "listItem",
|
||||
"label": "Goodness of God",
|
||||
"icon": "smart_display",
|
||||
"variant": "filled",
|
||||
"supporting": "Bethel Music \u00b7 5:12",
|
||||
"action": {
|
||||
"to": "player",
|
||||
"transition": "fade"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "g75",
|
||||
"x": 1968,
|
||||
"y": 0,
|
||||
"axis": "x",
|
||||
"items": [
|
||||
{
|
||||
"id": "i74",
|
||||
"kind": "box",
|
||||
"label": "",
|
||||
"icon": null,
|
||||
"variant": "filled",
|
||||
"size": 412,
|
||||
"size2": 892,
|
||||
"fill": "inverseSurface",
|
||||
"note": "Video fills the screen (object-fit cover). Fit/Fill toggle in the controls."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "g77",
|
||||
"x": 1984,
|
||||
"y": 24,
|
||||
"axis": "x",
|
||||
"items": [
|
||||
{
|
||||
"id": "i76",
|
||||
"kind": "text",
|
||||
"label": "Way Maker \u2014 Leeland",
|
||||
"icon": null,
|
||||
"variant": "filled",
|
||||
"size": 14
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "g79",
|
||||
"x": 2316,
|
||||
"y": 12,
|
||||
"axis": "x",
|
||||
"items": [
|
||||
{
|
||||
"id": "i78",
|
||||
"kind": "iconButton",
|
||||
"label": "",
|
||||
"icon": "fullscreen_exit",
|
||||
"variant": "text",
|
||||
"action": {
|
||||
"to": "player",
|
||||
"transition": "fade"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "g81",
|
||||
"x": 1984,
|
||||
"y": 790,
|
||||
"axis": "x",
|
||||
"items": [
|
||||
{
|
||||
"id": "i80",
|
||||
"kind": "slider",
|
||||
"label": "",
|
||||
"icon": null,
|
||||
"variant": "filled",
|
||||
"value": 35
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "g85",
|
||||
"x": 1984,
|
||||
"y": 828,
|
||||
"axis": "x",
|
||||
"items": [
|
||||
{
|
||||
"id": "i82",
|
||||
"kind": "iconButton",
|
||||
"label": "",
|
||||
"icon": "pause",
|
||||
"variant": "text"
|
||||
},
|
||||
{
|
||||
"id": "i83",
|
||||
"kind": "iconButton",
|
||||
"label": "",
|
||||
"icon": "screen_rotation",
|
||||
"variant": "text"
|
||||
},
|
||||
{
|
||||
"id": "i84",
|
||||
"kind": "iconButton",
|
||||
"label": "",
|
||||
"icon": "fit_screen",
|
||||
"variant": "text",
|
||||
"note": "Fit \u2194 Fill."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -11456,7 +11456,7 @@ document.querySelectorAll('.chip').forEach((c) => {
|
||||
stage: els.video.parentElement, video: els.video,
|
||||
settings: () => data.settings, persist,
|
||||
toggle: () => Player.toggle(), seek: time => Player.seek(time),
|
||||
state: () => ({ time: Player.master.currentTime, duration: Player.master.duration, paused: Player.master.paused }),
|
||||
state: () => ({ time: Player.master.currentTime, duration: Player.master.duration, paused: Player.master.paused, title: current?.meta?.title || '' }),
|
||||
});
|
||||
window.playerFullscreen = fullscreen;
|
||||
els.fsBtn.addEventListener('click', () => fullscreen.enter());
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
// Which swipe actions a card gets in a given view. Pure, so it is testable.
|
||||
function cardActions(viewType,canDelete){
|
||||
const left=viewType==='queue'?[]:[{act:'queue',label:'Queue'}];
|
||||
const left=[{act:'queue',label:'Queue'}];
|
||||
const right=[{act:'playlist',label:'Playlist'}];
|
||||
if(canDelete)right.push({act:'remove',label:'Remove',danger:true});
|
||||
return {left,right};
|
||||
@@ -21,6 +21,10 @@
|
||||
if(ay>ax*1.4)return dy<0?'up':'down';
|
||||
return null;
|
||||
}
|
||||
function tabSwipeIndex(active,count,direction){
|
||||
if(direction!=='left'&&direction!=='right'||count<1)return -1;
|
||||
return Math.max(0,Math.min(count-1,active+(direction==='left'?1:-1)));
|
||||
}
|
||||
|
||||
function mount(doc){
|
||||
const html=doc.documentElement;let app=null,loading=null,active=false,sheet=null,cardsObserver=null;
|
||||
@@ -49,7 +53,22 @@
|
||||
|
||||
// ---- Swipeout cards ----
|
||||
// Framework7 only animates <a> children of the actions box, so these are links acting as buttons.
|
||||
function actionBtn(a){const b=doc.createElement('a');b.setAttribute('role','button');b.tabIndex=0;b.className='f7s-act'+(a.danger?' f7s-danger swipeout-overswipe':'');b.dataset.act=a.act;b.textContent=a.label;return b;}
|
||||
function actionBtn(a){const b=doc.createElement('a');b.setAttribute('role','button');b.tabIndex=0;b.className='swipeout-action f7s-act'+(a.danger?' f7s-danger':'');b.dataset.act=a.act;b.textContent=a.label;return b;}
|
||||
function closeRow(li){
|
||||
if(app){try{app.swipeout.close(li);return;}catch{}}
|
||||
li.classList.remove('f7s-touch-open','swipeout-opened');li.querySelector('.swipeout-content')?.style.removeProperty('transform');
|
||||
li.querySelectorAll('.swipeout-actions-opened').forEach(el=>el.classList.remove('swipeout-actions-opened'));
|
||||
li.querySelectorAll('.f7s-act').forEach(el=>el.style.removeProperty('transform'));
|
||||
}
|
||||
function openRow(li,side){
|
||||
const box=li.querySelector(`.swipeout-actions-${side}`);if(!box)return;
|
||||
doc.querySelectorAll('li.f7s-row.f7s-touch-open').forEach(other=>{if(other!==li)closeRow(other);});
|
||||
if(app){try{app.swipeout.open(li,side);if(li.classList.contains('swipeout-opened'))return;}catch{}}
|
||||
const width=[...box.children].reduce((sum,el)=>sum+el.offsetWidth,0)+16;
|
||||
li.classList.add('f7s-touch-open','swipeout-opened');box.classList.add('swipeout-actions-opened');
|
||||
li.querySelector('.swipeout-content').style.transform=`translate3d(${side==='right'?-width:width}px,0,0)`;
|
||||
box.querySelectorAll('.f7s-act').forEach(el=>el.style.transform='scale(1)');
|
||||
}
|
||||
function wrapCard(card){
|
||||
if(!card.classList||!card.classList.contains('card')||card.closest('li.swipeout')||card.classList.contains('skeleton-card')||!card._video)return;
|
||||
const li=doc.createElement('li');li.className='swipeout f7s-row';
|
||||
@@ -67,7 +86,7 @@
|
||||
function onAction(e){
|
||||
const btn=e.target.closest('.f7s-act');if(!btn)return;
|
||||
const li=btn.closest('li.swipeout'),card=li&&li.querySelector('.card'),v=card&&card._video;
|
||||
e.stopPropagation();if(app)app.swipeout.close(li);if(!v)return;
|
||||
e.stopPropagation();closeRow(li);if(!v)return;
|
||||
if(btn.dataset.act==='queue')root.addToQueue&&root.addToQueue(v);
|
||||
else if(btn.dataset.act==='playlist')root.openCardMenu&&root.openCardMenu(v);
|
||||
else if(btn.dataset.act==='remove'){const del=card.querySelector('.card-del');if(del)del.click();}
|
||||
@@ -81,8 +100,9 @@
|
||||
if(!items.length){const p=doc.createElement('p');p.className='f7s-empty';p.textContent='Nothing queued after this song.';list.append(p);return;}
|
||||
items.forEach((v,k)=>{
|
||||
const li=doc.createElement('li');li.className='f7s-up';
|
||||
li.innerHTML='<img alt="" loading="lazy" decoding="async"><div class="f7s-up-info"><div class="f7s-up-title"></div><div class="f7s-up-ch"></div></div>';
|
||||
li.querySelector('img').src=v.thumbnail||'';li.querySelector('.f7s-up-title').textContent=v.title||'';li.querySelector('.f7s-up-ch').textContent=v.channel||'';
|
||||
li.innerHTML='<div class="f7s-up-info"><div class="f7s-up-title"></div><div class="f7s-up-ch"></div></div>';
|
||||
if(v.thumbnail){const img=doc.createElement('img');img.alt='';img.loading='lazy';img.decoding='async';img.src=v.thumbnail;li.prepend(img);}
|
||||
li.querySelector('.f7s-up-title').textContent=v.title||'';li.querySelector('.f7s-up-ch').textContent=v.channel||'';
|
||||
li.addEventListener('click',()=>{const q=g('queue')||[],i=g('queueIndex');sheet.close();root.playFromList&&root.playFromList(q,(typeof i==='number'?i:-1)+1+k,g('queueSource'));});
|
||||
list.append(li);
|
||||
});
|
||||
@@ -98,17 +118,29 @@
|
||||
renderSheet();sheet.open();
|
||||
}).catch(()=>{});
|
||||
}
|
||||
function closeSheet(){
|
||||
const modal=sheet&&sheet.el&&sheet.el.f7Modal;if(!modal)return;
|
||||
sheet.el.classList.remove('f7s-full');modal.setBreakpoint(0);
|
||||
// F7 emits its initial `opened` transition after the sheet animation. If a
|
||||
// very fast drag reaches the handle during that transition, repeat close
|
||||
// after that event so its initial breakpoint cannot reopen the sheet.
|
||||
setTimeout(()=>{if(modal.opened)modal.setBreakpoint(0);},360);
|
||||
}
|
||||
|
||||
// ---- Mini bar + edge swipes (F7 has no component for these) ----
|
||||
let drag=null,swallowClick=false;
|
||||
function down(e){
|
||||
if(!on()||e.pointerType==='mouse'&&e.button!==0)return;
|
||||
if(!on()||e.pointerType==='touch'||e.pointerType==='mouse'&&e.button!==0)return;
|
||||
const mini=e.target.closest&&e.target.closest('#miniBar');
|
||||
const sidebarOpen=!!doc.querySelector('.app.sidebar-open');
|
||||
const edge=!mini&&e.clientX<22&&!e.target.closest('.player-stage');
|
||||
const inSidebar=sidebarOpen&&e.target.closest&&e.target.closest('#sidebar, #sidebarBackdrop');
|
||||
if(!mini&&!edge&&!inSidebar)return;
|
||||
drag={x:e.clientX,y:e.clientY,kind:mini?'mini':edge?'edge':'sidebar'};
|
||||
const inRow=!!(e.target.closest&&e.target.closest('.f7s-row, .card, input, textarea, select, button, a, [role="slider"], #seek, #volume'));
|
||||
const inPlayer=e.target.closest&&e.target.closest('#playerPane:not(.empty)');
|
||||
const inBody=e.target.closest&&e.target.closest('.body');
|
||||
if(!mini&&!edge&&!inSidebar&&(!inBody||inRow)&&!(inPlayer&&!inRow))return;
|
||||
const kind=mini?'mini':edge?'edge':inSidebar?'sidebar':inPlayer?'player':'tabs';
|
||||
drag={x:e.clientX,y:e.clientY,kind};
|
||||
}
|
||||
function up(e){
|
||||
if(!drag)return;const d=drag;drag=null;
|
||||
@@ -118,10 +150,67 @@
|
||||
if(dir==='left')root.playNext&&root.playNext();
|
||||
else if(dir==='right')root.playPrev&&root.playPrev();
|
||||
else if(dir==='up')openSheet();
|
||||
}else if(d.kind==='sheet'){
|
||||
if(dir==='up'&&sheet){sheet.el.f7Modal.setBreakpoint(1);sheet.el.classList.add('f7s-full');}
|
||||
else if(dir==='down')closeSheet();
|
||||
}else if(d.kind==='edge'&&dir==='right')root.openSidebar&&root.openSidebar();
|
||||
else if(d.kind==='sidebar'&&dir==='left')root.closeSidebar&&root.closeSidebar();
|
||||
else if(d.kind==='tabs'&&(dir==='left'||dir==='right')){
|
||||
const tabs=[...doc.querySelectorAll('#bottomNav .bottom-nav-btn')].filter(b=>!b.disabled&&!b.hidden);
|
||||
const active=Math.max(0,tabs.findIndex(b=>b.classList.contains('active'))),next=tabSwipeIndex(active,tabs.length,dir);
|
||||
if(tabs[next]&&next!==active){swallowClick=true;setTimeout(()=>{swallowClick=false;},400);tabs[next].click();}
|
||||
}else if(d.kind==='player'&&dir==='down'&&innerWidth<=860&&!doc.fullscreenElement&&!doc.querySelector('.orientation-fullscreen')){
|
||||
const search=doc.querySelector('#bottomNav .bottom-nav-btn[data-view="search"]');if(search)search.click();
|
||||
}
|
||||
}
|
||||
function click(e){if(swallowClick&&e.target.closest&&e.target.closest('#miniBar')){e.stopPropagation();e.preventDefault();swallowClick=false;}}
|
||||
let touchDrag=null,rowDrag=null;
|
||||
function touchDown(e){
|
||||
if(!on()||!e.changedTouches.length)return;
|
||||
const t=e.changedTouches[0],target=e.target,mini=target.closest&&target.closest('#miniBar');
|
||||
if(target.closest&&target.closest('.f7s-grab, .f7s-sheet-head')){touchDrag={x:t.clientX,y:t.clientY,kind:'sheet'};rowDrag=null;return;}
|
||||
const row=target.closest&&target.closest('li.f7s-row');
|
||||
if(row){rowDrag={row,x:t.clientX,y:t.clientY};touchDrag=null;return;}
|
||||
const sidebarOpen=!!doc.querySelector('.app.sidebar-open'),edge=!mini&&t.clientX<22&&!target.closest('.player-stage');
|
||||
const inSidebar=sidebarOpen&&target.closest&&target.closest('#sidebar, #sidebarBackdrop');
|
||||
const inRow=!!(target.closest&&target.closest('.f7s-row, .card, input, textarea, select, button, a, [role="slider"], #seek, #volume'));
|
||||
const inPlayer=target.closest&&target.closest('#playerPane:not(.empty)'),inBody=target.closest&&target.closest('.body');
|
||||
if(!mini&&!edge&&!inSidebar&&(!inBody||inRow)&&!(inPlayer&&!inRow))return;
|
||||
touchDrag={x:t.clientX,y:t.clientY,kind:mini?'mini':edge?'edge':inSidebar?'sidebar':inPlayer?'player':'tabs'};
|
||||
}
|
||||
function finish(d,x,y){
|
||||
if(!d)return;const dir=swipeDirection(x-d.x,y-d.y,d.kind==='mini'?36:56);if(!dir)return;
|
||||
if(d.kind==='mini'){
|
||||
swallowClick=true;setTimeout(()=>{swallowClick=false;},400);
|
||||
if(dir==='left')root.playNext&&root.playNext();else if(dir==='right')root.playPrev&&root.playPrev();else if(dir==='up')openSheet();
|
||||
}else if(d.kind==='sheet'){
|
||||
if(dir==='up'&&sheet){sheet.el.f7Modal.setBreakpoint(1);sheet.el.classList.add('f7s-full');}
|
||||
else if(dir==='down')closeSheet();
|
||||
}else if(d.kind==='edge'&&dir==='right')root.openSidebar&&root.openSidebar();
|
||||
else if(d.kind==='sidebar'&&dir==='left')root.closeSidebar&&root.closeSidebar();
|
||||
else if(d.kind==='tabs'&&(dir==='left'||dir==='right')){
|
||||
const tabs=[...doc.querySelectorAll('#bottomNav .bottom-nav-btn')].filter(b=>!b.disabled&&!b.hidden);
|
||||
const active=Math.max(0,tabs.findIndex(b=>b.classList.contains('active'))),next=tabSwipeIndex(active,tabs.length,dir);
|
||||
if(tabs[next]&&next!==active){swallowClick=true;setTimeout(()=>{swallowClick=false;},400);tabs[next].click();}
|
||||
}else if(d.kind==='player'&&dir==='down'&&innerWidth<=860&&!doc.fullscreenElement&&!doc.querySelector('.orientation-fullscreen')){
|
||||
const search=doc.querySelector('#bottomNav .bottom-nav-btn[data-view="search"]');if(search)search.click();
|
||||
}
|
||||
}
|
||||
function touchUp(e){
|
||||
if(!e.changedTouches.length)return;const t=e.changedTouches[0];
|
||||
if(rowDrag){const d=rowDrag;rowDrag=null;const dx=t.clientX-d.x,dy=t.clientY-d.y;
|
||||
if(Math.abs(dx)>Math.max(56,Math.abs(dy)*1.4)){
|
||||
const card=d.row.querySelector('.card');
|
||||
// A long swipe left removes once; F7's own overswipe is not relied on, so this is the only path.
|
||||
if(dx< -170){const remove=card&&card.querySelector('.card-del');if(remove)remove.click();}
|
||||
else if(dx<0)openRow(d.row,'right');
|
||||
else if(d.row.querySelector('.swipeout-actions-left'))openRow(d.row,'left');
|
||||
else {const v=card&&card._video;v&&root.addToQueue&&root.addToQueue(v);}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if(!touchDrag)return;const d=touchDrag;touchDrag=null;finish(d,t.clientX,t.clientY);
|
||||
}
|
||||
|
||||
function activate(){
|
||||
active=true;
|
||||
@@ -140,12 +229,16 @@
|
||||
doc.addEventListener('pointerdown',down,{passive:true});
|
||||
doc.addEventListener('pointerup',up,{passive:true});
|
||||
doc.addEventListener('pointercancel',()=>{drag=null;},{passive:true});
|
||||
doc.addEventListener('touchstart',touchDown,{passive:true,capture:true});
|
||||
doc.addEventListener('touchend',touchUp,{passive:true,capture:true});
|
||||
doc.addEventListener('touchcancel',()=>{touchDrag=null;rowDrag=null;},{passive:true,capture:true});
|
||||
doc.addEventListener('click',click,true);
|
||||
doc.addEventListener('click',e=>{if(e.target.classList?.contains('sheet-backdrop'))closeSheet();},true);
|
||||
new root.MutationObserver(sync).observe(html,{attributes:true,attributeFilter:['data-layout']});
|
||||
sync();
|
||||
}
|
||||
|
||||
const api={cardActions,swipeDirection};
|
||||
const api={cardActions,swipeDirection,tabSwipeIndex};
|
||||
if(typeof module!=='undefined')module.exports=api;
|
||||
else {root.F7Swipe=api; if(root.document.readyState==='loading')root.document.addEventListener('DOMContentLoaded',()=>mount(root.document),{once:true});else mount(root.document);}
|
||||
})(typeof window==='undefined'?globalThis:window);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
const { cardActions, swipeDirection } = require('./f7-layout');
|
||||
const { cardActions, swipeDirection, tabSwipeIndex } = require('./f7-layout');
|
||||
|
||||
test('cardActions: queue view has no Queue action, removable rows get Remove', () => {
|
||||
assert.deepStrictEqual(cardActions('queue', true).left, []);
|
||||
test('cardActions: every view gets Queue, removable rows get Playlist and Remove', () => {
|
||||
assert.deepStrictEqual(cardActions('queue', true).left.map((a) => a.act), ['queue']);
|
||||
assert.deepStrictEqual(cardActions('queue', true).right.map((a) => a.act), ['playlist', 'remove']);
|
||||
assert.deepStrictEqual(cardActions('search', false).left.map((a) => a.act), ['queue']);
|
||||
assert.deepStrictEqual(cardActions('search', false).right.map((a) => a.act), ['playlist']);
|
||||
@@ -17,3 +17,11 @@ test('swipeDirection classifies clear swipes and ignores short or diagonal drags
|
||||
assert.strictEqual(swipeDirection(60, 60), null);
|
||||
assert.strictEqual(swipeDirection(40, 0, 36), 'right');
|
||||
});
|
||||
|
||||
test('tabSwipeIndex moves between bottom tabs and clamps at either end', () => {
|
||||
assert.strictEqual(tabSwipeIndex(0, 5, 'left'), 1);
|
||||
assert.strictEqual(tabSwipeIndex(3, 5, 'right'), 2);
|
||||
assert.strictEqual(tabSwipeIndex(4, 5, 'left'), 4);
|
||||
assert.strictEqual(tabSwipeIndex(0, 5, 'right'), 0);
|
||||
assert.strictEqual(tabSwipeIndex(2, 5, 'up'), -1);
|
||||
});
|
||||
|
||||
@@ -1,36 +1,38 @@
|
||||
/* Shared fullscreen surface; inline player styling is unchanged. */
|
||||
.orientation-fullscreen .fullscreen-hud[hidden] { display: none; }
|
||||
.orientation-fullscreen .fullscreen-hud {
|
||||
--hud-glass: color-mix(in srgb, var(--bg-1, #111) 74%, transparent);
|
||||
--hud-ink: var(--text, #fff);
|
||||
--hud-line: color-mix(in srgb, var(--line, #fff) 32%, transparent);
|
||||
position: absolute; z-index: 20;
|
||||
right: max(12px, env(safe-area-inset-right));
|
||||
bottom: max(12px, env(safe-area-inset-bottom));
|
||||
left: max(12px, env(safe-area-inset-left));
|
||||
display: flex; align-items: center; gap: 6px;
|
||||
padding: 5px; border: 1px solid var(--hud-line); border-radius: 999px;
|
||||
background: var(--hud-glass); color: var(--hud-ink);
|
||||
-webkit-backdrop-filter: saturate(150%) blur(18px);
|
||||
backdrop-filter: saturate(150%) blur(18px);
|
||||
box-shadow: 0 8px 28px rgb(0 0 0 / .2);
|
||||
position: absolute; z-index: 20; right: max(12px, env(safe-area-inset-right));
|
||||
bottom: max(8px, env(safe-area-inset-bottom)); left: max(12px, env(safe-area-inset-left));
|
||||
display: flex; flex-direction: column; align-items: stretch; gap: 2px; padding: 4px 2px 2px;
|
||||
color: #fff; background: linear-gradient(to bottom, transparent, rgb(0 0 0 / .45));
|
||||
opacity: 1; transition: opacity .22s ease;
|
||||
}
|
||||
.fullscreen-top { display: flex; align-items: center; justify-content: space-between; min-height: 38px; }
|
||||
.fullscreen-top .fullscreen-title { min-width: 0; max-width: calc(100% - 52px); overflow: hidden; white-space: nowrap; text-overflow: ellipsis; font-size: 13px; font-weight: 500; color: #fff; text-shadow: 0 1px 5px #000; }
|
||||
.fullscreen-controls { display: flex; align-items: center; gap: 6px; }
|
||||
.fullscreen-top button { width: 38px; min-width: 38px; height: 38px; min-height: 38px; }
|
||||
.orientation-fullscreen .fullscreen-hud.hud-hidden { opacity: 0; pointer-events: none; }
|
||||
.fullscreen-hud button {
|
||||
display: grid; place-items: center; flex: none;
|
||||
width: 48px; min-width: 48px; height: 48px; min-height: 48px; padding: 0;
|
||||
border: 1px solid transparent; border-radius: 50%;
|
||||
background: color-mix(in srgb, var(--hud-ink, #fff) 10%, transparent);
|
||||
width: 44px; min-width: 44px; height: 44px; min-height: 44px; padding: 0;
|
||||
border: 0; border-radius: 50%; background: transparent;
|
||||
color: var(--hud-ink, #fff); font: inherit; cursor: pointer;
|
||||
filter: drop-shadow(0 1px 3px rgb(0 0 0 / .8));
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
.fullscreen-hud button:hover { background: color-mix(in srgb, var(--hud-ink, #fff) 18%, transparent); }
|
||||
.fullscreen-hud button:hover { background: rgb(255 255 255 / .12); }
|
||||
.fullscreen-hud button:active { transform: scale(.94); }
|
||||
.fullscreen-hud button svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
|
||||
.fullscreen-hud button .fill { fill: currentColor; stroke: none; }
|
||||
.fullscreen-hud button:focus-visible { outline: 2px solid var(--accent, #fff); outline-offset: 2px; }
|
||||
.fullscreen-hud input { min-width: 40px; min-height: 44px; height: 44px; flex: 1; margin: 0 4px; accent-color: var(--accent, #fff); touch-action: none; }
|
||||
.fullscreen-hud input { min-width: 40px; min-height: 18px; height: 18px; flex: 1; margin: 0 4px; accent-color: var(--accent, #fff); touch-action: none; appearance: none; background: transparent; }
|
||||
.fullscreen-hud input::-webkit-slider-runnable-track { height: 3px; border-radius: 2px; background: linear-gradient(to right, var(--accent, #fff) 0 var(--seek-progress, 0%), rgb(255 255 255 / .55) var(--seek-progress, 0%) 100%); }
|
||||
.fullscreen-hud input::-webkit-slider-thumb { appearance: none; width: 9px; height: 9px; margin-top: -3px; border-radius: 50%; background: #fff; box-shadow: 0 1px 4px #000; }
|
||||
.fullscreen-hud input:active::-webkit-slider-runnable-track { height: 5px; }
|
||||
.fullscreen-hud input::-moz-range-track { height: 3px; border-radius: 2px; background: rgb(255 255 255 / .55); }
|
||||
.fullscreen-hud input::-moz-range-progress { height: 3px; background: var(--accent, #fff); }
|
||||
.fullscreen-hud input::-moz-range-thumb { width: 9px; height: 9px; border: 0; border-radius: 50%; background: #fff; }
|
||||
.player-stage.orientation-fullscreen { background: #000; }
|
||||
.player-stage.orientation-overlay {
|
||||
position: fixed !important; z-index: 10000 !important; inset: 0 !important;
|
||||
@@ -49,8 +51,10 @@
|
||||
.player-stage.orientation-overlay[data-fullscreen-rotation="-90"] { transform: translate(-50%, -50%) rotate(-90deg) !important; }
|
||||
.orientation-fullscreen video {
|
||||
flex: none !important; width: 100% !important; height: 100% !important;
|
||||
max-width: none !important; max-height: none !important; object-fit: contain;
|
||||
max-width: none !important; max-height: none !important; object-fit: cover;
|
||||
}
|
||||
.orientation-fullscreen[data-fullscreen-fit="fit"] video { object-fit: contain; }
|
||||
.orientation-fullscreen[data-fullscreen-fit="fill"] video { object-fit: cover; }
|
||||
.orientation-fullscreen #stgOverlay { display: none !important; }
|
||||
.orientation-overlay[data-fullscreen-rotation="90"] .fullscreen-hud {
|
||||
bottom: max(12px, env(safe-area-inset-left)); left: max(12px, env(safe-area-inset-top)); right: max(12px, env(safe-area-inset-bottom));
|
||||
@@ -59,7 +63,7 @@
|
||||
bottom: max(12px, env(safe-area-inset-right)); left: max(12px, env(safe-area-inset-bottom)); right: max(12px, env(safe-area-inset-top));
|
||||
}
|
||||
@media (max-width: 460px) {
|
||||
.orientation-fullscreen .fullscreen-hud { gap: 2px; padding: 3px; }
|
||||
.orientation-fullscreen .fullscreen-hud { gap: 2px; padding: 4px 2px 2px; }
|
||||
.fullscreen-hud button { width: 44px; min-width: 44px; height: 44px; min-height: 44px; }
|
||||
}
|
||||
/* Raise existing ancestry instead of moving a playing video in WebKit. */
|
||||
|
||||
@@ -11,15 +11,23 @@
|
||||
if (angle === -90) return { x: rect.bottom - y, y: x - rect.left, width: rect.height, height: rect.width };
|
||||
return { x: x - rect.left, y: y - rect.top, width: rect.width, height: rect.height };
|
||||
}
|
||||
function normalizeFit(value) { return value === 'fit' ? 'fit' : 'fill'; }
|
||||
function create(options) {
|
||||
const { stage, settings, persist, toggle, seek, state } = options;
|
||||
let active = false, custom = false, native = false, locked = false, angle = 0, timer, hideTimer;
|
||||
const ancestors = [];
|
||||
for (let el = stage.parentElement; el && el !== document.body; el = el.parentElement) ancestors.push(el);
|
||||
const hud = document.createElement('div'); hud.className = 'fullscreen-hud'; hud.hidden = true;
|
||||
hud.innerHTML = '<button type="button" aria-label="Play or pause" title="Play or pause"><svg class="fill" viewBox="0 0 24 24" aria-hidden="true"><path d="M8 5v14l11-7z"/></svg></button><input type="range" min="0" max="1000" value="0" aria-label="Seek video"><button type="button" class="fullscreen-orientation" aria-label="Change fullscreen orientation" title="Change fullscreen orientation"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 8V4h4M20 16v4h-4M4 4l6 6m4 4 6 6M20 8V4h-4M4 16v4h4M20 4l-6 6m-4 4-6 6"/></svg></button><button type="button" aria-label="Exit fullscreen" title="Exit fullscreen"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M9 4H4v5m0-5 6 6m5 9h5v-5m0 5-6-6"/></svg></button>';
|
||||
hud.innerHTML = '<div class="fullscreen-top"><div class="fullscreen-title" aria-live="polite"></div><button type="button" aria-label="Exit fullscreen" title="Exit fullscreen"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M9 4H4v5m0-5 6 6m5 9h5v-5m0 5-6-6"/></svg></button></div><div class="fullscreen-controls"><button type="button" aria-label="Play or pause" title="Play or pause"><svg class="fill" viewBox="0 0 24 24" aria-hidden="true"><path d="M8 5v14l11-7z"/></svg></button><input type="range" min="0" max="1000" value="0" aria-label="Seek video"><button type="button" class="fullscreen-orientation" aria-label="Change fullscreen orientation" title="Change fullscreen orientation"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 8V4h4M20 16v4h-4M4 4l6 6m4 4 6 6M20 8V4h-4M4 16v4h4M20 4l-6 6m-4 4-6 6"/></svg></button><button type="button" class="fullscreen-fit" aria-label="Change video fit" title="Change video fit"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 7V4h3M17 4h3v3M20 17v3h-3M7 20H4v-3M7 9h10v6H7z"/></svg></button></div>';
|
||||
stage.append(hud);
|
||||
const [play, orientation, exit] = hud.querySelectorAll('button'), range = hud.querySelector('input');
|
||||
const play = hud.querySelector('button[aria-label="Play or pause"]'), orientation = hud.querySelector('.fullscreen-orientation'), fitButton = hud.querySelector('.fullscreen-fit'), exit = hud.querySelector('button[aria-label="Exit fullscreen"]'), range = hud.querySelector('input');
|
||||
const title = hud.querySelector('.fullscreen-title');
|
||||
function setFit(value) {
|
||||
const fit = normalizeFit(value); settings().fullscreenFit = fit; persist();
|
||||
stage.dataset.fullscreenFit = fit;
|
||||
fitButton.setAttribute('aria-label', `Video fill mode: ${fit === 'fill' ? 'Fill' : 'Fit'}`);
|
||||
fitButton.title = `${fit === 'fill' ? 'Fill' : 'Fit'} video frame`;
|
||||
}
|
||||
function revealControls() {
|
||||
if (!active) return;
|
||||
hud.classList.remove('hud-hidden'); clearTimeout(hideTimer);
|
||||
@@ -33,10 +41,13 @@
|
||||
hud.addEventListener('focusout', () => setTimeout(revealControls, 0));
|
||||
hud.addEventListener('click', revealControls);
|
||||
play.onclick = toggle; exit.onclick = leave;
|
||||
fitButton.onclick = () => setFit(settings().fullscreenFit === 'fill' ? 'fit' : 'fill');
|
||||
// Fit/Fill is only the HUD button: a centre double-tap is play/pause and two fingers pinch-zoom (app.js Gestures).
|
||||
function layout() {
|
||||
if (!active) return;
|
||||
angle = custom ? rotation(settings().fullscreenOrientation, innerWidth, innerHeight) : 0;
|
||||
stage.dataset.fullscreenRotation = String(angle);
|
||||
title.textContent = state().title || '';
|
||||
orientation.setAttribute('aria-label', `Change fullscreen orientation (currently ${normalize(settings().fullscreenOrientation)})`);
|
||||
orientation.title = `Orientation: ${normalize(settings().fullscreenOrientation)}`;
|
||||
}
|
||||
@@ -65,6 +76,7 @@
|
||||
ancestors.forEach(el => el.classList.remove('orientation-ancestor'));
|
||||
hud.hidden = true; hud.classList.remove('hud-hidden'); stage.classList.remove('orientation-overlay', 'orientation-fullscreen');
|
||||
delete stage.dataset.fullscreenRotation; document.body.classList.remove('landscape-fs');
|
||||
delete stage.dataset.fullscreenFit;
|
||||
if (locked) { screen.orientation?.unlock?.(); locked = false; }
|
||||
}
|
||||
function leave() {
|
||||
@@ -74,6 +86,7 @@
|
||||
async function enter() {
|
||||
if (active) { leave(); return; }
|
||||
active = true; ancestors.forEach(el => el.classList.add('orientation-ancestor')); stage.classList.add('orientation-fullscreen'); hud.hidden = false; revealControls();
|
||||
setFit(normalizeFit(settings().fullscreenFit));
|
||||
document.body.classList.add('landscape-fs');
|
||||
try {
|
||||
if (!stage.requestFullscreen) throw new Error('Custom fullscreen');
|
||||
@@ -86,6 +99,7 @@
|
||||
timer = setInterval(() => {
|
||||
const s = state();
|
||||
range.value = String(s.duration ? s.time / s.duration * 1000 : 0);
|
||||
range.style.setProperty('--seek-progress', `${s.duration ? Math.min(100, s.time / s.duration * 100) : 0}%`);
|
||||
play.setAttribute('aria-label', s.paused ? 'Play video' : 'Pause video');
|
||||
play.title = s.paused ? 'Play' : 'Pause';
|
||||
play.innerHTML = s.paused
|
||||
@@ -98,6 +112,6 @@
|
||||
window.addEventListener('resize', layout);
|
||||
return { enter, leave, point: event => coordinates(event.clientX, event.clientY, stage.getBoundingClientRect(), angle) };
|
||||
}
|
||||
const api = { normalize, rotation, coordinates, create };
|
||||
const api = { normalize, normalizeFit, rotation, coordinates, create };
|
||||
if (typeof module !== 'undefined' && module.exports) module.exports = api; else root.FullscreenOrientation = api;
|
||||
})(globalThis);
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
const { test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const { normalize, rotation, coordinates } = require('./fullscreen-orientation');
|
||||
const { normalize, normalizeFit, rotation, coordinates } = require('./fullscreen-orientation');
|
||||
test('fullscreen fit defaults safely to fill and accepts fit', () => {
|
||||
assert.equal(normalizeFit(undefined), 'fill');
|
||||
assert.equal(normalizeFit('fit'), 'fit');
|
||||
assert.equal(normalizeFit('unexpected'), 'fill');
|
||||
});
|
||||
test('orientation defaults and only rotates mismatched viewports', () => {
|
||||
assert.equal(normalize('invalid'), 'auto');
|
||||
assert.equal(rotation('auto', 390, 844), 0);
|
||||
|
||||
@@ -69,6 +69,7 @@ html[data-layout="f7-swipe"] .f7s-row.swipeout-transitioning .f7s-act {
|
||||
transition-duration: .3s; transition-property: transform, left, width, background-color;
|
||||
transition-timing-function: var(--ease, ease);
|
||||
}
|
||||
html[data-layout="f7-swipe"] .f7s-row.f7s-touch-open > .swipeout-content { transition: transform .24s var(--ease, ease); }
|
||||
html[data-layout="f7-swipe"] .f7s-row .swipeout-actions-left,
|
||||
html[data-layout="f7-swipe"] .f7s-row .swipeout-actions-right {
|
||||
position: absolute; top: 0; height: 100%; display: flex; align-items: center;
|
||||
@@ -109,7 +110,7 @@ html[data-layout="f7-swipe"] .bottom-nav-btn.active {
|
||||
html[data-layout="f7-swipe"] .bottom-nav-btn.active .bottom-nav-icon { transform: none; }
|
||||
|
||||
/* The mini-player reads as a floating sheet above the tab bar. */
|
||||
html[data-layout="f7-swipe"] .mini-bar {
|
||||
html[data-layout="f7-swipe"] #miniBar {
|
||||
left: 10px; right: 10px; bottom: calc(64px + env(safe-area-inset-bottom) + 8px);
|
||||
overflow: hidden; border: 1px solid var(--f7-hairline); border-radius: 16px;
|
||||
background: var(--f7-glass); box-shadow: 0 8px 28px rgb(0 0 0 / .18);
|
||||
@@ -170,3 +171,173 @@ html[data-layout="f7-swipe"] #miniBar .mini-bar-inner::before {
|
||||
html[data-layout="f7-swipe"] .bottom-nav { display: flex; height: calc(64px + env(safe-area-inset-bottom)); }
|
||||
html[data-layout="f7-swipe"] .bottom-nav-btn { min-height: 64px; }
|
||||
}
|
||||
|
||||
/* Full Framework7 v9 iOS surface. */
|
||||
html[data-layout="f7-swipe"] {
|
||||
--f7-glass: color-mix(in srgb, var(--bg-1) 86%, transparent);
|
||||
--f7-hairline: color-mix(in srgb, var(--line) 70%, transparent);
|
||||
--f7-inset: color-mix(in srgb, var(--bg-1) 84%, var(--bg) 16%);
|
||||
font-family: var(--ui, -apple-system, BlinkMacSystemFont, "SF Pro Text", sans-serif);
|
||||
color-scheme: dark;
|
||||
}
|
||||
html[data-layout="f7-swipe"][data-theme="light"] { color-scheme: light; }
|
||||
html[data-layout="f7-swipe"][data-theme="contrast"] { color-scheme: dark; }
|
||||
html[data-layout="f7-swipe"] body { background: var(--bg); }
|
||||
html[data-layout="f7-swipe"] body::before,
|
||||
html[data-layout="f7-swipe"] body::after { display: none; }
|
||||
html[data-layout="f7-swipe"] :focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
||||
html[data-layout="f7-swipe"] ::selection { color: var(--text); background: color-mix(in srgb, var(--accent) 42%, transparent); }
|
||||
html[data-layout="f7-swipe"] ::-webkit-scrollbar-thumb { background: var(--line); background-clip: content-box; }
|
||||
|
||||
/* Navigation/search navbar */
|
||||
html[data-layout="f7-swipe"] .topbar {
|
||||
position: relative; z-index: 20; min-height: 64px; padding: 9px 16px;
|
||||
background: var(--f7-glass); border-bottom: 1px solid var(--f7-hairline);
|
||||
-webkit-backdrop-filter: saturate(180%) blur(24px); backdrop-filter: saturate(180%) blur(24px);
|
||||
}
|
||||
html[data-layout="f7-swipe"] .sidebar-toggle,
|
||||
html[data-layout="f7-swipe"] .topbar button:not(.search-btn) { border: 0; background: transparent; box-shadow: none; color: var(--accent); }
|
||||
html[data-layout="f7-swipe"] .search-form { position: relative; gap: 8px; }
|
||||
html[data-layout="f7-swipe"] #searchInput {
|
||||
min-height: 42px; padding: 0 14px 0 38px; border: 0; border-radius: 12px;
|
||||
background: color-mix(in srgb, var(--bg-2) 92%, transparent); color: var(--text);
|
||||
box-shadow: none; font: 400 17px/1.25 var(--ui, system-ui, sans-serif);
|
||||
}
|
||||
html[data-layout="f7-swipe"] .search-form::before { content: ''; position: absolute; left: 13px; top: 12px; z-index: 1; width: 18px; height: 18px; background: var(--text-dim); -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Ccircle cx='10.5' cy='10.5' r='6.5' fill='none' stroke='black' stroke-width='2'/%3E%3Cpath d='m16 16 5 5' stroke='black' stroke-width='2'/%3E%3C/svg%3E") center/contain no-repeat; mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Ccircle cx='10.5' cy='10.5' r='6.5' fill='none' stroke='black' stroke-width='2'/%3E%3Cpath d='m16 16 5 5' stroke='black' stroke-width='2'/%3E%3C/svg%3E") center/contain no-repeat; pointer-events: none; }
|
||||
html[data-layout="f7-swipe"] .search-btn { min-height: 42px; padding: 0 8px; border: 0; background: transparent; color: var(--accent); font-size: 16px; font-weight: 500; box-shadow: none; }
|
||||
html[data-layout="f7-swipe"] #updateIconBtn { width: 40px; height: 40px; }
|
||||
html[data-layout="f7-swipe"] .search-recent { top: calc(100% + 8px); border: 1px solid var(--f7-hairline); border-radius: 14px; background: var(--bg-1); box-shadow: 0 12px 36px rgb(0 0 0 / .22); overflow: hidden; }
|
||||
html[data-layout="f7-swipe"] .search-recent > * { border-radius: 0; border: 0; border-bottom: 1px solid var(--f7-hairline); background: transparent; min-height: 48px; }
|
||||
|
||||
/* Sidebar panel and grouped destinations */
|
||||
html[data-layout="f7-swipe"] #sidebar { background: var(--bg-1); border-right: 1px solid var(--f7-hairline); padding: 24px 12px max(16px, env(safe-area-inset-bottom)); }
|
||||
html[data-layout="f7-swipe"] #sidebar .brand { font: 700 22px/1.2 var(--ui, system-ui, sans-serif); padding: 4px 12px 24px; }
|
||||
html[data-layout="f7-swipe"] #sidebar .sidebar-section-title,
|
||||
html[data-layout="f7-swipe"] #sidebar .pl-header,
|
||||
html[data-layout="f7-swipe"] #sidebar h2,
|
||||
html[data-layout="f7-swipe"] #sidebar h3 { color: var(--text-dim); font-size: 12px; letter-spacing: .04em; text-transform: uppercase; }
|
||||
html[data-layout="f7-swipe"] #sidebar .nav-item,
|
||||
html[data-layout="f7-swipe"] #sidebar .sidebar-nav button,
|
||||
html[data-layout="f7-swipe"] #playlistList > *, html[data-layout="f7-swipe"] #smartPlaylistList > * {
|
||||
min-height: 48px; border: 0; border-radius: 10px; background: transparent; box-shadow: none;
|
||||
}
|
||||
html[data-layout="f7-swipe"] #sidebar .nav-item.active,
|
||||
html[data-layout="f7-swipe"] #sidebar .nav-item[aria-current="page"] { color: var(--accent); background: color-mix(in srgb, var(--accent) 10%, transparent); }
|
||||
html[data-layout="f7-swipe"] #sidebar .nav-item { display: flex; align-items: center; gap: 10px; }
|
||||
html[data-layout="f7-swipe"] .legacy-nav-icon { display: none !important; }
|
||||
html[data-layout="f7-swipe"] .classic-nav-icon { display: block !important; width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
|
||||
html[data-layout="f7-swipe"] #sidebar .nav-item::before { display: none; }
|
||||
html[data-layout="f7-swipe"] #sidebar .nav-item::after { content: '›'; margin-left: auto; color: var(--text-dim); font-size: 22px; line-height: 1; }
|
||||
html[data-layout="f7-swipe"] #sidebar .nav-item > .classic-nav-icon { width: 30px; height: 30px; flex: none; padding: 6px; border-radius: 8px; background: var(--bg-2); }
|
||||
html[data-layout="f7-swipe"] #sidebar .playlist-item { min-height: 46px; border-radius: 10px; }
|
||||
html[data-layout="f7-swipe"] #sidebar .playlist-item::before { content: '›'; font-size: 20px; }
|
||||
html[data-layout="f7-swipe"] #newPlaylistBtn { width: 40px; height: 40px; border: 0; border-radius: 50%; background: transparent; color: var(--accent); }
|
||||
html[data-layout="f7-swipe"] #sidebar .switch { display: flex; align-items: center; justify-content: space-between; }
|
||||
html[data-layout="f7-swipe"] #audioOnlyToggle { appearance: none; width: 48px; height: 29px; margin: 0; border-radius: 99px; background: var(--bg-3); transition: background .18s; }
|
||||
html[data-layout="f7-swipe"] #audioOnlyToggle { background: radial-gradient(circle at 14px 50%, #fff 0 11px, transparent 12px), var(--bg-3); }
|
||||
html[data-layout="f7-swipe"] #audioOnlyToggle:checked { background: radial-gradient(circle at 34px 50%, #fff 0 11px, transparent 12px), var(--accent); }
|
||||
|
||||
/* Large-title home and browsing pane */
|
||||
html[data-layout="f7-swipe"] .player-placeholder { padding: 30px 24px; }
|
||||
html[data-layout="f7-swipe"] #playerPane.empty .player-stage,
|
||||
html[data-layout="f7-swipe"] #playerPane.home-playlists .player-stage { width: 100%; min-height: 0; max-height: none; aspect-ratio: auto; overflow: visible; border: 0; border-radius: 0; background: transparent; box-shadow: none; }
|
||||
html[data-layout="f7-swipe"] #playerPane.empty .player-placeholder { position: relative; inset: auto; min-height: 0; padding: 34px 16px 24px; background: transparent; }
|
||||
html[data-layout="f7-swipe"] #playerPane.empty .hero { align-items: flex-start; }
|
||||
html[data-layout="f7-swipe"] #playerPane.empty .ph-logo { display: none; }
|
||||
html[data-layout="f7-swipe"] #playerPane.empty .hero-pills { display: none; }
|
||||
html[data-layout="f7-swipe"] .hero { max-width: 740px; margin: 0 auto; text-align: left; }
|
||||
html[data-layout="f7-swipe"] .hero-title { font: 700 clamp(34px, 5vw, 40px)/1.08 var(--ui, system-ui, sans-serif); letter-spacing: -.035em; }
|
||||
html[data-layout="f7-swipe"] .hero-tagline { max-width: 54ch; color: var(--text-2); }
|
||||
html[data-layout="f7-swipe"] .hero-pills { gap: 8px; }
|
||||
html[data-layout="f7-swipe"] .hero-pill, html[data-layout="f7-swipe"] .chip { border-radius: 999px; border: 1px solid var(--f7-hairline); background: var(--bg-2); box-shadow: none; }
|
||||
html[data-layout="f7-swipe"] .home-pl { margin: 24px auto; max-width: 900px; }
|
||||
html[data-layout="f7-swipe"] .home-pl-title { font-size: 25px; font-weight: 700; }
|
||||
html[data-layout="f7-swipe"] .home-pl-grid { gap: 14px; }
|
||||
html[data-layout="f7-swipe"] .home-pl-card { overflow: hidden; border: 0; border-radius: 14px; background: var(--bg-1); box-shadow: none; }
|
||||
html[data-layout="f7-swipe"] .home-pl-card img { border-radius: 10px; }
|
||||
html[data-layout="f7-swipe"] .list-pane { background: transparent; }
|
||||
html[data-layout="f7-swipe"] .list-header { padding: 20px 20px 10px; }
|
||||
html[data-layout="f7-swipe"] .list-title-row h2 { font: 700 32px/1.12 var(--ui, system-ui, sans-serif); letter-spacing: -.035em; }
|
||||
html[data-layout="f7-swipe"] .list-sub { color: var(--text-dim); }
|
||||
@media (max-width: 860px) {
|
||||
html[data-layout="f7-swipe"]:has(#bottomNav .bottom-nav-btn.active:not([data-view="search"])) #playerPane.empty { display: none; }
|
||||
}
|
||||
html[data-layout="f7-swipe"] #listActions button:first-child { background: var(--accent); color: var(--on-accent, #fff); border-color: transparent; }
|
||||
html[data-layout="f7-swipe"] #listActions button:nth-child(2) { color: var(--text-dim); background: transparent; border-color: transparent; }
|
||||
html[data-layout="f7-swipe"]:has(#bottomNav .bottom-nav-btn.active[data-view="settings"]) #listFilterBar { display: none !important; }
|
||||
html[data-layout="f7-swipe"] #listTitleActions button, html[data-layout="f7-swipe"] #listActions button,
|
||||
html[data-layout="f7-swipe"] #batchBar button { min-height: 40px; border-radius: 999px; }
|
||||
html[data-layout="f7-swipe"] #listFilterBar { border-radius: 12px; background: var(--bg-2); }
|
||||
html[data-layout="f7-swipe"] #listFilterInput { min-height: 42px; border: 0; background: transparent; box-shadow: none; }
|
||||
html[data-layout="f7-swipe"] #batchBar { background: var(--f7-glass); border-block: 1px solid var(--f7-hairline); }
|
||||
html[data-layout="f7-swipe"] #searchMeta, html[data-layout="f7-swipe"] #status, html[data-layout="f7-swipe"] #p2pStrip { color: var(--text-dim); }
|
||||
|
||||
/* Shared inset grouped media list */
|
||||
html[data-layout="f7-swipe"] #cards { gap: 0; margin: 8px 16px 28px; padding: 0; overflow: visible; border: 1px solid var(--f7-hairline); border-radius: 14px; background: var(--f7-inset); }
|
||||
html[data-layout="f7-swipe"] #cards .card { min-height: 80px; padding: 8px 12px; border: 0; border-radius: 0; background: transparent; box-shadow: none; }
|
||||
html[data-layout="f7-swipe"] #cards .f7s-row:not(:last-child) .card { border-bottom: 1px solid var(--f7-hairline); }
|
||||
html[data-layout="f7-swipe"] #cards .thumb { width: 60px; height: 60px; flex: 0 0 60px; border-radius: 8px; }
|
||||
html[data-layout="f7-swipe"] #cards .thumb img { width: 60px; height: 60px; object-fit: cover; border-radius: 8px; }
|
||||
html[data-layout="f7-swipe"] #cards .card-title { font-size: 15px; font-weight: 600; }
|
||||
html[data-layout="f7-swipe"] #cards .card-subtitle, html[data-layout="f7-swipe"] #cards .card-channel { color: var(--text-dim); }
|
||||
html[data-layout="f7-swipe"] #cards .card-menu { width: 42px; height: 42px; border: 0; border-radius: 50%; background: transparent; box-shadow: none; }
|
||||
@media (hover: none) { html[data-layout="f7-swipe"] #cards .card-del { display: none !important; } }
|
||||
html[data-layout="f7-swipe"] #cards .card.playing { background: color-mix(in srgb, var(--accent) 9%, transparent); }
|
||||
html[data-layout="f7-swipe"] #cards .badge, html[data-layout="f7-swipe"] #cards .saved-badge { border-radius: 999px; font-size: 11px; }
|
||||
|
||||
/* Settings, player deck, notes and related lists */
|
||||
html[data-layout="f7-swipe"] #cards .settings-overview { padding: 14px 8px; border: 1px solid var(--f7-hairline); border-radius: 14px; background: var(--f7-inset); }
|
||||
html[data-layout="f7-swipe"] #cards .settings-overview > * { min-height: 56px; border-bottom: 1px solid var(--f7-hairline); background: transparent; }
|
||||
html[data-layout="f7-swipe"] #cards .settings-section { margin: 18px 0; overflow: hidden; border: 1px solid var(--f7-hairline); border-radius: 14px; background: var(--f7-inset); box-shadow: none; }
|
||||
html[data-layout="f7-swipe"] #cards .settings-section-title { padding: 12px 16px 6px; color: var(--accent); font-size: 13px; }
|
||||
html[data-layout="f7-swipe"] #cards .set-row, html[data-layout="f7-swipe"] #cards .settings-section-row { min-height: 52px; padding-inline: 16px; border-bottom: 1px solid var(--f7-hairline); }
|
||||
html[data-layout="f7-swipe"] #cards .set-row input[type="checkbox"] { appearance: none; width: 48px; height: 29px; border: 0; border-radius: 99px; background: radial-gradient(circle at 14px 50%, #fff 0 11px, transparent 12px), var(--bg-3); }
|
||||
html[data-layout="f7-swipe"] #cards .set-row input[type="checkbox"]:checked { background: radial-gradient(circle at 34px 50%, #fff 0 11px, transparent 12px), var(--accent); }
|
||||
html[data-layout="f7-swipe"] #cards .set-row select { min-height: 38px; border: 1px solid var(--f7-hairline); border-radius: 9px; background: var(--bg-2); color: var(--text); }
|
||||
html[data-layout="f7-swipe"] #controls { padding: 10px 16px; }
|
||||
html[data-layout="f7-swipe"] #controls .seek { height: 3px; accent-color: var(--accent); }
|
||||
html[data-layout="f7-swipe"] #controls .ctrl, html[data-layout="f7-swipe"] #controls .dk-more,
|
||||
html[data-layout="f7-swipe"] #nowPlayingMeta .np-btn { border: 0; border-radius: 50%; background: transparent; box-shadow: none; }
|
||||
html[data-layout="f7-swipe"] #controls .play { width: 62px; height: 62px; border-radius: 50%; background: var(--accent); color: var(--on-accent, #fff); }
|
||||
html[data-layout="f7-swipe"] #nowPlayingMeta { border: 0; border-radius: 0; background: transparent; box-shadow: none; }
|
||||
html[data-layout="f7-swipe"] .np-actions { display: flex; gap: 4px; }
|
||||
html[data-layout="f7-swipe"] #notesPanel { border: 0; border-radius: 0; background: transparent; box-shadow: none; }
|
||||
html[data-layout="f7-swipe"] #notesPanel [role="tablist"], html[data-layout="f7-swipe"] .notes-tabs { display: flex; border-radius: 999px; background: var(--bg-2); padding: 3px; }
|
||||
html[data-layout="f7-swipe"] #notesPanel [role="tab"], html[data-layout="f7-swipe"] .notes-tabs button { border: 0; border-radius: 999px; background: transparent; }
|
||||
html[data-layout="f7-swipe"] #notesPanel [role="tab"][aria-selected="true"], html[data-layout="f7-swipe"] #notesPanel .notes-tab.active { color: var(--text); background: var(--bg-3); }
|
||||
html[data-layout="f7-swipe"] #upnext, html[data-layout="f7-swipe"] #relatedPanel { border: 0; border-radius: 0; background: transparent; box-shadow: none; }
|
||||
html[data-layout="f7-swipe"] #relatedPanel .related-item { border-bottom: 1px solid var(--f7-hairline); }
|
||||
|
||||
/* Tab bar, toast, modal, action sheet and auxiliary modes */
|
||||
html[data-layout="f7-swipe"] #bottomNav { background: var(--f7-glass); border-top: 1px solid var(--f7-hairline); -webkit-backdrop-filter: blur(22px); backdrop-filter: blur(22px); }
|
||||
html[data-layout="f7-swipe"] .bottom-nav-btn { min-height: 54px; border-radius: 0; color: var(--text-dim); }
|
||||
html[data-layout="f7-swipe"] .bottom-nav-icon .classic-nav-icon { width: 20px; height: 20px; padding: 0; }
|
||||
html[data-layout="f7-swipe"] .bottom-nav-btn.active { color: var(--accent); background: transparent; }
|
||||
html[data-layout="f7-swipe"] #toastContainer { bottom: calc(74px + env(safe-area-inset-bottom)); }
|
||||
html[data-layout="f7-swipe"] #toastContainer .toast { border-radius: 999px; background: #242428; color: #fff; box-shadow: 0 4px 18px rgb(0 0 0 / .22); }
|
||||
html[data-layout="f7-swipe"] #modal .modal { border-radius: 18px; box-shadow: 0 14px 42px rgb(0 0 0 / .2); }
|
||||
html[data-layout="f7-swipe"] #modalBody { box-shadow: none; }
|
||||
html[data-layout="f7-swipe"] #modalActions button { min-height: 48px; border-radius: 12px; }
|
||||
html[data-layout="f7-swipe"] #modal .modal { background: var(--bg-1); }
|
||||
html[data-layout="f7-swipe"] .f7s-sheet { box-shadow: none; border-radius: 18px 18px 0 0; }
|
||||
html[data-layout="f7-swipe"] .f7s-sheet.f7s-full { top: 0; bottom: 0; height: 100dvh; max-height: none; border-radius: 0; }
|
||||
html[data-layout="f7-swipe"] #sectionRail { top: auto; right: 12px; bottom: calc(76px + env(safe-area-inset-bottom)); transform: none; border: 1px solid var(--f7-hairline); border-radius: 22px; background: var(--f7-glass); box-shadow: 0 4px 16px rgb(0 0 0 / .12); -webkit-backdrop-filter: blur(18px); backdrop-filter: blur(18px); }
|
||||
html[data-layout="f7-swipe"] #sectionRail button { min-width: 44px; min-height: 44px; border: 0; border-radius: 50%; background: transparent; color: var(--text-dim); }
|
||||
html[data-layout="f7-swipe"] #sectionRail button.active { color: var(--accent); }
|
||||
html[data-layout="f7-swipe"] #serviceMode, html[data-layout="f7-swipe"] #partyPanel,
|
||||
html[data-layout="f7-swipe"] #remoteView, html[data-layout="f7-swipe"] #presenterView { color: var(--text); font-family: var(--ui, system-ui, sans-serif); }
|
||||
|
||||
@media (max-width: 860px) {
|
||||
html[data-layout="f7-swipe"] .topbar { padding-inline: 10px; }
|
||||
html[data-layout="f7-swipe"] .body { touch-action: pan-y; }
|
||||
html[data-layout="f7-swipe"] #cards { margin-inline: 10px; }
|
||||
html[data-layout="f7-swipe"] .bottom-nav { display: flex; height: calc(62px + env(safe-area-inset-bottom)); padding-bottom: env(safe-area-inset-bottom); }
|
||||
html[data-layout="f7-swipe"] #miniBar { bottom: calc(66px + env(safe-area-inset-bottom)) !important; }
|
||||
html[data-layout="f7-swipe"] #modal { align-items: flex-end; padding: 0; }
|
||||
html[data-layout="f7-swipe"] #modal .modal { width: 100%; max-width: none; max-height: min(84dvh, 720px); margin: 0; overflow-y: auto; border-radius: 18px 18px 0 0; padding-bottom: max(18px, env(safe-area-inset-bottom)); }
|
||||
}
|
||||
@media (max-width: 420px) {
|
||||
html[data-layout="f7-swipe"] .list-header { padding-inline: 14px; }
|
||||
html[data-layout="f7-swipe"] .list-title-row h2 { font-size: 29px; }
|
||||
html[data-layout="f7-swipe"] #cards .card { min-height: 76px; }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user