mirror of
https://github.com/Pandipipas/scoreko-dev.git
synced 2026-06-06 03:32:06 +00:00
265 lines
7.1 KiB
Vue
265 lines
7.1 KiB
Vue
<script setup lang="ts">
|
|
import { useHead } from '@unhead/vue';
|
|
import { computed, ref, watch } from 'vue';
|
|
import bundlePackage from '../../../../package.json';
|
|
import { graphicsSettingsReplicant } from '../../../browser_shared/replicants';
|
|
import { t } from '../i18n';
|
|
|
|
defineOptions({ name: 'GraphicsView' });
|
|
|
|
type GraphicConfig = {
|
|
name?: string;
|
|
title?: string;
|
|
file: string;
|
|
width?: number;
|
|
height?: number;
|
|
};
|
|
|
|
type GraphicCard = {
|
|
id: 'scoreboard' | 'commentary';
|
|
label: string;
|
|
graphic: GraphicConfig;
|
|
skinOptions?: Array<{ label: string; value: string }>;
|
|
};
|
|
|
|
useHead(() => ({ title: t('graphicsTitle') }));
|
|
|
|
const graphics = computed<GraphicConfig[]>(() => bundlePackage.nodecg?.graphics ?? []);
|
|
|
|
const baseUrl = computed(() => {
|
|
const bundleName = bundlePackage.name ?? 'bundle';
|
|
return `${window.location.origin}/bundles/${bundleName}/graphics/`;
|
|
});
|
|
|
|
const buildGraphicUrl = (graphic: GraphicConfig) => `${baseUrl.value}${graphic.file}`;
|
|
|
|
const buildGraphicName = (graphic: GraphicConfig) => {
|
|
const cleaned = graphic.file.replace(/\/?main\.html$/i, '');
|
|
const parts = cleaned.split('/').filter(Boolean);
|
|
return parts.at(-1) ?? graphic.file;
|
|
};
|
|
|
|
const getGraphicKey = (graphic: GraphicConfig) =>
|
|
(graphic.name ?? buildGraphicName(graphic)).toLowerCase();
|
|
|
|
const scoreboardGraphics = computed(() =>
|
|
graphics.value.filter((graphic) => getGraphicKey(graphic).includes('scoreboard')),
|
|
);
|
|
|
|
const canonicalScoreboardGraphic = computed(() => {
|
|
const explicitDefault = scoreboardGraphics.value.find(
|
|
(graphic) => getGraphicKey(graphic) === 'scoreboard',
|
|
);
|
|
return explicitDefault ?? scoreboardGraphics.value[0];
|
|
});
|
|
|
|
const commentaryGraphic = computed(() =>
|
|
graphics.value.find((graphic) => getGraphicKey(graphic).includes('commentary')),
|
|
);
|
|
|
|
const selectedScoreboardSkin = ref<string>('');
|
|
|
|
watch(
|
|
[scoreboardGraphics, () => graphicsSettingsReplicant?.data?.scoreboardSkin],
|
|
([availableSkins, replicatedSkin]) => {
|
|
if (availableSkins.length === 0) {
|
|
selectedScoreboardSkin.value = '';
|
|
return;
|
|
}
|
|
|
|
const hasReplicatedSkin = availableSkins.some((graphic) => graphic.file === replicatedSkin);
|
|
if (hasReplicatedSkin) {
|
|
selectedScoreboardSkin.value = replicatedSkin ?? "";
|
|
return;
|
|
}
|
|
|
|
const hasCurrentSkin = availableSkins.some(
|
|
(graphic) => graphic.file === selectedScoreboardSkin.value,
|
|
);
|
|
|
|
if (!hasCurrentSkin) {
|
|
selectedScoreboardSkin.value = availableSkins[0]!.file;
|
|
}
|
|
},
|
|
{ immediate: true },
|
|
);
|
|
|
|
watch(
|
|
selectedScoreboardSkin,
|
|
(value) => {
|
|
if (!value || !graphicsSettingsReplicant) {
|
|
return;
|
|
}
|
|
|
|
if (graphicsSettingsReplicant.data?.scoreboardSkin === value) {
|
|
return;
|
|
}
|
|
|
|
graphicsSettingsReplicant.data = {
|
|
scoreboardSkin: value,
|
|
};
|
|
graphicsSettingsReplicant.save();
|
|
},
|
|
{ immediate: true },
|
|
);
|
|
|
|
const cards = computed<GraphicCard[]>(() => {
|
|
const result: GraphicCard[] = [];
|
|
|
|
if (canonicalScoreboardGraphic.value) {
|
|
result.push({
|
|
id: 'scoreboard',
|
|
label: t('graphicsScoreboard'),
|
|
graphic: canonicalScoreboardGraphic.value,
|
|
skinOptions: scoreboardGraphics.value.map((graphic) => ({
|
|
label: graphic.title ?? buildGraphicName(graphic),
|
|
value: graphic.file,
|
|
})),
|
|
});
|
|
}
|
|
|
|
if (commentaryGraphic.value) {
|
|
result.push({
|
|
id: 'commentary',
|
|
label: t('graphicsCommentary'),
|
|
graphic: commentaryGraphic.value,
|
|
});
|
|
}
|
|
|
|
return result;
|
|
});
|
|
|
|
const copiedCardId = ref<string | null>(null);
|
|
|
|
const copyUrl = async (graphic: GraphicConfig, cardId: string) => {
|
|
const url = buildGraphicUrl(graphic);
|
|
if (navigator.clipboard?.writeText) {
|
|
await navigator.clipboard.writeText(url);
|
|
} else {
|
|
const input = document.createElement('input');
|
|
input.value = url;
|
|
document.body.appendChild(input);
|
|
input.select();
|
|
document.execCommand('copy');
|
|
document.body.removeChild(input);
|
|
}
|
|
|
|
copiedCardId.value = cardId;
|
|
setTimeout(() => {
|
|
copiedCardId.value = null;
|
|
}, 2000);
|
|
};
|
|
|
|
const openUrl = (graphic: GraphicConfig) => {
|
|
window.open(buildGraphicUrl(graphic), '_blank');
|
|
};
|
|
|
|
const onDragStart = (event: DragEvent, graphic: GraphicConfig) => {
|
|
const url = buildGraphicUrl(graphic);
|
|
const name = buildGraphicName(graphic);
|
|
const payload = JSON.stringify({
|
|
name,
|
|
url,
|
|
width: graphic.width ?? 1920,
|
|
height: graphic.height ?? 1080,
|
|
});
|
|
|
|
event.dataTransfer?.setData('text/uri-list', url);
|
|
event.dataTransfer?.setData('text/plain', url);
|
|
event.dataTransfer?.setData('application/x-obs-browser-source', payload);
|
|
event.dataTransfer?.setData('application/json', payload);
|
|
event.dataTransfer?.setDragImage?.(new Image(), 0, 0);
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<QPage class="q-pa-lg">
|
|
<div class="q-mb-lg">
|
|
<div class="text-h5 text-weight-medium">
|
|
{{ t('graphicsTitle') }}
|
|
</div>
|
|
<div class="text-body2 text-grey-7 q-mt-xs">
|
|
{{ t('graphicsDescription') }}
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
v-if="cards.length === 0"
|
|
class="text-body2 text-grey-6"
|
|
>
|
|
{{ t('graphicsNoConfigured') }}
|
|
</div>
|
|
|
|
<div class="row q-col-gutter-md">
|
|
<div
|
|
v-for="card in cards"
|
|
:key="card.id"
|
|
class="col-12 col-md-6"
|
|
>
|
|
<QCard
|
|
bordered
|
|
class="bg-grey-10"
|
|
>
|
|
<QCardSection class="row items-center justify-between">
|
|
<div>
|
|
<div class="text-h6">
|
|
{{ card.label }}
|
|
</div>
|
|
<div class="text-caption text-grey-4">
|
|
{{ card.graphic.file }}
|
|
</div>
|
|
</div>
|
|
<QBadge
|
|
color="primary"
|
|
outline
|
|
>
|
|
{{ card.graphic.width ?? 1920 }}x{{ card.graphic.height ?? 1080 }}
|
|
</QBadge>
|
|
</QCardSection>
|
|
|
|
<QSeparator />
|
|
|
|
<QCardSection>
|
|
<QSelect
|
|
v-if="card.skinOptions"
|
|
v-model="selectedScoreboardSkin"
|
|
class="q-mb-md"
|
|
dark
|
|
dense
|
|
emit-value
|
|
map-options
|
|
outlined
|
|
:label="t('graphicsSkinLabel')"
|
|
:options="card.skinOptions"
|
|
/>
|
|
|
|
<div class="row items-center q-gutter-sm">
|
|
<QBtn
|
|
:color="copiedCardId === card.id ? 'positive' : 'primary'"
|
|
:icon="copiedCardId === card.id ? 'check' : 'content_copy'"
|
|
no-caps
|
|
:label="copiedCardId === card.id ? t('graphicsCopied') : t('graphicsCopyUrl')"
|
|
@click="copyUrl(card.graphic, card.id)"
|
|
/>
|
|
<QBtn
|
|
color="secondary"
|
|
icon="open_with"
|
|
no-caps
|
|
draggable="true"
|
|
:label="t('graphicsDragObs')"
|
|
@dragstart="onDragStart($event, card.graphic)"
|
|
/>
|
|
<QBtn
|
|
color="grey-7"
|
|
icon="open_in_new"
|
|
no-caps
|
|
:label="t('graphicsOpenBrowser')"
|
|
@click="openUrl(card.graphic)"
|
|
/>
|
|
</div>
|
|
</QCardSection>
|
|
</QCard>
|
|
</div>
|
|
</div>
|
|
</QPage>
|
|
</template> |