mirror of
https://github.com/PhaseII-eAmusement-Network/PhaseWeb3-Vue.git
synced 2026-09-28 11:46:30 -05:00
Update Video View 2.0
This commit is contained in:
@@ -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"]
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user