mirror of
https://github.com/rommapp/romm.git
synced 2026-10-12 02:21:58 +00:00
* feat(permissions): gate emulator streaming behind a group permission Add a streaming permission entity. Read shows streaming and allows joining a session; write allows starting one. Users without read get a disabled streaming config, so every streaming surface hides. Migration 0152 grants read and write to every existing group, so nobody loses access on upgrade. Groups created afterwards start without it. Owner-scoped session controls (save, exit, release) stay ungated so a revoked user can still leave a running game without losing saves. * fix(streaming): pr-ready code-review fixes for the streaming grant Own-only read no longer joins other users' sessions, the config refetches on permissions:changed, the store skips the joinable request when streaming is off, and tests clean up the groups they create. * refactor(streaming): pr-ready simplify pass on the streaming grant Collapse the streaming permission helpers onto assert_can/can_access, return an empty joinable list instead of 403, skip the permission resolve when streaming is off, and add a stream.start action key so the UI hides Stream (but keeps Join) for users without the write grant. * docs(streaming): pr-ready review-polish pass on the streaming grant Trim the 0152 migration docstring to a single summary line, matching 0150 and 0151. * docs(streaming): pr-ready humanizer pass on the streaming grant Plainer wording for the stream.start action key comment and the Permission Groups paragraph, which now says an own-only read shows streaming without the join. * feat(permissions): gate in-browser emulation behind a group permission Adds an emulation entity next to streaming. Read plays in the browser (EmulatorJS, Ruffle, js-dos, PICO-8, EasyRPG) and joins netplay rooms, write hosts them. Migration 0152 now backfills both entities onto every existing group. * fix(streaming): address Greptile review on PR 5185 - Pin the claimed container in the stream player, so a permission refresh that turns streaming off mid-game keeps save, load and disc swap until the session ends. - Empty the Join list when a refreshed config turns streaming off, and discard any joinable request still in flight. - Update the installPermissionsHydration doc to name every layout that calls it. * fix(migrations): renumber play permissions migration to 0153 after master's 0152 Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * chore(permissions): drop redundant PermEntity comments Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * refactor(streaming): drop the live config refresh on permission changes Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> --------- Co-authored-by: Georges-Antoine Assi <[email protected]> Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
193 lines
7.4 KiB
TypeScript
193 lines
7.4 KiB
TypeScript
// usePlatformPlayable: reactive "can any ROM on this platform run
|
|
// in-browser?" check. Companion to useCanPlay (which takes a rom);
|
|
// platform-level surfaces (PlatformTile, PlatformListRow) only know the
|
|
// slug, so they read this instead. Reuses the same engine-support utils
|
|
// so the marker on the tile and the Play button on the ROM agree.
|
|
//
|
|
// `usePlatformPlayableChecker` is the batch sibling: returns a plain
|
|
// function (no per-call computed) for surfaces that need to test many
|
|
// slugs at once (sort comparators, group-by buckets in PlatformsIndex).
|
|
//
|
|
// `emulator` resolves to the in-browser engine that actually drives the
|
|
// platform: "ruffle" for Flash, "jsdos" for Windows 3.x/9x, "dosbox" when
|
|
// the EJS catalogue picks the dosbox_pure core (DOS is wrapped by EJS but
|
|
// distinctive enough to surface by name in the UI), "emulatorjs" for
|
|
// everything else playable, and `null` when nothing on the server can run it
|
|
// or the user lacks the in-browser play grant.
|
|
//
|
|
// `mode` folds the in-browser answer together with streaming: a platform
|
|
// served by a configured streaming container is playable too, just not in
|
|
// this tab. Streaming comes from the same `containerForPlatform` the Play
|
|
// button reads, so the badge can never disagree with the button.
|
|
import { storeToRefs } from "pinia";
|
|
import { computed, type ComputedRef } from "vue";
|
|
import i18n from "@/locales";
|
|
import storeConfig, { type Config } from "@/stores/config";
|
|
import storeHeartbeat, { type Heartbeat } from "@/stores/heartbeat";
|
|
import { useNativeStore } from "@/stores/native";
|
|
import { useStreamingStore } from "@/stores/streaming";
|
|
import {
|
|
getSupportedEJSCores,
|
|
isEasyRpgEmulationSupported,
|
|
isEJSEmulationSupported,
|
|
isJsDosEmulationSupported,
|
|
isPico8EmulationSupported,
|
|
isRuffleEmulationSupported,
|
|
resolvePlatformSlug,
|
|
} from "@/utils";
|
|
import { useCan } from "@/v2/composables/useCan";
|
|
|
|
export type PlatformEmulator =
|
|
"emulatorjs" | "ruffle" | "jsdos" | "pico8" | "easyrpg" | "dosbox" | null;
|
|
|
|
export type PlatformPlayMode = "browser" | "stream" | "both" | null;
|
|
|
|
function resolveMode(playable: boolean, streamable: boolean): PlatformPlayMode {
|
|
if (playable && streamable) return "both";
|
|
if (playable) return "browser";
|
|
if (streamable) return "stream";
|
|
return null;
|
|
}
|
|
|
|
/** Pure helper: picks the engine that would actually run a platform.
|
|
* Shared between the reactive and the batch composables so both surface
|
|
* the same label for the same slug. */
|
|
function resolveEmulator(
|
|
slug: string | null | undefined,
|
|
heartbeat: Heartbeat,
|
|
config: Config | undefined,
|
|
): PlatformEmulator {
|
|
if (!slug) return null;
|
|
if (isRuffleEmulationSupported(slug, heartbeat, config)) return "ruffle";
|
|
if (isJsDosEmulationSupported(slug, heartbeat, config)) return "jsdos";
|
|
if (isPico8EmulationSupported(slug, heartbeat, config)) return "pico8";
|
|
if (isEasyRpgEmulationSupported(slug, heartbeat, config)) return "easyrpg";
|
|
if (!isEJSEmulationSupported(slug, heartbeat, config)) return null;
|
|
const cores = getSupportedEJSCores(resolvePlatformSlug(slug, config));
|
|
if (cores.includes("dosbox_pure")) return "dosbox";
|
|
return "emulatorjs";
|
|
}
|
|
|
|
export function usePlatformPlayable(getSlug: () => string | null | undefined): {
|
|
playable: ComputedRef<boolean>;
|
|
emulator: ComputedRef<PlatformEmulator>;
|
|
streamLabel: ComputedRef<string | null>;
|
|
mode: ComputedRef<PlatformPlayMode>;
|
|
} {
|
|
const heartbeatStore = storeHeartbeat();
|
|
const configStore = storeConfig();
|
|
const streamingStore = useStreamingStore();
|
|
const { value: heartbeat } = storeToRefs(heartbeatStore);
|
|
|
|
const canPlayInBrowser = useCan("rom.play");
|
|
const emulator = computed<PlatformEmulator>(() =>
|
|
canPlayInBrowser.value
|
|
? resolveEmulator(getSlug(), heartbeat.value, configStore.config)
|
|
: null,
|
|
);
|
|
|
|
const playable = computed(() => emulator.value !== null);
|
|
|
|
const streamContainer = computed(() =>
|
|
streamingStore.containerForPlatform(getSlug()),
|
|
);
|
|
|
|
const streamable = computed(() => streamContainer.value !== null);
|
|
|
|
// The container's label is what the Play button says, so the tooltip says
|
|
// the same.
|
|
const streamLabel = computed(() =>
|
|
streamingStore.containerLabelForPlatform(getSlug()),
|
|
);
|
|
|
|
const mode = computed<PlatformPlayMode>(() =>
|
|
resolveMode(playable.value, streamable.value),
|
|
);
|
|
|
|
return { playable, emulator, streamLabel, mode };
|
|
}
|
|
|
|
export function usePlatformPlayableChecker(): {
|
|
isPlayable: ComputedRef<(slug: string | null | undefined) => boolean>;
|
|
getEmulator: ComputedRef<
|
|
(slug: string | null | undefined) => PlatformEmulator
|
|
>;
|
|
isStreamable: ComputedRef<(slug: string | null | undefined) => boolean>;
|
|
isNativeSupported: ComputedRef<(slug: string | null | undefined) => boolean>;
|
|
} {
|
|
const heartbeatStore = storeHeartbeat();
|
|
const configStore = storeConfig();
|
|
const streamingStore = useStreamingStore();
|
|
const nativeStore = useNativeStore();
|
|
const { value: heartbeat } = storeToRefs(heartbeatStore);
|
|
|
|
// Expose computed functions so callers that consume them inside another
|
|
// computed (sort comparator, bucket discriminator) re-run when the
|
|
// heartbeat or admin-toggle state changes.
|
|
const canPlayInBrowser = useCan("rom.play");
|
|
const getEmulator = computed(() => {
|
|
const hb = heartbeat.value;
|
|
const cfg = configStore.config;
|
|
const allowed = canPlayInBrowser.value;
|
|
return (slug: string | null | undefined): PlatformEmulator =>
|
|
allowed ? resolveEmulator(slug, hb, cfg) : null;
|
|
});
|
|
|
|
const isPlayable = computed(() => {
|
|
const resolve = getEmulator.value;
|
|
return (slug: string | null | undefined) => resolve(slug) !== null;
|
|
});
|
|
|
|
const isStreamable = computed(() => {
|
|
// Read the config object so the returned function is rebuilt when
|
|
// /config lands or an admin changes it.
|
|
const cfg = streamingStore.config;
|
|
return (slug: string | null | undefined): boolean =>
|
|
cfg.enabled && streamingStore.containerForPlatform(slug) !== null;
|
|
});
|
|
|
|
// Whether the desktop shell can launch this platform locally. Deliberately
|
|
// not folded into `mode`: the badge answers a question about the server, and
|
|
// this one is about the machine the page is open on.
|
|
const isNativeSupported = computed(() => {
|
|
const probed = nativeStore.support;
|
|
return (slug: string | null | undefined): boolean =>
|
|
Boolean(probed) && nativeStore.isSupportedPlatform(slug);
|
|
});
|
|
|
|
return { isPlayable, getEmulator, isStreamable, isNativeSupported };
|
|
}
|
|
|
|
/** Human-readable tooltip for the play badge / column. Shared by every
|
|
* surface so the wording stays in lock-step. */
|
|
export function playTooltip(
|
|
mode: PlatformPlayMode,
|
|
emulator: PlatformEmulator,
|
|
streamLabel: string | null,
|
|
): string {
|
|
const t = i18n.global.t;
|
|
switch (mode) {
|
|
case "both":
|
|
return t("platform.playable-both", { label: streamLabel ?? "" });
|
|
case "stream":
|
|
return t("platform.playable-stream", { label: streamLabel ?? "" });
|
|
case "browser":
|
|
switch (emulator) {
|
|
case "ruffle":
|
|
return t("platform.playable-browser-ruffle");
|
|
case "jsdos":
|
|
return t("platform.playable-browser-jsdos");
|
|
case "pico8":
|
|
return t("platform.playable-browser-pico8");
|
|
case "easyrpg":
|
|
return t("platform.playable-browser-easyrpg");
|
|
case "dosbox":
|
|
return t("platform.playable-browser-dosbox");
|
|
default:
|
|
return t("platform.playable-browser-emulatorjs");
|
|
}
|
|
case null:
|
|
return t("platform.playable-none");
|
|
}
|
|
}
|