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:
@@ -78,7 +78,7 @@
|
|||||||
"description": "Alternate action button text displayed instead of popupCastButtonTitle."
|
"description": "Alternate action button text displayed instead of popupCastButtonTitle."
|
||||||
},
|
},
|
||||||
"popupCastMenuTitle": {
|
"popupCastMenuTitle": {
|
||||||
"message": "Cast to \"$deviceName$\"",
|
"message": "Cast to $deviceName$",
|
||||||
"description": "Menu text for cast item in context menu for receivers in receiver selector.",
|
"description": "Menu text for cast item in context menu for receivers in receiver selector.",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"deviceName": {
|
"deviceName": {
|
||||||
@@ -88,7 +88,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"popupStopMenuTitle": {
|
"popupStopMenuTitle": {
|
||||||
"message": "Stop \"$appName$\" on \"$deviceName$\"",
|
"message": "Stop \"$appName$\" on $deviceName$",
|
||||||
"description": "Menu text for stop item in context menu for receivers in receiver selector.",
|
"description": "Menu text for stop item in context menu for receivers in receiver selector.",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"appName": {
|
"appName": {
|
||||||
@@ -133,11 +133,11 @@
|
|||||||
"message": "Seek forwards",
|
"message": "Seek forwards",
|
||||||
"description": "Media controls seek forward button title."
|
"description": "Media controls seek forward button title."
|
||||||
},
|
},
|
||||||
"popupMediaSubtitlesClosedCaptions": {
|
"popupMediaSubtitlesCaptions": {
|
||||||
"message": "Subtitles/closed captions",
|
"message": "Subtitles/captions",
|
||||||
"description": "Media controls subtitles/cc button title."
|
"description": "Media controls subtitles/cc button title."
|
||||||
},
|
},
|
||||||
"popupMediaSubtitlesClosedCaptionsOff": {
|
"popupMediaSubtitlesCaptionsOff": {
|
||||||
"message": "Off",
|
"message": "Off",
|
||||||
"description": "Media controls subtitles/cc button title."
|
"description": "Media controls subtitles/cc button title."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import { ReceiverSelectorMediaType } from "../types";
|
|||||||
import ReceiverSelector, { ReceiverSelection } from "./ReceiverSelector";
|
import ReceiverSelector, { ReceiverSelection } from "./ReceiverSelector";
|
||||||
import castManager from "./castManager";
|
import castManager from "./castManager";
|
||||||
|
|
||||||
|
import * as menuIds from "../menuIds";
|
||||||
|
|
||||||
const _ = browser.i18n.getMessage;
|
const _ = browser.i18n.getMessage;
|
||||||
|
|
||||||
const URL_PATTERN_HTTP = "http://*/*";
|
const URL_PATTERN_HTTP = "http://*/*";
|
||||||
@@ -69,16 +71,51 @@ export async function initMenus() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Popup context menus
|
// Popup context menus
|
||||||
const popupUrlPattern = `${browser.runtime.getURL("ui/popup")}/*`;
|
const createPopupMenu = (props: browser.menus._CreateCreateProperties) =>
|
||||||
browser.menus.create({
|
browser.menus.create({
|
||||||
id: "popup_cast",
|
visible: false,
|
||||||
title: _("popupCastButtonTitle"),
|
documentUrlPatterns: [`${browser.runtime.getURL("ui/popup")}/*`],
|
||||||
documentUrlPatterns: [popupUrlPattern]
|
...props
|
||||||
|
});
|
||||||
|
|
||||||
|
createPopupMenu({
|
||||||
|
id: menuIds.POPUP_MEDIA_PLAY_PAUSE,
|
||||||
|
title: _("popupMediaPlay")
|
||||||
});
|
});
|
||||||
browser.menus.create({
|
createPopupMenu({
|
||||||
id: "popup_stop",
|
id: menuIds.POPUP_MEDIA_MUTE,
|
||||||
title: _("popupStopButtonTitle"),
|
type: "checkbox",
|
||||||
documentUrlPatterns: [popupUrlPattern]
|
title: _("popupMediaMute")
|
||||||
|
});
|
||||||
|
createPopupMenu({
|
||||||
|
id: menuIds.POPUP_MEDIA_SKIP_PREVIOUS,
|
||||||
|
title: _("popupMediaSkipPrevious")
|
||||||
|
});
|
||||||
|
createPopupMenu({
|
||||||
|
id: menuIds.POPUP_MEDIA_SKIP_NEXT,
|
||||||
|
title: _("popupMediaSkipNext")
|
||||||
|
});
|
||||||
|
createPopupMenu({
|
||||||
|
id: menuIds.POPUP_MEDIA_CC,
|
||||||
|
title: _("popupMediaSubtitlesCaptions")
|
||||||
|
});
|
||||||
|
createPopupMenu({
|
||||||
|
id: menuIds.POPUP_MEDIA_CC_OFF,
|
||||||
|
parentId: menuIds.POPUP_MEDIA_CC,
|
||||||
|
type: "radio",
|
||||||
|
title: _("popupMediaSubtitlesCaptionsOff")
|
||||||
|
});
|
||||||
|
|
||||||
|
createPopupMenu({ id: menuIds.POPUP_MEDIA_SEPARATOR, type: "separator" });
|
||||||
|
|
||||||
|
createPopupMenu({
|
||||||
|
id: menuIds.POPUP_CAST,
|
||||||
|
title: _("popupCastButtonTitle"),
|
||||||
|
icons: { 16: "icons/icon.svg" }
|
||||||
|
});
|
||||||
|
createPopupMenu({
|
||||||
|
id: menuIds.POPUP_STOP,
|
||||||
|
title: _("popupStopButtonTitle")
|
||||||
});
|
});
|
||||||
|
|
||||||
browser.menus.onShown.addListener(onMenuShown);
|
browser.menus.onShown.addListener(onMenuShown);
|
||||||
|
|||||||
30
ext/src/menuIds.ts
Normal file
30
ext/src/menuIds.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
export const POPUP_CAST = "popupCastMenuId";
|
||||||
|
export const POPUP_STOP = "popupStopMenuId";
|
||||||
|
|
||||||
|
export const POPUP_MEDIA_SEPARATOR = "popupMediaSeparatorMenuId";
|
||||||
|
export const POPUP_MEDIA_PLAY_PAUSE = "popupMediaPlayPauseMenuId";
|
||||||
|
export const POPUP_MEDIA_MUTE = "popupMediaMuteMenuId";
|
||||||
|
export const POPUP_MEDIA_SKIP_PREVIOUS = "popupMediaSkipPreviousMenuId";
|
||||||
|
export const POPUP_MEDIA_SKIP_NEXT = "popupMediaSkipNextMenuId";
|
||||||
|
export const POPUP_MEDIA_CC = "popupMediaSubtitlesCaptionsMenuId";
|
||||||
|
export const POPUP_MEDIA_CC_OFF = "popupMediaSubtitlesCaptionsOffMenuId";
|
||||||
|
|
||||||
|
export const receiverMenuIds = [
|
||||||
|
POPUP_CAST,
|
||||||
|
POPUP_STOP,
|
||||||
|
POPUP_MEDIA_SEPARATOR,
|
||||||
|
POPUP_MEDIA_PLAY_PAUSE,
|
||||||
|
POPUP_MEDIA_MUTE,
|
||||||
|
POPUP_MEDIA_SKIP_PREVIOUS,
|
||||||
|
POPUP_MEDIA_SKIP_NEXT,
|
||||||
|
POPUP_MEDIA_CC
|
||||||
|
];
|
||||||
|
|
||||||
|
export const mediaMenuIds = [
|
||||||
|
POPUP_MEDIA_SEPARATOR,
|
||||||
|
POPUP_MEDIA_PLAY_PAUSE,
|
||||||
|
POPUP_MEDIA_MUTE,
|
||||||
|
POPUP_MEDIA_SKIP_PREVIOUS,
|
||||||
|
POPUP_MEDIA_SKIP_NEXT,
|
||||||
|
POPUP_MEDIA_CC
|
||||||
|
];
|
||||||
@@ -5,6 +5,8 @@
|
|||||||
import options, { Options } from "../../lib/options";
|
import options, { Options } from "../../lib/options";
|
||||||
import { RemoteMatchPattern } from "../../lib/matchPattern";
|
import { RemoteMatchPattern } from "../../lib/matchPattern";
|
||||||
|
|
||||||
|
import { receiverMenuIds } from "../../menuIds";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ReceiverDevice,
|
ReceiverDevice,
|
||||||
ReceiverDeviceCapabilities,
|
ReceiverDeviceCapabilities,
|
||||||
@@ -128,8 +130,6 @@
|
|||||||
|
|
||||||
resizeObserver.observe(document.documentElement);
|
resizeObserver.observe(document.documentElement);
|
||||||
|
|
||||||
window.addEventListener("contextmenu", onContextMenu);
|
|
||||||
browser.menus.onClicked.addListener(onMenuClicked);
|
|
||||||
browser.menus.onShown.addListener(onMenuShown);
|
browser.menus.onShown.addListener(onMenuShown);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -137,8 +137,6 @@
|
|||||||
port?.disconnect();
|
port?.disconnect();
|
||||||
resizeObserver.disconnect();
|
resizeObserver.disconnect();
|
||||||
|
|
||||||
window.removeEventListener("contextmenu", onContextMenu);
|
|
||||||
browser.menus.onClicked.removeListener(onMenuClicked);
|
|
||||||
browser.menus.onShown.removeListener(onMenuShown);
|
browser.menus.onShown.removeListener(onMenuShown);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -247,93 +245,27 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onContextMenu(ev: MouseEvent) {
|
/** Device ID associated with the last receiver menu that was shown. */
|
||||||
if (!(ev.target instanceof Element)) return;
|
let lastMenuShownDeviceId: string;
|
||||||
|
|
||||||
const receiverElement = ev.target.closest(".receiver");
|
|
||||||
if (receiverElement) {
|
|
||||||
browser.menus.overrideContext({
|
|
||||||
showDefaults: false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDeviceFromElement(target: Element) {
|
|
||||||
const receiverElement = target.closest(".receiver");
|
|
||||||
if (!receiverElement) return;
|
|
||||||
|
|
||||||
const receiverElementIndex = [
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
||||||
...receiverElement.parentElement!.children
|
|
||||||
].indexOf(receiverElement);
|
|
||||||
|
|
||||||
// Match by index rendered receiver element to device array
|
|
||||||
if (receiverElementIndex > -1) {
|
|
||||||
return devices[receiverElementIndex];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Handle show events for receiver context menus. */
|
/** Handle show events for receiver context menus. */
|
||||||
function onMenuShown(info: browser.menus._OnShownInfo) {
|
function onMenuShown(info: browser.menus._OnShownInfo) {
|
||||||
if (!info.targetElementId) return;
|
// Only handle menu events on this page
|
||||||
const target = browser.menus.getTargetElement(info.targetElementId);
|
if (info.pageUrl !== window.location.href) return;
|
||||||
if (!target) return;
|
|
||||||
|
|
||||||
const device = getDeviceFromElement(target);
|
|
||||||
if (!device) {
|
|
||||||
browser.menus.update("popup_cast", { visible: false });
|
|
||||||
browser.menus.update("popup_stop", { visible: false });
|
|
||||||
} else {
|
|
||||||
const app = device.status?.applications?.[0];
|
|
||||||
const isAppRunning = !!(app && !app.isIdleScreen);
|
|
||||||
|
|
||||||
browser.menus.update("popup_cast", {
|
|
||||||
visible: true,
|
|
||||||
title: _("popupCastMenuTitle", device.friendlyName),
|
|
||||||
enabled:
|
|
||||||
// Not already connecting to a receiver
|
|
||||||
!isConnecting &&
|
|
||||||
// Selected media type available
|
|
||||||
isMediaTypeAvailable
|
|
||||||
});
|
|
||||||
|
|
||||||
browser.menus.update("popup_stop", {
|
|
||||||
visible: isAppRunning,
|
|
||||||
title: isAppRunning
|
|
||||||
? _("popupStopMenuTitle", [
|
|
||||||
app.displayName,
|
|
||||||
device.friendlyName
|
|
||||||
])
|
|
||||||
: ""
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
browser.menus.refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Handle click events for receiver context menus. */
|
|
||||||
function onMenuClicked(info: browser.menus.OnClickData) {
|
|
||||||
if (
|
|
||||||
info.menuItemId !== "popup_cast" &&
|
|
||||||
info.menuItemId !== "popup_stop"
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!info.targetElementId) return;
|
if (!info.targetElementId) return;
|
||||||
const target = browser.menus.getTargetElement(info.targetElementId);
|
const targetElement = browser.menus.getTargetElement(
|
||||||
if (!target) return;
|
info.targetElementId
|
||||||
|
);
|
||||||
|
if (!targetElement) return;
|
||||||
|
|
||||||
const device = getDeviceFromElement(target);
|
const receiverElement = targetElement.closest(".receiver");
|
||||||
if (!device) return;
|
if (!receiverElement) {
|
||||||
|
for (const menuId of receiverMenuIds) {
|
||||||
|
browser.menus.update(menuId, { visible: false });
|
||||||
|
}
|
||||||
|
|
||||||
switch (info.menuItemId) {
|
browser.menus.refresh();
|
||||||
case "popup_cast":
|
|
||||||
onReceiverCast(device);
|
|
||||||
break;
|
|
||||||
case "popup_stop":
|
|
||||||
onReceiverStop(device);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -434,6 +366,7 @@
|
|||||||
ReceiverSelectorMediaType.None &&
|
ReceiverSelectorMediaType.None &&
|
||||||
isDeviceCompatible(device)}
|
isDeviceCompatible(device)}
|
||||||
isAnyConnecting={isConnecting}
|
isAnyConnecting={isConnecting}
|
||||||
|
bind:lastMenuShownDeviceId
|
||||||
on:cast={ev => onReceiverCast(ev.detail.device)}
|
on:cast={ev => onReceiverCast(ev.detail.device)}
|
||||||
on:stop={ev => onReceiverStop(ev.detail.device)}
|
on:stop={ev => onReceiverStop(ev.detail.device)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -4,10 +4,14 @@
|
|||||||
import { ReceiverDevice, ReceiverDeviceCapabilities } from "../../types";
|
import { ReceiverDevice, ReceiverDeviceCapabilities } from "../../types";
|
||||||
import type { Port } from "../../messaging";
|
import type { Port } from "../../messaging";
|
||||||
|
|
||||||
import { PlayerState } from "../../cast/sdk/media/enums";
|
import * as menuIds from "../../menuIds";
|
||||||
import type {
|
|
||||||
|
import type { Volume } from "../../cast/sdk/classes";
|
||||||
|
import { PlayerState, TrackType } from "../../cast/sdk/media/enums";
|
||||||
|
import {
|
||||||
SenderMediaMessage,
|
SenderMediaMessage,
|
||||||
SenderMessage
|
SenderMessage,
|
||||||
|
_MediaCommand
|
||||||
} from "../../cast/sdk/types";
|
} from "../../cast/sdk/types";
|
||||||
|
|
||||||
import LoadingIndicator from "../LoadingIndicator.svelte";
|
import LoadingIndicator from "../LoadingIndicator.svelte";
|
||||||
@@ -37,6 +41,42 @@
|
|||||||
/** Current media status (if available) */
|
/** Current media status (if available) */
|
||||||
$: mediaStatus = device.mediaStatus;
|
$: 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. */
|
/** Whether media controls are shown. */
|
||||||
let isExpanded = false;
|
let isExpanded = false;
|
||||||
$: if (!device.mediaStatus) {
|
$: 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(() => {
|
onMount(() => {
|
||||||
sendMediaMessage({
|
sendMediaMessage({
|
||||||
type: "GET_STATUS"
|
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>
|
</script>
|
||||||
|
|
||||||
<li class="receiver">
|
<li class="receiver" bind:this={receiverElement} on:contextmenu={onContextMenu}>
|
||||||
<img
|
<img
|
||||||
class="receiver__icon"
|
class="receiver__icon"
|
||||||
src="icons/{device.capabilities & ReceiverDeviceCapabilities.VIDEO_OUT
|
src="icons/{device.capabilities & ReceiverDeviceCapabilities.VIDEO_OUT
|
||||||
@@ -151,46 +429,19 @@
|
|||||||
<ReceiverMedia
|
<ReceiverMedia
|
||||||
status={mediaStatus}
|
status={mediaStatus}
|
||||||
{device}
|
{device}
|
||||||
on:togglePlayback={() => {
|
{textTracks}
|
||||||
switch (mediaStatus?.playerState) {
|
on:togglePlayback={() => handleMediaPlayPause()}
|
||||||
case PlayerState.PLAYING:
|
on:previous={() => handleMediaSkipPrevious()}
|
||||||
sendMediaMessage({ type: "PAUSE" });
|
on:next={() => handleMediaSkipNext()}
|
||||||
break;
|
|
||||||
case PlayerState.PAUSED:
|
|
||||||
sendMediaMessage({ type: "PLAY" });
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
on:previous={() => {
|
|
||||||
sendMediaMessage({
|
|
||||||
type: "QUEUE_UPDATE",
|
|
||||||
jump: -1
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
on:next={() => {
|
|
||||||
sendMediaMessage({
|
|
||||||
type: "QUEUE_UPDATE",
|
|
||||||
jump: 1
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
on:seek={ev => {
|
on:seek={ev => {
|
||||||
sendMediaMessage({
|
sendMediaMessage({
|
||||||
type: "SEEK",
|
type: "SEEK",
|
||||||
currentTime: ev.detail.position
|
currentTime: ev.detail.position
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
on:trackChanged={ev => {
|
on:trackChanged={ev =>
|
||||||
sendMediaMessage({
|
handleMediaTrackChange(ev.detail.activeTrackIds)}
|
||||||
type: "EDIT_TRACKS_INFO",
|
on:volumeChanged={ev => handleVolumeChange(ev.detail)}
|
||||||
activeTrackIds: ev.detail.activeTrackIds
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
on:volumeChanged={ev => {
|
|
||||||
sendReceiverMessage({
|
|
||||||
type: "SET_VOLUME",
|
|
||||||
volume: ev.detail
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -8,9 +8,9 @@
|
|||||||
import {
|
import {
|
||||||
MetadataType,
|
MetadataType,
|
||||||
PlayerState,
|
PlayerState,
|
||||||
StreamType,
|
StreamType
|
||||||
TrackType
|
|
||||||
} from "../../cast/sdk/media/enums";
|
} from "../../cast/sdk/media/enums";
|
||||||
|
import type { Track } from "../../cast/sdk/media/classes";
|
||||||
import { getEstimatedTime } from "../../cast/utils";
|
import { getEstimatedTime } from "../../cast/utils";
|
||||||
|
|
||||||
const _ = browser.i18n.getMessage;
|
const _ = browser.i18n.getMessage;
|
||||||
@@ -26,6 +26,7 @@
|
|||||||
|
|
||||||
export let status: MediaStatus;
|
export let status: MediaStatus;
|
||||||
export let device: ReceiverDevice;
|
export let device: ReceiverDevice;
|
||||||
|
export let textTracks: Track[] = [];
|
||||||
|
|
||||||
$: isPlayingOrPaused =
|
$: isPlayingOrPaused =
|
||||||
status.playerState === PlayerState.PLAYING ||
|
status.playerState === PlayerState.PLAYING ||
|
||||||
@@ -72,32 +73,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const languageNames = new Intl.DisplayNames(
|
|
||||||
[browser.i18n.getUILanguage()],
|
|
||||||
{ type: "language" }
|
|
||||||
);
|
|
||||||
|
|
||||||
// Subtitle/caption tracks
|
|
||||||
$: textTracks = status?.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;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Keep track of update times for currentTime estimations
|
// Keep track of update times for currentTime estimations
|
||||||
let lastUpdateTime = 0;
|
let lastUpdateTime = 0;
|
||||||
let currentTime = getEstimatedMediaTime();
|
let currentTime = getEstimatedMediaTime();
|
||||||
@@ -258,7 +233,7 @@
|
|||||||
class="media__cc-button ghost"
|
class="media__cc-button ghost"
|
||||||
class:media__cc-button--off={activeTextTrackId ===
|
class:media__cc-button--off={activeTextTrackId ===
|
||||||
undefined}
|
undefined}
|
||||||
title={_("popupMediaSubtitlesClosedCaptions")}
|
title={_("popupMediaSubtitlesCaptions")}
|
||||||
value={activeTextTrackId}
|
value={activeTextTrackId}
|
||||||
on:change={ev => {
|
on:change={ev => {
|
||||||
if (!status.activeTrackIds) return;
|
if (!status.activeTrackIds) return;
|
||||||
@@ -276,7 +251,7 @@
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<option value={undefined}>
|
<option value={undefined}>
|
||||||
{_("popupMediaSubtitlesClosedCaptionsOff")}
|
{_("popupMediaSubtitlesCaptionsOff")}
|
||||||
</option>
|
</option>
|
||||||
{#each textTracks as track}
|
{#each textTracks as track}
|
||||||
<option value={track.trackId}>
|
<option value={track.trackId}>
|
||||||
|
|||||||
Reference in New Issue
Block a user