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(() => {