Compare commits
2 commits
8dae161d7c
...
8e8e82350d
Author | SHA1 | Date | |
---|---|---|---|
8e8e82350d | |||
5fc85b610f |
9 changed files with 435 additions and 77 deletions
|
@ -46,11 +46,9 @@
|
||||||
"@auth/sveltekit": "^0.2.2",
|
"@auth/sveltekit": "^0.2.2",
|
||||||
"@ltd/j-toml": "^1.38.0",
|
"@ltd/j-toml": "^1.38.0",
|
||||||
"@rgossiaux/svelte-headlessui": "^1.0.2",
|
"@rgossiaux/svelte-headlessui": "^1.0.2",
|
||||||
"@xmcl/modrinth": "^1.1.0",
|
|
||||||
"minio": "^7.0.32",
|
"minio": "^7.0.32",
|
||||||
"nano": "^10.1.2",
|
"nano": "^10.1.2",
|
||||||
"nanoid": "^4.0.1",
|
"nanoid": "^4.0.1",
|
||||||
"octokit": "^2.0.14",
|
|
||||||
"svelte-feather-icons": "^4.0.0",
|
"svelte-feather-icons": "^4.0.0",
|
||||||
"zod": "^3.20.6"
|
"zod": "^3.20.6"
|
||||||
}
|
}
|
||||||
|
|
76
src/lib/modrinth.ts
Normal file
76
src/lib/modrinth.ts
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export const searchProjectsHit = z.object({
|
||||||
|
slug: z.string().regex(/^[\w!@$()`.+,"\-']{3,64}$/),
|
||||||
|
title: z.string(),
|
||||||
|
description: z.string(),
|
||||||
|
categories: z.string().array().default([]),
|
||||||
|
client_side: z.enum(['required', 'optional', 'unsupported', 'unknown']),
|
||||||
|
server_side: z.enum(['required', 'optional', 'unsupported', 'unknown']),
|
||||||
|
project_type: z.enum(['mod', 'modpack', 'resourcepack', 'shader']),
|
||||||
|
downloads: z.number(),
|
||||||
|
icon_url: z.union([z.string().url(), z.literal('').transform(() => '/favicon.png')]),
|
||||||
|
color: z.number().nullable(),
|
||||||
|
project_id: z.string(),
|
||||||
|
author: z.string(),
|
||||||
|
display_categories: z.string().array().default([]),
|
||||||
|
versions: z.string().array(),
|
||||||
|
follows: z.number(),
|
||||||
|
date_created: z.string().datetime({ offset: true }),
|
||||||
|
date_modified: z.string().datetime({ offset: true }),
|
||||||
|
latest_version: z.string().optional(),
|
||||||
|
license: z.string(),
|
||||||
|
gallery: z.string().array().default([]),
|
||||||
|
featured_gallery: z.string().nullable()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const searchProjectsResponse = z.object({
|
||||||
|
hits: searchProjectsHit.array(),
|
||||||
|
offset: z.number(),
|
||||||
|
limit: z.number(),
|
||||||
|
total_hits: z.number()
|
||||||
|
});
|
||||||
|
|
||||||
|
const searchProjectsRequest = z.object({
|
||||||
|
query: z.string().optional(),
|
||||||
|
facets: z
|
||||||
|
.string()
|
||||||
|
.regex(/^(categories|versions|license|project_type):(.+)$/)
|
||||||
|
.array()
|
||||||
|
.array()
|
||||||
|
.default([]),
|
||||||
|
index: z.enum(['relevance', 'downloads', 'follows', 'newest', 'updated']).default('relevance'),
|
||||||
|
offset: z.number().default(0),
|
||||||
|
limit: z.number().default(10),
|
||||||
|
filters: z.string().optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
function searchProjectsRequestToUrlParams(
|
||||||
|
request: z.infer<typeof searchProjectsRequest>
|
||||||
|
): URLSearchParams {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (typeof request.query != 'undefined') {
|
||||||
|
params.set('query', request.query);
|
||||||
|
}
|
||||||
|
params.set('facets', JSON.stringify(request.facets));
|
||||||
|
params.set('index', request.index);
|
||||||
|
params.set('offset', request.offset.toString());
|
||||||
|
params.set('limit', request.limit.toString());
|
||||||
|
if (typeof request.filters != 'undefined') {
|
||||||
|
params.set('filters', request.filters);
|
||||||
|
}
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function searchProjects(
|
||||||
|
request: z.input<typeof searchProjectsRequest>
|
||||||
|
): Promise<z.infer<typeof searchProjectsResponse>> {
|
||||||
|
const parsedRequest = searchProjectsRequest.parse(request);
|
||||||
|
const params = searchProjectsRequestToUrlParams(parsedRequest);
|
||||||
|
const response = await fetch('https://api.modrinth.com/v2/search?' + params.toString(), {
|
||||||
|
headers: {
|
||||||
|
'User-Agent': 'NotModdermore/noversion (+https://git.skye.vg/me/not-moddermore/)'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return searchProjectsResponse.parse(await response.json());
|
||||||
|
}
|
|
@ -32,22 +32,22 @@ export const indexSchema = z.object({
|
||||||
preserve: z.boolean().default(false)
|
preserve: z.boolean().default(false)
|
||||||
})
|
})
|
||||||
.array(),
|
.array(),
|
||||||
update: z
|
update: z
|
||||||
.object({
|
.object({
|
||||||
curseforge: z
|
curseforge: z
|
||||||
.object({
|
.object({
|
||||||
'file-id': z.number(),
|
'file-id': z.number(),
|
||||||
'project-id': z.number()
|
'project-id': z.number()
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
modrinth: z
|
modrinth: z
|
||||||
.object({
|
.object({
|
||||||
'mod-id': z.string(),
|
'mod-id': z.string(),
|
||||||
version: z.string()
|
version: z.string()
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
})
|
})
|
||||||
.default({})
|
.default({})
|
||||||
});
|
});
|
||||||
|
|
||||||
// export const metafileSchema = z.object({
|
// export const metafileSchema = z.object({
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { fileTreeSchema } from '$lib/types-zod';
|
||||||
import { get_blobs } from '$lib/utils';
|
import { get_blobs } from '$lib/utils';
|
||||||
import type { Modpack } from '$lib/types';
|
import type { Modpack } from '$lib/types';
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
import { minioClient, nano } from '$lib/clients';
|
import { minioClient, nano } from '$lib/server/clients';
|
||||||
|
|
||||||
export const actions: Actions = {
|
export const actions: Actions = {
|
||||||
default: async (event) => {
|
default: async (event) => {
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import FlattenedFileList from './FlattenedFileList.svelte';
|
import FlattenedFileList from './FlattenedFileList.svelte';
|
||||||
|
import FileTree from './FileTree.svelte';
|
||||||
|
|
||||||
import type { Folder } from '$lib/types';
|
import type { File, Folder } from '$lib/types';
|
||||||
import {
|
import {
|
||||||
Tab,
|
Tab,
|
||||||
TabGroup,
|
TabGroup,
|
||||||
|
@ -13,16 +14,25 @@
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogDescription
|
DialogDescription
|
||||||
} from '@rgossiaux/svelte-headlessui';
|
} from '@rgossiaux/svelte-headlessui';
|
||||||
import { PlusIcon } from 'svelte-feather-icons';
|
import { FolderIcon, PlusIcon } from 'svelte-feather-icons';
|
||||||
|
import {
|
||||||
|
searchProjects,
|
||||||
|
type searchProjectsHit,
|
||||||
|
type searchProjectsResponse
|
||||||
|
} from '$lib/modrinth';
|
||||||
|
import type { z } from 'zod';
|
||||||
|
import toml from '@ltd/j-toml';
|
||||||
let fileTree: Folder = {
|
let fileTree: Folder = {
|
||||||
type: 'folder',
|
type: 'folder',
|
||||||
children: {
|
children: {
|
||||||
mods: { type: 'folder', children: {
|
mods: {
|
||||||
"quark.pw.toml": {
|
type: 'folder',
|
||||||
type: 'file',
|
children: {
|
||||||
inline: true,
|
'quark.pw.toml': {
|
||||||
metafile: true,
|
type: 'file',
|
||||||
content: `name = "Quark"
|
inline: true,
|
||||||
|
metafile: true,
|
||||||
|
content: `name = "Quark"
|
||||||
filename = "Quark-3.4-389.jar"
|
filename = "Quark-3.4-389.jar"
|
||||||
side = "both"
|
side = "both"
|
||||||
|
|
||||||
|
@ -36,14 +46,15 @@ mode = "metadata:curseforge"
|
||||||
file-id = 4366541
|
file-id = 4366541
|
||||||
project-id = 243121
|
project-id = 243121
|
||||||
`,
|
`,
|
||||||
sha1: '478805ce54b082dadb1042ccfb46a68cfde3fc35',
|
sha1: '478805ce54b082dadb1042ccfb46a68cfde3fc35',
|
||||||
sha512: '5d364049ff53af2154ccae58dd53d2111ef592b21bff03705314cd6d12378cfeb6376b8bc3065ac59e1c9452447bec0eb345ecc19f0de1a17c20ea374f802f05'
|
sha512:
|
||||||
},
|
'5d364049ff53af2154ccae58dd53d2111ef592b21bff03705314cd6d12378cfeb6376b8bc3065ac59e1c9452447bec0eb345ecc19f0de1a17c20ea374f802f05'
|
||||||
"waystones.pw.toml": {
|
},
|
||||||
type: 'file',
|
'waystones.pw.toml': {
|
||||||
inline: true,
|
type: 'file',
|
||||||
metafile: true,
|
inline: true,
|
||||||
content: `name = "Waystones"
|
metafile: true,
|
||||||
|
content: `name = "Waystones"
|
||||||
filename = "waystones-forge-1.19-11.1.0.jar"
|
filename = "waystones-forge-1.19-11.1.0.jar"
|
||||||
side = "both"
|
side = "both"
|
||||||
|
|
||||||
|
@ -57,47 +68,239 @@ hash = "24c5403c1d5791f977a0ba69f08cf7959169c685"
|
||||||
mod-id = "LOpKHB2A"
|
mod-id = "LOpKHB2A"
|
||||||
version = "2sIhirkG"
|
version = "2sIhirkG"
|
||||||
`,
|
`,
|
||||||
sha1: '25f4b57fbd8a985b4872bb8fdadd14580b0f9f81',
|
sha1: '25f4b57fbd8a985b4872bb8fdadd14580b0f9f81',
|
||||||
sha512: '125fb6fc706363753c1c201192024415a23c1098c8e44d5deeaf5448215d83108b9a9a5aba0a029dd9015eff536dc1da63d16c3be0d7f9e39d6f482b8ce008c3'
|
sha512:
|
||||||
|
'125fb6fc706363753c1c201192024415a23c1098c8e44d5deeaf5448215d83108b9a9a5aba0a029dd9015eff536dc1da63d16c3be0d7f9e39d6f482b8ce008c3'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} }
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let blobs: Blob[] = [];
|
let blobs: Blob[] = [];
|
||||||
let isOpen = false;
|
let isOpen = false;
|
||||||
|
let modalStatus:
|
||||||
|
| 'none'
|
||||||
|
| 'search:mods'
|
||||||
|
| 'search:resourcepacks'
|
||||||
|
| 'search:shaderpacks'
|
||||||
|
| 'edit:simple'
|
||||||
|
| 'edit:advanced'
|
||||||
|
| 'select:modversion' = 'none';
|
||||||
|
|
||||||
|
let searchTerm = '';
|
||||||
|
let searchTermSaved = '';
|
||||||
|
let searchResult: z.infer<typeof searchProjectsResponse>;
|
||||||
|
let searchLoading = false;
|
||||||
|
let searchLoadingMore = false;
|
||||||
|
async function search() {
|
||||||
|
searchLoading = true;
|
||||||
|
searchTermSaved = searchTerm;
|
||||||
|
let facet: string = '';
|
||||||
|
switch (modalStatus) {
|
||||||
|
case 'search:mods':
|
||||||
|
facet = 'project_type:mod';
|
||||||
|
break;
|
||||||
|
case 'search:resourcepacks':
|
||||||
|
facet = 'project_type:resourcepack';
|
||||||
|
break;
|
||||||
|
case 'search:shaderpacks':
|
||||||
|
facet = 'project_type:shader';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const result = await searchProjects({
|
||||||
|
query: searchTerm,
|
||||||
|
facets: [[facet]]
|
||||||
|
});
|
||||||
|
console.log(result);
|
||||||
|
searchResult = result;
|
||||||
|
searchLoading = false;
|
||||||
|
}
|
||||||
|
async function searchMore() {
|
||||||
|
searchLoadingMore = true;
|
||||||
|
const result = await searchProjects({
|
||||||
|
query: searchTermSaved,
|
||||||
|
facets: [['project_type:mod']],
|
||||||
|
offset: searchResult.hits.length
|
||||||
|
});
|
||||||
|
console.log(result);
|
||||||
|
searchResult.hits = searchResult.hits.concat(result.hits);
|
||||||
|
searchLoadingMore = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addMod(mod: z.infer<typeof searchProjectsHit>) {}
|
||||||
|
|
||||||
|
let editPath: string[] = [];
|
||||||
|
function getFileFromPath(path: string[], tree: Folder): File | undefined {
|
||||||
|
const segment = editPath.shift();
|
||||||
|
if (typeof segment == 'undefined') {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
const next = tree.children[segment];
|
||||||
|
if (!next) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (next.type == 'file' && editPath.length == 0) {
|
||||||
|
return next;
|
||||||
|
} else if (next.type == 'file') {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return getFileFromPath(editPath, next);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<TabGroup>
|
<div>
|
||||||
<TabList>
|
<TabGroup>
|
||||||
<Tab>Simple</Tab>
|
<TabList>
|
||||||
<Tab>Advanced</Tab>
|
<Tab>Simple</Tab>
|
||||||
</TabList>
|
<Tab>Advanced</Tab>
|
||||||
<TabPanels>
|
</TabList>
|
||||||
<TabPanel>
|
<TabPanels>
|
||||||
<h1>Mods <button><PlusIcon /></button></h1>
|
<TabPanel class="flex flex-col gap-2">
|
||||||
<FlattenedFileList tree={fileTree.children.mods ?? { type: 'folder', children: {} }} />
|
<div>
|
||||||
<h1>Resource Packs <button><PlusIcon /></button></h1>
|
<div class="flex flex-row gap-2">
|
||||||
<FlattenedFileList
|
<h1 class="text-xl">Mods</h1>
|
||||||
tree={fileTree.children.resourcepacks ?? { type: 'folder', children: {} }}
|
<button
|
||||||
/>
|
class="rounded w-6 h-6 bg-green outline-lavender flex justify-center items-center"
|
||||||
<h1>Shader Packs <button><PlusIcon /></button></h1>
|
on:click={() => (modalStatus = 'search:mods')}
|
||||||
<FlattenedFileList tree={fileTree.children.shaderpacks ?? { type: 'folder', children: {} }} />
|
>
|
||||||
</TabPanel>
|
<PlusIcon class="w-4 h-4 text-black" />
|
||||||
<TabPanel>Content 2</TabPanel>
|
</button>
|
||||||
</TabPanels>
|
</div>
|
||||||
</TabGroup>
|
<FlattenedFileList
|
||||||
|
editFile={(path) => {
|
||||||
|
modalStatus = 'edit:simple';
|
||||||
|
editPath = ['mods'].concat(path);
|
||||||
|
}}
|
||||||
|
tree={fileTree.children.mods?.type == 'folder'
|
||||||
|
? fileTree.children.mods
|
||||||
|
: { type: 'folder', children: {} }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="flex flex-row gap-2">
|
||||||
|
<h1 class="text-xl">Resource Packs</h1>
|
||||||
|
<button
|
||||||
|
class="rounded w-6 h-6 bg-green outline-lavender flex justify-center items-center"
|
||||||
|
on:click={() => (modalStatus = 'search:resourcepacks')}
|
||||||
|
>
|
||||||
|
<PlusIcon class="w-4 h-4 text-black" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<FlattenedFileList
|
||||||
|
editFile={(path) => {
|
||||||
|
modalStatus = 'edit:simple';
|
||||||
|
editPath = ['resourcepacks'].concat(path);
|
||||||
|
}}
|
||||||
|
tree={fileTree.children.resourcepacks?.type == 'folder'
|
||||||
|
? fileTree.children.resourcepacks
|
||||||
|
: { type: 'folder', children: {} }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="flex flex-row gap-2">
|
||||||
|
<h1 class="text-xl">Shader Packs</h1>
|
||||||
|
<button
|
||||||
|
class="rounded w-6 h-6 bg-green outline-lavender flex justify-center items-center"
|
||||||
|
on:click={() => (modalStatus = 'search:shaderpacks')}
|
||||||
|
>
|
||||||
|
<PlusIcon class="w-4 h-4 text-black" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<FlattenedFileList
|
||||||
|
editFile={(path) => {
|
||||||
|
modalStatus = 'edit:simple';
|
||||||
|
editPath = ['shaderpacks'].concat(path);
|
||||||
|
}}
|
||||||
|
tree={fileTree.children.shaderpacks?.type == 'folder'
|
||||||
|
? fileTree.children.shaderpacks
|
||||||
|
: { type: 'folder', children: {} }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel>
|
||||||
|
<FolderIcon class="inline" /> /
|
||||||
|
<FileTree tree={fileTree} />
|
||||||
|
</TabPanel>
|
||||||
|
</TabPanels>
|
||||||
|
</TabGroup>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Dialog open={isOpen} on:close={() => (isOpen = false)}>
|
<Dialog
|
||||||
<DialogOverlay />
|
open={modalStatus != 'none'}
|
||||||
|
on:close={() => (modalStatus = 'none')}
|
||||||
<DialogTitle>Deactivate account</DialogTitle>
|
class="fixed top-0 left-0 w-screen h-screen flex overflow-scroll"
|
||||||
<DialogDescription>This will permanently deactivate your account</DialogDescription>
|
>
|
||||||
|
<DialogOverlay class="fixed top-0 left-0 bg-overlay0 opacity-50 w-screen h-screen" />
|
||||||
<p>
|
<div class="m-auto z-10">
|
||||||
Are you sure you want to deactivate your account? All of your data will be permanently removed.
|
<div class="bg-base rounded-xl shadow-xl p-5 m-2 flex flex-col gap-1">
|
||||||
This action cannot be undone.
|
{#if modalStatus == 'search:mods' || modalStatus == 'search:resourcepacks' || modalStatus == 'search:shaderpacks'}
|
||||||
</p>
|
{#if modalStatus == 'search:mods'}
|
||||||
|
<DialogTitle class="text-xl">Add Mod</DialogTitle>
|
||||||
<button on:click={() => (isOpen = false)}>Deactivate</button>
|
<DialogDescription>Add a mod to your modpack</DialogDescription>
|
||||||
<button on:click={() => (isOpen = false)}>Cancel</button>
|
{:else if modalStatus == 'search:resourcepacks'}
|
||||||
|
<DialogTitle class="text-xl">Add Resource Pack</DialogTitle>
|
||||||
|
<DialogDescription>Add a resource pack to your modpack</DialogDescription>
|
||||||
|
{:else if modalStatus == 'search:shaderpacks'}
|
||||||
|
<DialogTitle class="text-xl">Add Shader</DialogTitle>
|
||||||
|
<DialogDescription>Add a shader to your modpack</DialogDescription>
|
||||||
|
{/if}
|
||||||
|
<div class="flex gap-1">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search"
|
||||||
|
bind:value={searchTerm}
|
||||||
|
on:change={search}
|
||||||
|
class="bg-surface0 rounded grow border-1 border-overlay0 outline-lavender focus:ring-lavender focus:border-lavender"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
on:click={search}
|
||||||
|
class="rounded p-3 bg-surface0 hover:bg-surface1 outline-lavender">Search</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
{#if searchLoading}
|
||||||
|
Searching...
|
||||||
|
{:else if searchResult?.hits.length > 0}
|
||||||
|
<ul class="flex flex-col gap-2 mt-2">
|
||||||
|
{#each searchResult?.hits ?? [] as hit}
|
||||||
|
<li class="flex flex-row rounded bg-surface0 gap-2 p-1 w-md max-w-md min-w-md shadow">
|
||||||
|
<img
|
||||||
|
src={hit.icon_url}
|
||||||
|
alt="{hit.title} icon"
|
||||||
|
class="bg-surface1 h-16 w-16 rounded-lg"
|
||||||
|
/>
|
||||||
|
<div class="grow">
|
||||||
|
<h1 class="text-lg">{hit.title}</h1>
|
||||||
|
<p>{hit.description}</p>
|
||||||
|
<button
|
||||||
|
on:click={() => addMod(hit)}
|
||||||
|
class="float-right bg-green text-crust p-1 rounded m-1"
|
||||||
|
>
|
||||||
|
<PlusIcon class="inline" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
{#if searchResult?.hits.length < searchResult?.total_hits}
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
on:click={searchMore}
|
||||||
|
class="rounded w-full p-3 bg-surface0 hover:bg-surface1 outline-lavender"
|
||||||
|
>
|
||||||
|
Load More
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
{:else if searchLoadingMore}
|
||||||
|
Loading...
|
||||||
|
{/if}
|
||||||
|
</ul>
|
||||||
|
{/if}
|
||||||
|
{:else if modalStatus == 'edit:simple'}
|
||||||
|
{@const file = getFileFromPath(editPath, fileTree)}
|
||||||
|
{#if file && file.inline && file.metafile}
|
||||||
|
{@const parsed = toml.parse(file.content)}
|
||||||
|
{JSON.stringify(parsed)}
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
27
src/routes/create/FileTree.svelte
Normal file
27
src/routes/create/FileTree.svelte
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { Folder } from '$lib/types';
|
||||||
|
import { DownloadIcon, FileIcon, FolderIcon } from 'svelte-feather-icons';
|
||||||
|
import FileTree from './FileTree.svelte';
|
||||||
|
|
||||||
|
export let tree: Folder;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
{#each Object.entries(tree.children) as item}
|
||||||
|
<li class="ml-4">
|
||||||
|
{#if item[1].type == 'file'}
|
||||||
|
{#if item[1].inline && item[1].metafile}
|
||||||
|
<DownloadIcon class="inline" />
|
||||||
|
{item[0]}
|
||||||
|
{:else}
|
||||||
|
<FileIcon class="inline" />
|
||||||
|
{item[0]}
|
||||||
|
{/if}
|
||||||
|
{:else}
|
||||||
|
<FolderIcon class="inline" />
|
||||||
|
{item[0]}
|
||||||
|
<FileTree tree={item[1]} />
|
||||||
|
{/if}
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
|
@ -1,8 +1,16 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { FileTree, File } from '$lib/types';
|
import type { FileTree, File, Folder } from '$lib/types';
|
||||||
import { metafileSchema } from '$lib/packwiz-types';
|
import { metafileSchema } from '$lib/packwiz-types';
|
||||||
import toml from '@ltd/j-toml';
|
import toml from '@ltd/j-toml';
|
||||||
export let tree: FileTree;
|
import {
|
||||||
|
Edit3Icon,
|
||||||
|
ExternalLinkIcon,
|
||||||
|
MonitorIcon,
|
||||||
|
ServerIcon,
|
||||||
|
XIcon
|
||||||
|
} from 'svelte-feather-icons';
|
||||||
|
export let tree: Folder;
|
||||||
|
export let editFile: (path: string[]) => void;
|
||||||
function getFilesRecursively(tree: FileTree): [string[], File][] {
|
function getFilesRecursively(tree: FileTree): [string[], File][] {
|
||||||
if (tree.type == 'file') {
|
if (tree.type == 'file') {
|
||||||
return [[[], tree]];
|
return [[[], tree]];
|
||||||
|
@ -18,23 +26,69 @@
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
function deletePath(path: string[], base: Folder) {
|
||||||
|
if (path.length <= 1) {
|
||||||
|
delete base.children[path[0]];
|
||||||
|
} else {
|
||||||
|
const newBase = base.children[path.shift() ?? ''];
|
||||||
|
if (newBase.type == 'file') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
deletePath(path, newBase);
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ul>
|
<ul class="flex flex-col gap-2 mt-2">
|
||||||
{#each getFilesRecursively(tree) as mod (mod[0])}
|
{#each getFilesRecursively(tree) as mod (mod[0])}
|
||||||
<li>
|
<li class="flex flex-col rounded bg-surface0 gap-2 p-1 max-w-md shadow">
|
||||||
{#if mod[1].inline && mod[1].metafile}
|
{#if mod[1].inline && mod[1].metafile}
|
||||||
{@const parsed = metafileSchema.parse(toml.parse(mod[1].content))}
|
{@const parsed = metafileSchema.parse(toml.parse(mod[1].content))}
|
||||||
<h2>{parsed.name} ({mod[0].join('/')})</h2>
|
<div class="flex flex-row items-end gap-1">
|
||||||
|
<h2 class="text-lg">
|
||||||
|
{parsed.name}
|
||||||
|
</h2>
|
||||||
|
<h3 class="text-sm">
|
||||||
|
({mod[0].join('/')})
|
||||||
|
</h3>
|
||||||
|
<div class="grow" />
|
||||||
|
{#if parsed.side == 'both' || parsed.side == 'client'}
|
||||||
|
<MonitorIcon class="w-4 h-4 shrink-0" />
|
||||||
|
{/if}
|
||||||
|
{#if parsed.side == 'both' || parsed.side == 'server'}
|
||||||
|
<ServerIcon class="w-4 h-4 shrink-0" />
|
||||||
|
{/if}
|
||||||
|
<button
|
||||||
|
on:click={() => editFile(mod[0])}
|
||||||
|
class="rounded w-6 h-6 bg-green outline-lavender flex justify-center items-center shrink-0"
|
||||||
|
>
|
||||||
|
<Edit3Icon class="w-4 h-4 text-crust" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
on:click={() => {
|
||||||
|
deletePath(mod[0], tree);
|
||||||
|
tree = tree;
|
||||||
|
}}
|
||||||
|
class="rounded w-6 h-6 bg-red outline-lavender flex justify-center items-center shrink-0"
|
||||||
|
>
|
||||||
|
<XIcon class="w-4 h-4 text-crust" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
{#if parsed.update.curseforge}
|
{#if parsed.update.curseforge}
|
||||||
<a
|
<a
|
||||||
href="https://minecraft.curseforge.com/projects/{parsed.update.curseforge[
|
href="https://minecraft.curseforge.com/projects/{parsed.update.curseforge[
|
||||||
'project-id'
|
'project-id'
|
||||||
]}">Curseforge</a
|
]}"
|
||||||
>
|
>
|
||||||
|
Curseforge
|
||||||
|
<ExternalLinkIcon class="inline" />
|
||||||
|
</a>
|
||||||
{/if}
|
{/if}
|
||||||
{#if parsed.update.modrinth}
|
{#if parsed.update.modrinth}
|
||||||
<a href="https://modrinth.com/project/{parsed.update.modrinth['mod-id']}">Modrinth</a>
|
<a href="https://modrinth.com/project/{parsed.update.modrinth['mod-id']}">
|
||||||
|
Modrinth
|
||||||
|
<ExternalLinkIcon class="inline" />
|
||||||
|
</a>
|
||||||
{/if}
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
<h2>{mod[0].join('/')}</h2>
|
<h2>{mod[0].join('/')}</h2>
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 633 KiB After Width: | Height: | Size: 285 KiB |
Loading…
Reference in a new issue