mirror of
https://github.com/hensm/fx_cast.git
synced 2026-06-11 10:09:59 +00:00
Add media context menu items to receiver popup
This commit is contained in:
@@ -4,10 +4,14 @@
|
||||
import { ReceiverDevice, ReceiverDeviceCapabilities } from "../../types";
|
||||
import type { Port } from "../../messaging";
|
||||
|
||||
import { PlayerState } from "../../cast/sdk/media/enums";
|
||||
import type {
|
||||
import * as menuIds from "../../menuIds";
|
||||
|
||||
import type { Volume } from "../../cast/sdk/classes";
|
||||
import { PlayerState, TrackType } from "../../cast/sdk/media/enums";
|
||||
import {
|
||||
SenderMediaMessage,
|
||||
SenderMessage
|
||||
SenderMessage,
|
||||
_MediaCommand
|
||||
} from "../../cast/sdk/types";
|
||||
|
||||
import LoadingIndicator from "../LoadingIndicator.svelte";
|
||||
@@ -37,6 +41,42 @@
|
||||
/** Current media status (if available) */
|
||||
$: mediaStatus = device.mediaStatus;
|
||||
|
||||
export let lastMenuShownDeviceId: string;
|
||||
$: if (lastMenuShownDeviceId === device.id) {
|
||||
void device.mediaStatus;
|
||||
updateMediaMenus();
|
||||
browser.menus.refresh();
|
||||
}
|
||||
|
||||
const languageNames = new Intl.DisplayNames(
|
||||
[browser.i18n.getUILanguage()],
|
||||
{ type: "language" }
|
||||
);
|
||||
|
||||
// Subtitle/caption tracks
|
||||
$: textTracks = mediaStatus?.media?.tracks
|
||||
?.filter(track => track.type === TrackType.TEXT)
|
||||
.map(track => {
|
||||
/**
|
||||
* If track has no name, but does have a language, get a
|
||||
* display name for the language.
|
||||
*/
|
||||
if (!track.name && track.language) {
|
||||
try {
|
||||
const displayName = languageNames.of(track.language);
|
||||
if (displayName) {
|
||||
track.name = displayName;
|
||||
}
|
||||
// eslint-disable-next-line no-empty
|
||||
} catch (err) {}
|
||||
}
|
||||
|
||||
return track;
|
||||
});
|
||||
$: activeTextTrackId = mediaStatus?.activeTrackIds?.find(trackId =>
|
||||
textTracks?.find(track => track.trackId === trackId)
|
||||
);
|
||||
|
||||
/** Whether media controls are shown. */
|
||||
let isExpanded = false;
|
||||
$: if (!device.mediaStatus) {
|
||||
@@ -79,14 +119,252 @@
|
||||
});
|
||||
}
|
||||
|
||||
let receiverElement: HTMLLIElement;
|
||||
function isTarget(
|
||||
info?: browser.menus._OnShownInfo | browser.menus.OnClickData
|
||||
) {
|
||||
// Only handle menu events on this page
|
||||
if (info?.pageUrl !== window.location.href) return false;
|
||||
|
||||
if (!info.targetElementId) return false;
|
||||
const targetElement = browser.menus.getTargetElement(
|
||||
info.targetElementId
|
||||
);
|
||||
if (!targetElement) return false;
|
||||
|
||||
return (
|
||||
targetElement === receiverElement ||
|
||||
receiverElement.contains(targetElement)
|
||||
);
|
||||
}
|
||||
|
||||
// Map of menu IDs to track IDs
|
||||
const captionSubmenus = new Map<number | string, number>();
|
||||
|
||||
function onMenuShown(info: browser.menus._OnShownInfo) {
|
||||
if (!isTarget(info)) {
|
||||
return;
|
||||
}
|
||||
|
||||
lastMenuShownDeviceId = device.id;
|
||||
|
||||
browser.menus.update(menuIds.POPUP_CAST, {
|
||||
visible: true,
|
||||
title: _("popupCastMenuTitle", device.friendlyName),
|
||||
enabled:
|
||||
// Not already connecting to a receiver
|
||||
!isConnecting &&
|
||||
// Selected media type available
|
||||
isMediaTypeAvailable
|
||||
});
|
||||
|
||||
browser.menus.update(menuIds.POPUP_STOP, {
|
||||
visible: !!application && !application.isIdleScreen,
|
||||
title: application?.displayName
|
||||
? _("popupStopMenuTitle", [
|
||||
application.displayName,
|
||||
device.friendlyName
|
||||
])
|
||||
: ""
|
||||
});
|
||||
|
||||
updateMediaMenus();
|
||||
browser.menus.refresh();
|
||||
}
|
||||
|
||||
function handleMediaPlayPause() {
|
||||
switch (mediaStatus?.playerState) {
|
||||
case PlayerState.PLAYING:
|
||||
sendMediaMessage({ type: "PAUSE" });
|
||||
break;
|
||||
case PlayerState.PAUSED:
|
||||
sendMediaMessage({ type: "PLAY" });
|
||||
break;
|
||||
}
|
||||
}
|
||||
function handleMediaSkipPrevious() {
|
||||
sendMediaMessage({
|
||||
type: "QUEUE_UPDATE",
|
||||
jump: -1
|
||||
});
|
||||
}
|
||||
function handleMediaSkipNext() {
|
||||
sendMediaMessage({
|
||||
type: "QUEUE_UPDATE",
|
||||
jump: 1
|
||||
});
|
||||
}
|
||||
function handleMediaTrackChange(activeTrackIds: number[]) {
|
||||
sendMediaMessage({
|
||||
type: "EDIT_TRACKS_INFO",
|
||||
activeTrackIds: activeTrackIds
|
||||
});
|
||||
}
|
||||
function handleVolumeChange(volume: Partial<Volume>) {
|
||||
sendReceiverMessage({
|
||||
type: "SET_VOLUME",
|
||||
volume
|
||||
});
|
||||
}
|
||||
|
||||
function onMenuClicked(info: browser.menus.OnClickData) {
|
||||
if (!isTarget(info)) return;
|
||||
|
||||
switch (info.menuItemId) {
|
||||
case menuIds.POPUP_MEDIA_PLAY_PAUSE:
|
||||
handleMediaPlayPause();
|
||||
break;
|
||||
case menuIds.POPUP_MEDIA_MUTE:
|
||||
if (
|
||||
!device.status?.volume.muted &&
|
||||
device.status?.volume.level === 0
|
||||
) {
|
||||
handleVolumeChange({ level: 1 });
|
||||
} else {
|
||||
handleVolumeChange({ muted: !device.status?.volume.muted });
|
||||
}
|
||||
break;
|
||||
case menuIds.POPUP_MEDIA_SKIP_PREVIOUS:
|
||||
handleMediaSkipPrevious();
|
||||
break;
|
||||
case menuIds.POPUP_MEDIA_SKIP_NEXT:
|
||||
handleMediaSkipNext();
|
||||
break;
|
||||
}
|
||||
|
||||
// Handle caption submenu items
|
||||
if (info.parentMenuItemId === menuIds.POPUP_MEDIA_CC) {
|
||||
// Filter and append active track IDs array
|
||||
if (!mediaStatus?.activeTrackIds) return;
|
||||
const activeTrackIds = mediaStatus.activeTrackIds.filter(
|
||||
activeTrackId => activeTrackId !== activeTextTrackId
|
||||
);
|
||||
|
||||
const trackId = captionSubmenus.get(info.menuItemId);
|
||||
if (trackId) {
|
||||
activeTrackIds.push(trackId);
|
||||
}
|
||||
|
||||
handleMediaTrackChange(activeTrackIds);
|
||||
}
|
||||
}
|
||||
|
||||
function onContextMenu() {
|
||||
browser.menus.overrideContext({ showDefaults: false });
|
||||
}
|
||||
|
||||
/** Updates media menu items from media status. */
|
||||
function updateMediaMenus() {
|
||||
// Clear caption submenu for re-build
|
||||
if (captionSubmenus.size) {
|
||||
for (const menuId of captionSubmenus.keys()) {
|
||||
browser.menus.remove(menuId);
|
||||
}
|
||||
captionSubmenus.clear();
|
||||
}
|
||||
|
||||
// Hide all media menu items if no media status
|
||||
if (!mediaStatus) {
|
||||
for (const menuId of menuIds.mediaMenuIds) {
|
||||
browser.menus.update(menuId, { visible: false });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
browser.menus.update(menuIds.POPUP_MEDIA_SEPARATOR, {
|
||||
visible: true
|
||||
});
|
||||
|
||||
// Play/pause menu item
|
||||
if (mediaStatus.supportedMediaCommands & _MediaCommand.PAUSE) {
|
||||
browser.menus.update(menuIds.POPUP_MEDIA_PLAY_PAUSE, {
|
||||
visible: true,
|
||||
title:
|
||||
mediaStatus.playerState === PlayerState.PLAYING ||
|
||||
mediaStatus.playerState === PlayerState.BUFFERING
|
||||
? _("popupMediaPause")
|
||||
: _("popupMediaPlay"),
|
||||
enabled:
|
||||
mediaStatus.playerState === PlayerState.PLAYING ||
|
||||
mediaStatus.playerState === PlayerState.PAUSED
|
||||
});
|
||||
} else {
|
||||
browser.menus.update(menuIds.POPUP_MEDIA_PLAY_PAUSE, {
|
||||
visible: false
|
||||
});
|
||||
}
|
||||
|
||||
// Mute/unmute menu item
|
||||
if (device.status?.volume) {
|
||||
const volume = device.status.volume;
|
||||
|
||||
browser.menus.update(menuIds.POPUP_MEDIA_MUTE, {
|
||||
visible: true,
|
||||
title: _("popupMediaMute"),
|
||||
checked: volume.muted || volume.level === 0,
|
||||
enabled: "muted" in volume
|
||||
});
|
||||
} else {
|
||||
browser.menus.update(menuIds.POPUP_MEDIA_MUTE, {
|
||||
visible: false
|
||||
});
|
||||
}
|
||||
|
||||
browser.menus.update(menuIds.POPUP_MEDIA_SKIP_PREVIOUS, {
|
||||
visible: !!(
|
||||
mediaStatus.supportedMediaCommands & _MediaCommand.QUEUE_PREV
|
||||
)
|
||||
});
|
||||
browser.menus.update(menuIds.POPUP_MEDIA_SKIP_NEXT, {
|
||||
visible: !!(
|
||||
mediaStatus.supportedMediaCommands & _MediaCommand.QUEUE_NEXT
|
||||
)
|
||||
});
|
||||
|
||||
// Build captions submenu from text tracks
|
||||
if (
|
||||
textTracks?.length &&
|
||||
mediaStatus.supportedMediaCommands & _MediaCommand.EDIT_TRACKS
|
||||
) {
|
||||
browser.menus.update(menuIds.POPUP_MEDIA_CC, { visible: true });
|
||||
browser.menus.update(menuIds.POPUP_MEDIA_CC_OFF, {
|
||||
visible: true,
|
||||
checked: activeTextTrackId === undefined
|
||||
});
|
||||
|
||||
for (const track of textTracks) {
|
||||
const menuId = browser.menus.create({
|
||||
title: track.name ?? track.trackId.toString(),
|
||||
parentId: menuIds.POPUP_MEDIA_CC,
|
||||
type: "radio",
|
||||
checked: track.trackId === activeTextTrackId
|
||||
});
|
||||
|
||||
captionSubmenus.set(menuId, track.trackId);
|
||||
}
|
||||
} else {
|
||||
browser.menus.update(menuIds.POPUP_MEDIA_CC, {
|
||||
visible: false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
sendMediaMessage({
|
||||
type: "GET_STATUS"
|
||||
});
|
||||
|
||||
browser.menus.onShown.addListener(onMenuShown);
|
||||
browser.menus.onClicked.addListener(onMenuClicked);
|
||||
|
||||
return () => {
|
||||
browser.menus.onShown.removeListener(onMenuShown);
|
||||
browser.menus.onClicked.removeListener(onMenuClicked);
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<li class="receiver">
|
||||
<li class="receiver" bind:this={receiverElement} on:contextmenu={onContextMenu}>
|
||||
<img
|
||||
class="receiver__icon"
|
||||
src="icons/{device.capabilities & ReceiverDeviceCapabilities.VIDEO_OUT
|
||||
@@ -151,46 +429,19 @@
|
||||
<ReceiverMedia
|
||||
status={mediaStatus}
|
||||
{device}
|
||||
on:togglePlayback={() => {
|
||||
switch (mediaStatus?.playerState) {
|
||||
case PlayerState.PLAYING:
|
||||
sendMediaMessage({ type: "PAUSE" });
|
||||
break;
|
||||
case PlayerState.PAUSED:
|
||||
sendMediaMessage({ type: "PLAY" });
|
||||
break;
|
||||
}
|
||||
}}
|
||||
on:previous={() => {
|
||||
sendMediaMessage({
|
||||
type: "QUEUE_UPDATE",
|
||||
jump: -1
|
||||
});
|
||||
}}
|
||||
on:next={() => {
|
||||
sendMediaMessage({
|
||||
type: "QUEUE_UPDATE",
|
||||
jump: 1
|
||||
});
|
||||
}}
|
||||
{textTracks}
|
||||
on:togglePlayback={() => handleMediaPlayPause()}
|
||||
on:previous={() => handleMediaSkipPrevious()}
|
||||
on:next={() => handleMediaSkipNext()}
|
||||
on:seek={ev => {
|
||||
sendMediaMessage({
|
||||
type: "SEEK",
|
||||
currentTime: ev.detail.position
|
||||
});
|
||||
}}
|
||||
on:trackChanged={ev => {
|
||||
sendMediaMessage({
|
||||
type: "EDIT_TRACKS_INFO",
|
||||
activeTrackIds: ev.detail.activeTrackIds
|
||||
});
|
||||
}}
|
||||
on:volumeChanged={ev => {
|
||||
sendReceiverMessage({
|
||||
type: "SET_VOLUME",
|
||||
volume: ev.detail
|
||||
});
|
||||
}}
|
||||
on:trackChanged={ev =>
|
||||
handleMediaTrackChange(ev.detail.activeTrackIds)}
|
||||
on:volumeChanged={ev => handleVolumeChange(ev.detail)}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user