Update Video View 2.0

This commit is contained in:
Trenton Zimmer
2026-09-12 16:10:45 -04:00
parent e02cd9be53
commit 937d1539ef
2 changed files with 247 additions and 79 deletions

View File

@@ -55,5 +55,5 @@
"3.1.6": ["- (Major) Add Song Record Cards for all games that support scores", "- (Major) Update layout and design of a core component, CardBoxStat", "- (Major) Overhaul the game timeline", "- (Bugfix) Various fixes over the past week", "- (Greetings) Add new greetings from superaustin7"],
"3.1.7": ["- (Major) Update SDVX 6 customizations", "- (Major) Add new DDR customization cards (archibun)", "- (Minor) Add video backgrounds for pop'n 22 -> 28, SDVX 1-6 (archibun)"],
"3.1.8": ["- (Major) Add support for Gitadora GALAXY WAVE", "- (Feature) Add pop'n music characters to webui (archibun)", "- (Minor) Add flare skill calculation to DDR", "- (Minor) Update game event settings for DDR (archibun)"],
"3.1.9": ["- (Major) Add beta support for IIDX Sparkle Shower", "- (Feature) Add new DDR score statistics to records (archibun)", "- (hotfix) Add modal teleporter, allowing for better modals", "- (hotfix) Add blank ddr customizations (archibun)", "- (hotfix) Update/fix global custom scrollbar", "- (hotfix) Add support for customization groups (archibun)", "- (hotfix) Update SDVX EG event options (archibun)", "- (hotfix) Start working on top secret feature 👀", "- (hotfix) Updated GitaDora support", "- (hotfix) Updated IIDX33 support", "- (hotfix) Add customization background videos to game header"]
"3.1.9": ["- (Major) Add new video viewer", "- (Major) Add beta support for IIDX Sparkle Shower", "- (Feature) Add new DDR score statistics to records (archibun)", "- (hotfix) Add modal teleporter, allowing for better modals", "- (hotfix) Add blank ddr customizations (archibun)", "- (hotfix) Update/fix global custom scrollbar", "- (hotfix) Add support for customization groups (archibun)", "- (hotfix) Update SDVX EG event options (archibun)", "- (hotfix) Start working on top secret feature 👀", "- (hotfix) Updated GitaDora support", "- (hotfix) Updated IIDX33 support", "- (hotfix) Add customization background videos to game header"]
}

View File

@@ -1,13 +1,18 @@
<script setup>
import { ref, onMounted } from "vue";
import { PhFilmReel, PhVideo } from "@phosphor-icons/vue";
import { ref, computed, onMounted } from "vue";
import {
PhFilmReel,
PhPlay,
PhUploadSimple,
PhVideo,
} from "@phosphor-icons/vue";
import SectionMain from "@/components/SectionMain.vue";
import CardBox from "@/components/CardBox.vue";
import BaseButton from "@/components/BaseButton.vue";
import BaseIcon from "@/components/BaseIcon.vue";
import UserCard from "@/components/UserCard.vue";
import LayoutAuthenticated from "@/layouts/LayoutAuthenticated.vue";
import SectionTitleLine from "@/components/SectionTitleLine.vue";
import GeneralTable from "@/components/GeneralTable.vue";
import { getGameInfo } from "@/constants";
import { formatSortableDate } from "@/constants/date";
@@ -19,45 +24,45 @@ const videoData = ref([]);
const loading = ref(false);
const musicData = ref({});
const newestVideo = ref(null);
const selectedVideo = ref(newestVideo);
const headers = [
{
text: "Timestamp",
value: "timestamp",
sortable: true,
width: 120,
},
{
text: "Song",
value: "name",
sortable: true,
width: 120,
},
{
text: "Artist",
value: "artist",
sortable: true,
width: 120,
},
{
text: "Game",
value: "game",
sortable: true,
width: 120,
},
{
text: "Version",
value: "version",
sortable: true,
width: 120,
},
{
text: "Status",
value: "data.status",
sortable: true,
width: 120,
},
];
const groupedVideos = computed(() => {
const groups = [];
for (const video of videoData.value) {
let dateGroup = groups.find((group) => group.key === video.dateKey);
if (!dateGroup) {
dateGroup = {
key: video.dateKey,
label: video.dateLabel,
games: [],
};
groups.push(dateGroup);
}
let gameGroup = dateGroup.games.find((group) => group.key === video.gameId);
if (!gameGroup) {
gameGroup = {
key: video.gameId,
name: video.game,
videos: [],
};
dateGroup.games.push(gameGroup);
}
gameGroup.videos.push(video);
}
return groups;
});
const selectVideo = (video) => {
selectedVideo.value = video;
};
async function loadVideos() {
loading.value = true;
@@ -66,19 +71,24 @@ async function loadVideos() {
const data = await APIGetPlayVideos();
const videos = filterVideos(JSON.parse(JSON.stringify(data)));
const musicIdsByGameVersion = {};
for (const video of videos) {
const game = video.gameId;
const version = video.versionId;
if (!musicIdsByGameVersion[game]) {
musicIdsByGameVersion[game] = {};
}
if (!musicIdsByGameVersion[game][version]) {
musicIdsByGameVersion[game][version] = new Set();
}
musicIdsByGameVersion[game][version].add(video.musicid);
}
const musicRequests = [];
for (const [game, versions] of Object.entries(musicIdsByGameVersion)) {
for (const [version, musicIds] of Object.entries(versions)) {
musicRequests.push(
@@ -93,11 +103,14 @@ async function loadVideos() {
const musicResults = await Promise.all(musicRequests);
const sortedMusicData = {};
for (const { game, version, songs } of musicResults) {
if (!sortedMusicData[game]) {
sortedMusicData[game] = {};
}
const songMap = Object.fromEntries(songs.map((song) => [song.id, song]));
sortedMusicData[game][version] = Object.fromEntries(
Object.entries(songMap).sort(([a], [b]) => Number(a) - Number(b)),
);
@@ -124,7 +137,9 @@ async function loadVideos() {
artist: song?.artist || "Unknown Artist",
};
});
newestVideo.value = videoData.value[0];
newestVideo.value = videoData.value[0] ?? null;
selectedVideo.value = newestVideo.value;
} catch (error) {
console.error("Failed to fetch video data:", error);
} finally {
@@ -149,6 +164,19 @@ function filterVideos(playVideos) {
video.version =
game.versions.find((x) => x.id == video.versionId)?.label ?? 0;
const timestamp =
video.timestamp < 100000000000 ? video.timestamp * 1000 : video.timestamp;
const date = new Date(timestamp);
video.dateKey = date.toISOString().slice(0, 10);
video.dateLabel = date.toLocaleDateString(undefined, {
weekday: "long",
month: "long",
day: "numeric",
year: "numeric",
});
if (video.timestamp) {
video.timestamp = formatSortableDate(video.timestamp);
}
@@ -171,10 +199,6 @@ const copyToClipboard = (text) => {
alert("Failed to copy to clipboard!");
});
};
function openInNewTab(url) {
window.open(url.data?.url, "_blank").focus();
}
</script>
<template>
@@ -183,74 +207,218 @@ function openInNewTab(url) {
<UserCard class="mb-6" use-small even-smaller />
<template v-if="newestVideo">
<SectionTitleLine :icon="PhVideo" title="Your Latest Video" main />
<SectionTitleLine :icon="PhVideo" title="Video Player" main />
<CardBox class="mb-6">
<div
v-if="newestVideo?.data?.status == 'uploaded'"
class="grid grid-cols-1 md:grid-cols-2 gap-4"
v-if="selectedVideo?.data?.status === 'uploaded'"
class="grid grid-cols-1 gap-6 xl:grid-cols-[minmax(0,2fr)_minmax(320px,1fr)]"
>
<video controls class="mt-3 rounded-xl w-160 xl:w-7xl">
<source :src="newestVideo?.data?.url" type="video/mp4" />
Your browser does not support the video tag.
</video>
<div>
<video
:key="selectedVideo?.data?.url"
controls
autoplay
class="aspect-video w-full rounded-xl bg-black object-contain"
>
<source :src="selectedVideo?.data?.url" type="video/mp4" />
Your browser does not support the video tag.
</video>
</div>
<div
class="text-center md:text-left grid grid-cols-1 place-content-start gap-6"
class="grid grid-cols-1 content-start gap-6 text-center xl:text-left"
>
<div class="space-y-2">
<div>
<h1 class="text-2xl xl:text-4xl font-bold">
{{ newestVideo?.name }}
</h1>
<h1 class="text-xl xl:text-3xl font-semibold">
{{ newestVideo?.artist }}
<h1 class="text-2xl font-bold xl:text-4xl">
{{ selectedVideo?.name }}
</h1>
<h2 class="text-xl font-semibold xl:text-3xl">
{{ selectedVideo?.artist }}
</h2>
</div>
<BaseDivider class="mx-1" />
<h2 class="text-xl xl:text-2xl font-light">
{{ newestVideo?.game }} {{ newestVideo?.version }}
</h2>
<p>{{ newestVideo?.timestamp }}</p>
<h3 class="text-xl font-light xl:text-2xl">
{{ selectedVideo?.game }}
{{ selectedVideo?.version }}
</h3>
<p>
{{ selectedVideo?.timestamp }}
</p>
</div>
<div>
<div class="flex justify-center gap-2 xl:justify-start">
<BaseButton
color="info"
label="Share"
@click="copyToClipboard(newestVideo?.data?.url)"
@click="copyToClipboard(selectedVideo?.data?.url)"
/>
</div>
</div>
</div>
<div v-else>
<div class="space-y-1 text-center">
<h1 class="text-2xl xl:text-4xl font-bold">
<h1 class="text-2xl font-bold xl:text-4xl">
Your video is still uploading!
</h1>
<h2 class="text-xl xl:text-2xl font-light">
<h2 class="text-xl font-light xl:text-2xl">
Please wait for the upload to complete.
</h2>
<h2 class="text-xl xl:text-xl">
Upload started at {{ newestVideo.timestamp }}
<h2 class="text-xl">
Upload started at {{ selectedVideo?.timestamp }}
</h2>
</div>
</div>
</CardBox>
</template>
<SectionTitleLine :icon="PhFilmReel" title="All Play Videos" main />
<CardBox has-table>
<div class="flex items-center justify-between">
<SectionTitleLine :icon="PhFilmReel" title="Video Library" main />
<div
class="bg-white dark:bg-slate-900/95 rounded-2xl lg:flex lg:justify-between"
class="-mt-4 flex items-center rounded-2xl bg-slate-900 p-3.5 drop-shadow-2xl"
>
<div class="w-full">
<GeneralTable
:headers="headers"
:items="videoData"
@row-clicked="openInNewTab"
/>
<span>{{ videoData.length }} videos</span>
</div>
</div>
<CardBox>
<div class="space-y-10">
<div
v-for="dateGroup in groupedVideos"
:key="dateGroup.key"
class="space-y-5"
>
<div class="flex items-center gap-4">
<div class="h-px flex-1 bg-slate-800" />
<h2 class="text-sm font-semibold tracking-wider text-slate-400">
{{ dateGroup.label }}
</h2>
<div class="h-px flex-1 bg-slate-800" />
</div>
<div
v-for="gameGroup in dateGroup.games"
:key="gameGroup.key"
class="space-y-3"
>
<div class="flex items-center gap-2">
<h3
class="text-base font-semibold text-slate-700 dark:text-slate-200"
>
{{ gameGroup.name }}
</h3>
<span
class="rounded-full bg-slate-100 px-2 py-0.5 text-xs font-medium text-slate-500 dark:bg-slate-800 dark:text-slate-400"
>
{{ gameGroup.videos.length }}
</span>
</div>
<div
class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"
>
<button
v-for="video in gameGroup.videos"
:key="video.id"
type="button"
class="group overflow-hidden rounded-xl border text-left transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-info"
:class="
selectedVideo?.id === video.id
? 'border-blue-400 bg-slate-800 ring-2 ring-blue-400'
: 'border-slate-800 hover:border-info/60 hover:shadow-md'
"
@click="selectVideo(video)"
>
<div
class="relative aspect-video overflow-hidden bg-slate-900"
>
<video
v-if="video?.data?.status === 'uploaded'"
:src="video?.data?.url"
preload="metadata"
muted
playsinline
class="h-full w-full object-cover transition-transform duration-300 group-hover:scale-105"
/>
<div
v-if="video?.data?.status === 'uploaded'"
class="absolute inset-0 flex items-center justify-center bg-black/10 transition-colors group-hover:bg-black/30"
>
<div
class="flex h-12 w-12 items-center justify-center rounded-full bg-slate-200/90 text-slate-800/90 shadow-lg transition-transform group-hover:scale-110"
>
<template v-if="selectedVideo?.id === video.id">
<BaseIcon
:icon="PhPlay"
:size="24"
class="animate-pulse text-emerald-700"
/>
</template>
<template v-else>
<BaseIcon :icon="PhPlay" :size="24" />
</template>
</div>
</div>
<div
v-else
class="absolute inset-0 flex flex-col items-center justify-center bg-slate-900"
>
<BaseIcon :icon="PhUploadSimple" :size="20" />
<span class="text-sm font-medium"> Uploading </span>
</div>
<div
v-if="selectedVideo?.id === video.id"
class="absolute right-2 top-2 rounded-full bg-info px-2 py-1 text-xs font-semibold text-slate-100 drop-shadow-2xl"
>
<span>Selected</span>
</div>
</div>
<div class="space-y-1 p-4">
<h3 class="truncate font-semibold" :title="video?.name">
{{ video?.name }}
</h3>
<p
class="truncate text-sm text-slate-500 dark:text-slate-400"
>
{{ video?.artist }}
</p>
<div
class="flex items-center justify-between gap-2 pt-2 text-xs text-slate-400"
>
<span class="truncate">
{{ video?.version }}
</span>
<span class="shrink-0">
{{ video?.timestamp }}
</span>
</div>
</div>
</button>
</div>
</div>
</div>
<div
v-if="!videoData.length"
class="py-12 text-center text-slate-500 dark:text-slate-400"
>
<h3
class="text-lg font-semibold text-slate-700 dark:text-slate-200"
>
No videos yet
</h3>
<p class="mt-1">Your uploaded videos will appear here.</p>
</div>
</div>
</CardBox>