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)" /> + +