Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 6 additions & 22 deletions src/components/ChannelDrawer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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
Expand Down
44 changes: 44 additions & 0 deletions src/composables/uplLaunch.ts
Original file line number Diff line number Diff line change
@@ -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.',
});
}
}
5 changes: 5 additions & 0 deletions src/docs/playlists.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions src/docs/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 18 additions & 1 deletion src/screens/DashboardScreen.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -430,7 +431,23 @@ onBeforeUnmount(() => {
<Btn variant="ghost" size="sm" @click="go('/playlists')">View all</Btn>
<Btn variant="ghost" size="sm" icon="plus" @click="emit('add', 'playlist')">Add playlist</Btn>
</div>
<PlaylistRow v-for="p in sortedPlaylists" :key="p.id" :playlist="p" compact @open="go(`/playlists/${p.id}`)" />
<!-- Same Ultimate Player launcher as the Playlists rows, plus the chevron this panel showed
before (PlaylistRow's default #actions) so the open affordance survives. -->
<PlaylistRow v-for="p in sortedPlaylists" :key="p.id" :playlist="p" compact @open="go(`/playlists/${p.id}`)">
<template #actions>
<Btn
size="sm"
variant="ghost"
icon="play"
class="upl-btn"
:disabled="!p.channels"
title="Open in the Ultimate Video Player"
aria-label="Open in the Ultimate Video Player"
@click="openUltimatePlayer(p.id)"
/>
<Btn variant="ghost" size="sm" icon="chevron-r" @click="go(`/playlists/${p.id}`)" />
</template>
</PlaylistRow>
</div>

<div class="card flush">
Expand Down
22 changes: 22 additions & 0 deletions src/screens/PlaylistsScreen.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -378,6 +379,27 @@ function onPlaylistUpdated(patch: Partial<Playlist>): void {
:class="['pin-btn', { 'is-pinned': p.pinned }]"
@click="togglePin(p)"
/>
<!-- Launch the Ultimate Player scoped to THIS playlist. No channel id: UplApp.boot() falls back
to the first row of the rail's current order, so the player starts on the playlist's first
channel. Shown regardless of the videoPlayer setting — that setting decides what the channel
slide-out renders, which is a separate question from this row's launcher. Ghost + .upl-btn
rather than cyan so it doesn't compete with the waffle sitting next to it.
Sits BEFORE the waffle so the waffle stays the row's last control — which also keeps
RowActionsMenu (position:absolute; right:0 against this cell) anchored under it.
Disabled while the playlist has no channels (a built-in is a zero-channel shell until its
first sync — launching 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 two cells to the left already says it. -->
<Btn
size="sm"
variant="ghost"
icon="play"
class="upl-btn"
:disabled="!p.channels"
title="Open in the Ultimate Video Player"
aria-label="Open in the Ultimate Video Player"
@click="openUltimatePlayer(p.id)"
/>
<Btn
variant="cyan"
size="sm"
Expand Down
7 changes: 6 additions & 1 deletion src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -1324,7 +1324,7 @@ textarea.input { height: auto; padding: 10px; min-height: 80px; resize: vertical
display: flex;
justify-content: flex-end;
align-items: center;
gap: 2px;
gap: 4px;
width: 100%;
justify-self: end;
}
Expand All @@ -1334,6 +1334,11 @@ textarea.input { height: auto; padding: 10px; min-height: 80px; resize: vertical
.src-row.pl-row .pl-row-actions .btn.pin-btn { color: var(--text-3); }
.src-row.pl-row .pl-row-actions .btn.pin-btn:hover { color: var(--text-1); }
.src-row.pl-row .pl-row-actions .btn.pin-btn.is-pinned { color: var(--accent-hi); }
/* Ultimate Player launcher (right of the waffle). Same muted-at-rest treatment as the pin so the row keeps
ONE loud control — the cyan waffle; two cyan buttons side by side would fight for the eye. Falls back to
the shared .btn:disabled treatment on a playlist with nothing to tune yet. */
.src-row.pl-row .pl-row-actions .btn.upl-btn { color: var(--text-3); }
.src-row.pl-row .pl-row-actions .btn.upl-btn:hover { color: var(--accent-hi); }
/* Two stacked rows inside the playlist name cell: row 1 is the status dot + display name, row 2 is the
chip/pill cluster underneath. Scoped to .pl-row so the shared .src-name (EPG Sources / Dashboard rows,
which stay a single inline row) is unaffected. The column overrides the base .src-name flex-row. */
Expand Down
Loading