Move MC mod versions to a different page

This commit is contained in:
Melon 2024-01-02 17:46:19 +00:00
parent 82fc180afe
commit 8740bb8979
Signed by: melon
GPG Key ID: 6C9D970C50D26A25
11 changed files with 328 additions and 221 deletions

View File

@ -1,9 +1,5 @@
# MrMelon54.com Frontend
[![Build Status](https://ci.mrmelon54.com/api/badges/melon/summer-ui/status.svg)](https://ci.mrmelon54.com/melon/summer-ui)
UI for [Summer](https://code.mrmelon54.com/melon/summer)
## Building
```bash

View File

@ -1,7 +0,0 @@
export type ButtonData = {
id: string;
cfId: string;
modrinth: string;
curseforge: string;
github: string;
};

25
src/api/mc-upload.ts Normal file
View File

@ -0,0 +1,25 @@
export interface McUploadItem {
name: string;
modrinth: McUploadItemPlatform;
curseforge: McUploadItemPlatform;
github: string;
}
export interface McUploadItemPlatform {
url: string;
id: string;
}
export interface McUploadVersion {
meta: {
version: string;
channel: string;
game_versions: string[];
loaders: string[];
environment: string;
};
filename: string;
sha512: string;
modrinth_id: string;
curseforge_id: string;
}

View File

@ -1,19 +0,0 @@
export type Project = {
id: number;
key: string;
title: string;
description: string;
url: string;
group_id: number;
activity: Activity;
};
export type Activity = {
id: number;
key: string;
title: string;
description: string;
value: number;
project_id: number;
status: string;
};

View File

@ -1,4 +1,8 @@
<div class="center-screen-wrapper">
<script lang="ts">
export let noCenter = true;
</script>
<div class="center-screen-wrapper" class:no-center={noCenter}>
<div class="center-screen">
<slot />
</div>
@ -13,6 +17,10 @@
min-height: calc(100vh - 50px);
box-sizing: border-box;
&.no-center {
place-items: start;
}
> .center-screen {
max-width: min(100%, 2560px);
margin: 0 auto;

View File

@ -9,7 +9,7 @@
<div id="app-router">
<Header {isHome} />
<main>
<CenterScreen>
<CenterScreen noCenter={!isHome}>
<slot />
</CenterScreen>
</main>

View File

@ -1,7 +1,7 @@
<script lang="ts">
import {navigate} from "vite-plugin-ssr/client/router";
import MetaTags from "~/components/MetaTags.svelte";
import Layout from "./__layout.svelte";
import Layout from "~/pages/__layout.svelte";
export let __;
export let pageProps;

View File

@ -64,7 +64,7 @@
display: flex;
flex-direction: row;
background: var(--bg-panel);
border-radius: 16px;
border-radius: 14px;
-webkit-box-shadow: 0px 0px 10px 2px rgba(0, 0, 0, 0.5);
-moz-box-shadow: 0px 0px 10px 2px rgba(0, 0, 0, 0.5);
box-shadow: 0px 0px 10px 2px rgba(0, 0, 0, 0.5);
@ -77,7 +77,7 @@
img {
aspect-ratio: 1/1;
width: 100%;
border-radius: 16px;
border-radius: calc(100% / 16);
}
}

View File

@ -1,31 +1,19 @@
<script lang="ts">
import type {ModData, VersionData} from "~/api/modrinth";
import type {Project} from "~/api/timeline";
import type {ButtonData} from "~/api/button";
import type {ModData} from "~/api/modrinth";
import LazyComponent from "~/lib/LazyComponent.svelte";
import ModrinthLogo from "~/icons/ModrinthLogo.svelte";
import CurseforgeLogo from "~/icons/CurseforgeLogo.svelte";
import GithubLogo from "~/icons/GithubLogo.svelte";
import {modStore} from "~/stores/minecraft-cache";
import MetaTags from "~/components/MetaTags.svelte";
import Layout from "../__layout.svelte";
import {onMount} from "svelte";
import Layout from "~/pages/__layout.svelte";
import type {McUploadItem} from "~/api/mc-upload";
export const props = ["project"];
export let __;
let modData: ModData;
let buttonData: ButtonData;
let updateData: Promise<Project> = new Promise((res, rej) => {});
let versionData: Promise<VersionData[]> = new Promise((res, rej) => {});
onMount(() => {
updateData = new Promise((res, rej) => {
fetch(`https://api.mrmelon54.com/v1/timeline/project/minecraft/${__.routeParams.project}`)
.then(resp => res(resp.json()))
.catch(err => rej(err));
});
});
let buttonData: McUploadItem | null;
modStore.subscribe(x => {
if (x instanceof Error) {
@ -33,78 +21,12 @@
buttonData = null;
} else if (x) {
modData = x.projectsSlugMap[__.routeParams.project];
buttonData = modData ? x.modAlias[modData.id] : null;
versionData = new Promise((res, rej) => {
fetch(`https://api.modrinth.com/v2/versions?ids=${JSON.stringify(modData.versions)}`)
.then(resp => res(resp.json()))
.catch(err => rej(err));
});
buttonData = x.modAlias[__.routeParams.project];
} else {
modData = null;
buttonData = null;
}
});
type Version = {type: "version"; value: number[]};
type Range = {type: "range"; min: number[]; max: number[]};
function parseVersion(a: string): Version {
let v = a.split(".").map(x => parseInt(x));
if (v.length == 2) v.push(0);
return {type: "version", value: v};
}
function mergeVersions(a: Version | Range, b: Version): Range {
if (a.type === "version") {
let z = [...a.value];
z[z.length - 1]++;
if (arrayEquals(z, b.value)) return {type: "range", min: a.value, max: b.value};
} else if (a.type === "range") {
let z = [...a.max];
z[z.length - 1]++;
if (arrayEquals(z, b.value)) return {type: "range", min: a.min, max: b.value};
}
return null;
}
function squashVersions(a: Version[]): Array<Version | Range> {
if (a.length == 0) return [];
let out: Array<Version | Range> = [];
let z: Version | Range = a[0];
for (let i = 1; i < a.length; i++) {
let m = mergeVersions(z, a[i]);
if (m == null) {
out.push(z);
z = a[i];
} else {
z = m;
}
}
out.push(z);
return out;
}
function renderGameVersions(a: string[]) {
let b = a.map(x => parseVersion(x));
if (b.length == 0) return "";
let squash = squashVersions(b);
return squash.map(x => renderVersion(x)).join(", ");
}
function renderVersion(a: Version | Range): string {
if (a.type === "version") {
if (a.value[a.value.length - 1] === 0) a.value.splice(a.value.length - 1, 1);
return a.value.map(x => x.toString()).join(".");
} else if (a.type === "range") {
return renderVersion({type: "version", value: a.min}) + " - " + renderVersion({type: "version", value: a.max});
}
return "unknown";
}
function arrayEquals(a, b) {
return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((val, index) => val === b[index]);
}
</script>
<MetaTags
@ -119,10 +41,6 @@
<div class="mod-meta">
<img class="title-img" src={modData.icon_url} alt={modData.title} />
<h1 class="title-text">{modData.title}</h1>
<div class="platform-text">
<div>Modrinth ID: {buttonData.id}</div>
<div>CurseForge ID: {buttonData.cfId}</div>
</div>
{#if buttonData}
<div class="link-buttons">
<a href={buttonData.github} class="brand-button-wrapper" rel="noreferrer" target="_blank">
@ -131,52 +49,30 @@
<span>Source Code</span>
</div>
</a>
<a href={buttonData.modrinth} class="brand-button-wrapper" rel="noreferrer" target="_blank">
<div class="brand-button button-modrinth">
<ModrinthLogo />
<span>Modrinth</span>
</div>
</a>
<a href={buttonData.curseforge} class="brand-button-wrapper" rel="noreferrer" target="_blank">
<div class="brand-button button-curseforge">
<CurseforgeLogo />
<span>CurseForge</span>
</div>
</a>
{#if buttonData.modrinth}
<a href={buttonData.modrinth.url} class="brand-button-wrapper" rel="noreferrer" target="_blank">
<div class="brand-button button-modrinth">
<ModrinthLogo />
<span>Modrinth</span>
</div>
</a>
{/if}
{#if buttonData.curseforge}
<a href={buttonData.curseforge.url} class="brand-button-wrapper" rel="noreferrer" target="_blank">
<div class="brand-button button-curseforge">
<CurseforgeLogo />
<span>CurseForge</span>
</div>
</a>
{/if}
</div>
{:else}
<div class="buttons-loading" />
{/if}
<div class="game-versions">
{#await versionData then w}
{#each w as v, i}
<a class="version-pill" rel="noreferrer" target="_blank" href="https://modrinth.com/mod/{modData.slug}/version/{v.id}">
<span>{renderGameVersions(v.game_versions)}</span>
</a>
{/each}
{:catch}
<div data-text="No version data" />
{/await}
</div>
{#await updateData}
<div class="progress">
<div class="progress-bar progress-infinite" />
</div>
{:then w}
<div class="progress">
<div class="progress-title"><h2>Progress: {w.activity.title} (<span class="progress-status">{w.activity.status}</span>)</h2></div>
<div class="progress-details">
<p class="progress-text">{w.activity.description}</p>
</div>
<div class="progress-bar">
<div class="progress-bar-done" style="width: {w.activity.value}%;">
<span class="progress-bar-text">{w.activity.value + "%"}</span>
</div>
</div>
</div>
{:catch}
<div data-text="No update data" />
{/await}
</div>
<div class="body-tabs">
<a href="/minecraft/{__.routeParams.project}" class="selected">Description</a>
<a href="/minecraft/{__.routeParams.project}/versions">Versions</a>
</div>
<div class="body-text">
<LazyComponent component={() => import("~/components/Markdown.svelte")} delayMs={500} source={modData.body}>Loading...</LazyComponent>
@ -193,7 +89,7 @@
.title-img {
width: max(25%, 100px);
aspect-ratio: 1/1;
border-radius: 32px;
border-radius: calc(100% / 16);
margin-bottom: 32px;
-webkit-box-shadow: 0px 0px 10px 2px rgba(0, 0, 0, 0.5);
-moz-box-shadow: 0px 0px 10px 2px rgba(0, 0, 0, 0.5);
@ -209,42 +105,17 @@
.platform-text {
margin: 0 0 16px 0;
}
}
.progress {
margin-bottom: 32px;
.body-tabs {
display: flex;
margin-bottom: 32px;
> .progress-bar {
width: 100%;
height: 32px;
border-radius: 24px;
background-color: var(--bg-panel);
overflow: hidden;
position: relative;
-webkit-box-shadow: 0px 0px 10px 2px rgba(0, 0, 0, 0.5);
-moz-box-shadow: 0px 0px 10px 2px rgba(0, 0, 0, 0.5);
box-shadow: 0px 0px 10px 2px rgba(0, 0, 0, 0.5);
> a {
padding: 16px;
> .progress-bar-done {
transform: translateX(-100%);
-webkit-animation: loadBar 1s forwards;
animation: loadBar 1s forwards;
height: 100%;
background-color: var(--primary-main);
@keyframes loadBar {
100% {
transform: translateX(0);
}
}
> .progress-bar-text {
position: absolute;
top: 50%;
right: 10px;
transform: translateY(-50%);
font-family: monospace;
}
}
&.selected {
border-bottom: 3px solid var(--primary-main);
}
}
}
@ -262,18 +133,4 @@
display: none;
}
}
.game-versions {
display: flex;
justify-content: center;
gap: 16px;
margin-bottom: 32px;
.version-pill {
background: var(--primary-main);
padding: 6px 12px;
border-radius: 0.5rem;
color: var(--primary-text);
}
}
</style>

View File

@ -0,0 +1,252 @@
<script lang="ts">
import type {ModData} from "~/api/modrinth";
import ModrinthLogo from "~/icons/ModrinthLogo.svelte";
import CurseforgeLogo from "~/icons/CurseforgeLogo.svelte";
import GithubLogo from "~/icons/GithubLogo.svelte";
import {modStore} from "~/stores/minecraft-cache";
import MetaTags from "~/components/MetaTags.svelte";
import Layout from "~/pages/__layout.svelte";
import type {McUploadItem, McUploadVersion} from "~/api/mc-upload";
export const props = ["project"];
export let __;
let modData: ModData;
let buttonData: McUploadItem | null;
let versionData: Promise<McUploadVersion[]> = new Promise(() => {});
modStore.subscribe(x => {
if (x instanceof Error) {
modData = null;
buttonData = null;
} else if (x) {
modData = x.projectsSlugMap[__.routeParams.project];
buttonData = x.modAlias[__.routeParams.project];
versionData = new Promise((res, rej) => {
fetch(`https://api.mrmelon54.com/v1/mc-upload/mod/${__.routeParams.project}/versions`)
.then(resp => res(resp.json()))
.catch(err => rej(err));
});
} else {
modData = null;
buttonData = null;
}
});
type Version = {type: "version"; value: number[]};
type Range = {type: "range"; min: number[]; max: number[]};
function parseVersion(a: string): Version {
let v = a.split(".").map(x => parseInt(x));
if (v.length == 2) v.push(0);
return {type: "version", value: v};
}
function mergeVersions(a: Version | Range, b: Version): Range {
if (a.type === "version") {
let z = [...a.value];
z[z.length - 1]++;
if (arrayEquals(z, b.value)) return {type: "range", min: a.value, max: b.value};
} else if (a.type === "range") {
let z = [...a.max];
z[z.length - 1]++;
if (arrayEquals(z, b.value)) return {type: "range", min: a.min, max: b.value};
}
return null;
}
function squashVersions(a: Version[]): Array<Version | Range> {
if (a.length == 0) return [];
let out: Array<Version | Range> = [];
let z: Version | Range = a[0];
for (let i = 1; i < a.length; i++) {
let m = mergeVersions(z, a[i]);
if (m == null) {
out.push(z);
z = a[i];
} else {
z = m;
}
}
out.push(z);
return out;
}
function renderGameVersions(a: string[]) {
let b = a.map(x => parseVersion(x));
if (b.length == 0) return "";
b = b.sort((a, b) => {
if (a.value[0] != b.value[0]) return a.value[0] - b.value[0];
if (a.value[1] != b.value[1]) return a.value[1] - b.value[1];
if (a.value[2] != b.value[2]) return a.value[2] - b.value[2];
return 0;
});
let squash = squashVersions(b);
return squash.map(x => renderVersion(x)).join(", ");
}
function renderVersion(a: Version | Range): string {
if (a.type === "version") {
if (a.value[a.value.length - 1] === 0) a.value.splice(a.value.length - 1, 1);
return a.value.map(x => x.toString()).join(".");
} else if (a.type === "range") {
return renderVersion({type: "version", value: a.min}) + " - " + renderVersion({type: "version", value: a.max});
}
return "unknown";
}
function arrayEquals(a, b) {
return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((val, index) => val === b[index]);
}
</script>
<MetaTags
url={__.urlOriginal}
title={(modData ? `${modData.title} Versions | ` : "") + "Minecraft | MrMelon54"}
description=""
keywords="minecraft,minecraft mod,{__.routeParams.project}"
/>
<Layout>
{#if modData}
<div class="mod-meta">
<img class="title-img" src={modData.icon_url} alt={modData.title} />
<h1 class="title-text">{modData.title}</h1>
{#if buttonData}
<div class="link-buttons">
<a href={buttonData.github} class="brand-button-wrapper" rel="noreferrer" target="_blank">
<div class="brand-button button-github">
<GithubLogo />
<span>Source Code</span>
</div>
</a>
{#if buttonData.modrinth}
<a href={buttonData.modrinth.url} class="brand-button-wrapper" rel="noreferrer" target="_blank">
<div class="brand-button button-modrinth">
<ModrinthLogo />
<span>Modrinth</span>
</div>
</a>
{/if}
{#if buttonData.curseforge}
<a href={buttonData.curseforge.url} class="brand-button-wrapper" rel="noreferrer" target="_blank">
<div class="brand-button button-curseforge">
<CurseforgeLogo />
<span>CurseForge</span>
</div>
</a>
{/if}
</div>
{:else}
<div class="buttons-loading" />
{/if}
</div>
<div class="body-tabs">
<a href="/minecraft/{__.routeParams.project}">Description</a>
<a href="/minecraft/{__.routeParams.project}/versions" class="selected">Versions</a>
</div>
<div class="game-versions">
{#await versionData then w}
<table>
{#each w.filter(x => x.modrinth_id != "" || x.curseforge_id != "").reverse() as v}
<tr>
<th>{v.meta.version}</th>
<td>{renderGameVersions(v.meta.game_versions)}</td>
<td>
{#if v.modrinth_id != ""}
<a class="version-pill mr-dl" rel="noreferrer" target="_blank" href="{buttonData.modrinth.url}/version/{v.modrinth_id}">
<span>Modrinth</span>
</a>
{/if}
</td>
<td>
{#if v.curseforge_id != ""}
<a class="version-pill cf-dl" rel="noreferrer" target="_blank" href="{buttonData.curseforge.url}/files/{v.curseforge_id}">
<span>Curseforge</span>
</a>
{/if}
</td>
</tr>
{/each}
</table>
{:catch}
<div data-text="No version data" />
{/await}
</div>
{:else}
<div class="projects-loading" />
{/if}
</Layout>
<style lang="scss">
@import "../../../styles/link-buttons.scss";
.mod-meta {
.title-img {
width: max(25%, 100px);
aspect-ratio: 1/1;
border-radius: calc(100% / 16);
margin-bottom: 32px;
-webkit-box-shadow: 0px 0px 10px 2px rgba(0, 0, 0, 0.5);
-moz-box-shadow: 0px 0px 10px 2px rgba(0, 0, 0, 0.5);
box-shadow: 0px 0px 10px 2px rgba(0, 0, 0, 0.5);
}
.title-text {
margin: 0 0 16px 0;
font-size: 3.2em;
line-height: 1.1;
}
.platform-text {
margin: 0 0 16px 0;
}
}
.body-tabs {
display: flex;
margin-bottom: 32px;
> a {
padding: 16px;
&.selected {
border-bottom: 3px solid var(--primary-main);
}
}
}
.game-versions {
display: flex;
margin-bottom: 32px;
table {
width: 100%;
border-collapse: collapse;
th,
td {
padding: 8px;
}
tr:nth-child(2n) {
background-color: var(--bg-panel);
}
}
.version-pill {
padding: 6px 12px;
border-radius: 0.5rem;
color: var(--primary-text);
&.mr-dl {
background: green;
}
&.cf-dl {
background: orangered;
}
}
}
</style>

View File

@ -1,28 +1,23 @@
import {writable} from "svelte/store";
import {PromiseAllUnique} from "~/utils/promise-all-unique";
import type {ModData} from "~/api/modrinth";
import type { McUploadItem } from "~/api/mc-upload";
export interface ModStore {
projects: ModData[];
projectsIdMap: Map<string, ModData>;
projectsSlugMap: Map<string, ModData>;
modAlias: Map<string, ModStoreItem>;
modAlias: Map<string, McUploadItem>;
}
export interface ModStoreItem {
id: string;
github: string;
modrinth: string;
cfId: string;
curseforge: string;
}
export const modStore = writable<ModStore | Error | null>(
(() => {
(() => {
PromiseAllUnique([
fetch("https://api.modrinth.com/v2/user/mrmelon54/projects").then(resp => resp.json()),
fetch("https://cdn.mrmelon54.com/assets/minecraft/mods.json").then(resp => resp.json()),
fetch("https://api.mrmelon54.com/v1/mc-upload/summary").then(resp => resp.json()),
])
.then(([projects, modAlias]) => {
let projectsIdMap: Map<string, ModData> = new Map();