From d2d29f6ebf2e34f8bd1ca09f3022e48730f9242e Mon Sep 17 00:00:00 2001 From: iFlip721 Date: Thu, 3 Sep 2026 07:54:36 -0400 Subject: [PATCH] feat(playlists): launch the Ultimate Video Player from a playlist row MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Issue: #53 Adds a play button to every playlist row — on Playlists and on the Dashboard's Playlists panel — that opens the Ultimate Player scoped to that playlist. Until now the player was reachable from exactly one place: open a playlist, open a channel, then the slide-out's Launch button — and only when Settings → Video Config → Video player was set to Ultimate. There was no way to say "just start watching this playlist". The player itself needed no change. UplApp.boot() already falls back to the first channel when the hash carries no &ch=, so /player.html#pl= was already a working URL that nothing was constructing. The launch lands on the first row of the rail's CURRENT order, so it follows the viewer's A-Z / # preference (#51). The button is shown regardless of the videoPlayer setting: that setting decides which player the channel SLIDE-OUT renders, which is a separate question from a per-row launcher. It is disabled while a playlist has no channels — a built-in is a zero-channel shell until its first sync, and launching one would open the player on a dead "No channel". No explanatory title there: .btn:disabled sets pointer-events:none globally so it would never show, and the row's own "0 channels" stat already says it. Order in the action cell is pin → play → waffle, keeping the waffle as the row's last control — which also keeps RowActionsMenu (position:absolute; right:0 against that cell) anchored under the waffle rather than shifting right. The launch now has three call sites, so ChannelDrawer.launchUpl's body moves to a shared openUltimatePlayer helper: the synchronous-window.open rule, the fixed 'masq-upl' window name, the features string, and the pop-up-blocked toast, along with the comments explaining why each is the way it is. The drawer keeps its own guard and delegates in one line. PlaylistRow.vue is untouched — it stays purely presentational, with each host supplying its own #actions. --- src/components/ChannelDrawer.vue | 28 +++++--------------- src/composables/uplLaunch.ts | 44 ++++++++++++++++++++++++++++++++ src/docs/playlists.md | 5 ++++ src/docs/settings.md | 3 +++ src/screens/DashboardScreen.vue | 19 +++++++++++++- src/screens/PlaylistsScreen.vue | 22 ++++++++++++++++ src/styles.css | 7 ++++- 7 files changed, 104 insertions(+), 24 deletions(-) create mode 100644 src/composables/uplLaunch.ts diff --git a/src/components/ChannelDrawer.vue b/src/components/ChannelDrawer.vue index 8cf3f36..1ef1f72 100644 --- a/src/components/ChannelDrawer.vue +++ b/src/components/ChannelDrawer.vue @@ -13,7 +13,7 @@ import GroupManager from './GroupManager.vue'; import TagPicker from './TagPicker.vue'; import { useStreamStats } from '../composables/useStreamStats'; import { videoPlayer } from '../composables/useSettings'; -import { pushToast } from '../composables/useToast'; +import { openUltimatePlayer } from '../composables/uplLaunch'; import { ACTIVE_STREAMS, CHANNELS, PLAYLISTS, appPlayerProxyPath, deleteChannels, playerSelectable, tagNames, type Channel, type StreamProbe } from '../data'; import { bus } from '../composables/bus'; @@ -128,34 +128,18 @@ function onResolution(res: string) { if (res !== props.ch.stream.res) putChannel({ stream: { res } }); } -// Ultimate Player launch (videoPlayer === 'ultimate'): open the standalone player.html window on this +// Ultimate Player launch (videoPlayer === 'ultimate'): open the standalone player.html window on THIS // channel. `ch.source` IS the owning playlist id for both playlist kinds (a custom playlist's channels are // keyed by its id; a (Default) playlist is provisioned with id === source), which is what lets the popup // load the right channel list + guide — the same lookup `playlist` above relies on. // -// window.open MUST be called synchronously from the click handler: await anything first and pop-up blockers -// treat it as unsolicited. One fixed window name means relaunching re-navigates and focuses the existing -// window rather than stacking new ones; since only the hash differs, no reload fires and the player picks -// the change up via its `hashchange` listener. -// -// `popup=yes` is already the most chrome a script can remove — it drops the tab strip, bookmarks bar, -// toolbar and menu. Do NOT add `location=no,toolbar=no,menubar=no`: every current browser IGNORES them and -// force-shows a read-only origin chip on any pop-up, as anti-spoofing rather than as a preference. The -// player window's F key / Full screen button is the supported way to get rid of that last strip. +// The window.open itself (synchronous-call rule, fixed window name, features string, pop-up-blocked toast) +// lives in the shared openUltimatePlayer helper, which the Playlists/Dashboard rows also call — they launch +// a playlist with no channel argument, which lands the player on that playlist's first channel. function launchUpl() { const { source, id } = props.ch; if (!source || !id) return; - const url = `/player.html#pl=${encodeURIComponent(source)}&ch=${encodeURIComponent(id)}`; - const w = window.open(url, 'masq-upl', 'popup=yes,width=1440,height=900'); - if (w) { - w.focus(); - } else { - pushToast({ - tone: 'warn', - title: 'Pop-up blocked', - text: 'Allow pop-ups for this site to open the Ultimate Video Player.', - }); - } + openUltimatePlayer(source, id); } // Persisted per-channel technical snapshot. The deep decode-metadata probe + its live poll (the removed diff --git a/src/composables/uplLaunch.ts b/src/composables/uplLaunch.ts new file mode 100644 index 0000000..6d90f88 --- /dev/null +++ b/src/composables/uplLaunch.ts @@ -0,0 +1,44 @@ +// Ultimate Player launch — the ONE place that opens the standalone player.html window. +// +// Three surfaces launch the player now (the channel slide-out, the Playlists rows, the Dashboard Playlists +// panel), and the parts of this that are easy to get wrong should not be copy-pasted three times: +// +// window.open MUST be called synchronously from the click handler: await anything first and pop-up blockers +// treat it as unsolicited. One fixed window name means relaunching re-navigates and focuses the existing +// window rather than stacking new ones; since only the hash differs, no reload fires and the player picks +// the change up via its `hashchange` listener. +// +// `popup=yes` is already the most chrome a script can remove — it drops the tab strip, bookmarks bar, +// toolbar and menu. Do NOT add `location=no,toolbar=no,menubar=no`: every current browser IGNORES them and +// force-shows a read-only origin chip on any pop-up, as anti-spoofing rather than as a preference. The +// player window's F key / Full screen button is the supported way to get rid of that last strip. +// +// pushToast is a module-level singleton function (see useToast.ts), so this needs no `use*()` hook shape and +// can be called from a plain handler. +import { pushToast } from './useToast'; + +const WINDOW_NAME = 'masq-upl'; +const WINDOW_FEATURES = 'popup=yes,width=1440,height=900'; + +// `playlistId` is the owning playlist id for both playlist kinds (a custom playlist's channels are keyed by +// its id; a (Default) playlist is provisioned with id === source), which is what lets the popup load the +// right channel list + guide. +// +// `channelId` is OPTIONAL. Omit it to launch a playlist without picking a channel: UplApp.boot() falls back +// to the first row of `orderedChannels` — i.e. the first channel in the viewer's current rail sort order — +// so a playlist-scoped launch needs nothing extra on the player side. +export function openUltimatePlayer(playlistId: string, channelId?: string): void { + if (!playlistId) return; + const url = `/player.html#pl=${encodeURIComponent(playlistId)}` + + (channelId ? `&ch=${encodeURIComponent(channelId)}` : ''); + const w = window.open(url, WINDOW_NAME, WINDOW_FEATURES); + if (w) { + w.focus(); + } else { + pushToast({ + tone: 'warn', + title: 'Pop-up blocked', + text: 'Allow pop-ups for this site to open the Ultimate Video Player.', + }); + } +} diff --git a/src/docs/playlists.md b/src/docs/playlists.md index 12df5bf..fcfed02 100644 --- a/src/docs/playlists.md +++ b/src/docs/playlists.md @@ -55,6 +55,11 @@ Rows are grouped by **kind** (built-in, clone, file, URL, HDHomeRun), and you ca - **Pin** a playlist with the **pin button** on its row to lift it into a **PINNED** section above the kind groups — handy for the handful you reach for most. +- **Play** a playlist with the **play button** on its row to open it in the **Ultimate Video Player** — a + dedicated player window scoped to that playlist, starting on its first channel in the player's current + sort order. It's there whatever **Settings → Video Config → Video player** is set to, and it's greyed out + on a playlist that has no channels yet. Relaunching reuses the same player window rather than piling up + pop-ups, so allow pop-ups for masqueradarr. The same button sits on the Dashboard's Playlists panel. - **Drag** a row by its grip to reorder it within its section; the order you set is saved. (Reordering pauses while a search filter is active.) - Toggle **A–Z** in the toolbar to sort rows alphabetically **within each kind group** instead. It's a diff --git a/src/docs/settings.md b/src/docs/settings.md index d04e34d..376b473 100644 --- a/src/docs/settings.md +++ b/src/docs/settings.md @@ -30,6 +30,9 @@ and the **Custom Tags** manager. Changes save to the single application settings and guide for the playlist you launched from, a channel switcher you can pull out from the right edge, and a **what's on now / next** strip under the picture. Best when you actually want to *watch* a channel rather than glance at it. Allow pop-ups for this site or the window can't open. + This setting only governs the **slide-out**. The **play button** on every playlist row — on Playlists + and on the Dashboard's Playlists panel — opens the Ultimate player scoped to that playlist whatever + this is set to, so you can leave the slide-out on the in-app player and still launch the big one. The channel switcher lists channels **A–Z by name**; the **A–Z** button in its header flips it to **channel-number** order (**#**), with unnumbered channels last. Channel numbers come from the provider and are often meaningless in a clone playlist, which is why name order is the default; your choice is diff --git a/src/screens/DashboardScreen.vue b/src/screens/DashboardScreen.vue index d2711c8..d56d0dd 100644 --- a/src/screens/DashboardScreen.vue +++ b/src/screens/DashboardScreen.vue @@ -16,6 +16,7 @@ import { usePublishedUrls } from '../composables/usePublishedUrls'; import { useToast } from '../composables/useToast'; import { useStreamStats } from '../composables/useStreamStats'; import { useSystemStats } from '../composables/useSystemStats'; +import { openUltimatePlayer } from '../composables/uplLaunch'; const emit = defineEmits<{ (e: 'add', k: 'playlist' | 'epg'): void }>(); const router = useRouter(); @@ -430,7 +431,23 @@ onBeforeUnmount(() => { View all Add playlist - + + + +
diff --git a/src/screens/PlaylistsScreen.vue b/src/screens/PlaylistsScreen.vue index 7e96b29..158f29d 100644 --- a/src/screens/PlaylistsScreen.vue +++ b/src/screens/PlaylistsScreen.vue @@ -16,6 +16,7 @@ import { useToast } from '../composables/useToast'; import { usePlaylistActions, hasLiveUpstream, isGlobalScope, syncRequestUrl } from '../composables/usePlaylistActions'; import { isAdmin } from '../composables/useAuth'; import { playlistsAlphaSort } from '../composables/useSettings'; +import { openUltimatePlayer } from '../composables/uplLaunch'; const emit = defineEmits<{ (e: 'add', k: 'playlist' | 'epg'): void }>(); const router = useRouter(); @@ -378,6 +379,27 @@ function onPlaylistUpdated(patch: Partial): void { :class="['pin-btn', { 'is-pinned': p.pinned }]" @click="togglePin(p)" /> + +