aasdfdfas

This commit is contained in:
Skye 2023-03-08 20:55:32 +09:00
parent 5fc85b610f
commit 8e8e82350d
Signed by: me
GPG key ID: 0104BC05F41B77B8
5 changed files with 364 additions and 104 deletions

View file

@ -1,65 +1,76 @@
import { z } from 'zod'; import { z } from 'zod';
const searchProjectsResponse = z.object({ export const searchProjectsHit = z.object({
hits: z.object({ slug: z.string().regex(/^[\w!@$()`.+,"\-']{3,64}$/),
slug: z.string().regex(/^[\w!@$()`.+,"\-']{3,64}$/), title: z.string(),
title: z.string(), description: z.string(),
description: z.string(), categories: z.string().array().default([]),
categories: z.string().array().default([]), client_side: z.enum(['required', 'optional', 'unsupported', 'unknown']),
client_side: z.enum(["required", "optional", "unsupported"]), server_side: z.enum(['required', 'optional', 'unsupported', 'unknown']),
server_side: z.enum(["required", "optional", "unsupported"]), project_type: z.enum(['mod', 'modpack', 'resourcepack', 'shader']),
project_type: z.enum(["mod", "modpack", "resourcepack", "shader"]), downloads: z.number(),
downloads: z.number(), icon_url: z.union([z.string().url(), z.literal('').transform(() => '/favicon.png')]),
icon_url: z.string().url(), color: z.number().nullable(),
color: z.number(), project_id: z.string(),
project_id: z.string(), author: z.string(),
author: z.string(), display_categories: z.string().array().default([]),
display_categories: z.string().array().default([]), versions: z.string().array(),
versions: z.string().array(), follows: z.number(),
follows: z.number(), date_created: z.string().datetime({ offset: true }),
date_created: z.string().datetime({offset: true}), date_modified: z.string().datetime({ offset: true }),
date_modified: z.string().datetime({offset: true}), latest_version: z.string().optional(),
latest_version: z.string().optional(), license: z.string(),
license: z.string(), gallery: z.string().array().default([]),
gallery: z.string().array().default([]), featured_gallery: z.string().nullable()
featured_gallery: z.string().optional(), });
}).array(),
offset: z.number(), export const searchProjectsResponse = z.object({
limit: z.number(), hits: searchProjectsHit.array(),
total_hits: z.number() offset: z.number(),
limit: z.number(),
total_hits: z.number()
}); });
const searchProjectsRequest = z.object({ const searchProjectsRequest = z.object({
query: z.string().optional(), query: z.string().optional(),
facets: z.string().regex(/^(categories|versions|license|project_type):(.+)$/).array().array().default([]), facets: z
index: z.enum(["relevance", "downloads", "follows", "newest", "updated"]).default("relevance"), .string()
offset: z.number().default(0), .regex(/^(categories|versions|license|project_type):(.+)$/)
limit: z.number().default(10), .array()
filters: z.string().optional() .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 { function searchProjectsRequestToUrlParams(
const params = new URLSearchParams(); request: z.infer<typeof searchProjectsRequest>
if (typeof request.query != 'undefined') { ): URLSearchParams {
params.set("query", request.query); const params = new URLSearchParams();
} if (typeof request.query != 'undefined') {
params.set("facets", JSON.stringify(request.facets)); params.set('query', request.query);
params.set("index", request.index); }
params.set("offset", request.offset.toString()); params.set('facets', JSON.stringify(request.facets));
params.set("limit", request.limit.toString()); params.set('index', request.index);
if (typeof request.filters != 'undefined') { params.set('offset', request.offset.toString());
params.set("filters", request.filters); params.set('limit', request.limit.toString());
} if (typeof request.filters != 'undefined') {
return params; params.set('filters', request.filters);
}
return params;
} }
export async function searchProjects(request: z.input<typeof searchProjectsRequest>): Promise<z.infer<typeof searchProjectsResponse>> { export async function searchProjects(
const parsedRequest = searchProjectsRequest.parse(request); request: z.input<typeof searchProjectsRequest>
const params = searchProjectsRequestToUrlParams(parsedRequest); ): Promise<z.infer<typeof searchProjectsResponse>> {
const response = await fetch("https://api.modrinth.com/v2/search?" + params.toString(), { const parsedRequest = searchProjectsRequest.parse(request);
headers: { const params = searchProjectsRequestToUrlParams(parsedRequest);
"User-Agent": "NotModdermore/noversion (+https://git.skye.vg/me/not-moddermore/)" 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()); }
} });
return searchProjectsResponse.parse(await response.json());
}

View file

@ -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,7 +14,14 @@
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: {
@ -70,49 +78,229 @@ version = "2sIhirkG"
}; };
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 searchTerm = '';
let searchTermSaved = '';
let searchResult: z.infer<typeof searchProjectsResponse>;
let searchLoading = false;
let searchLoadingMore = false;
async function search() { async function search() {
// const result = await modrinthClient.searchProjects({ searchLoading = true;
// te searchTermSaved = searchTerm;
// }) let facet: string = '';
// console.log(result) 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 on:click={() => isOpen = true}><PlusIcon /></button></h1> <TabPanel class="flex flex-col gap-2">
<FlattenedFileList tree={fileTree.children.mods?.type == 'folder' <div>
? fileTree.children.mods <div class="flex flex-row gap-2">
: { type: 'folder', children: {} }} /> <h1 class="text-xl">Mods</h1>
<h1>Resource Packs <button><PlusIcon /></button></h1> <button
<FlattenedFileList class="rounded w-6 h-6 bg-green outline-lavender flex justify-center items-center"
tree={fileTree.children.resourcepacks?.type == 'folder' on:click={() => (modalStatus = 'search:mods')}
? fileTree.children.resourcepacks >
: { type: 'folder', children: {} }} <PlusIcon class="w-4 h-4 text-black" />
/> </button>
<h1>Shader Packs <button><PlusIcon /></button></h1> </div>
<FlattenedFileList <FlattenedFileList
tree={fileTree.children.shaderpacks?.type == 'folder' editFile={(path) => {
? fileTree.children.shaderpacks modalStatus = 'edit:simple';
: { type: 'folder', children: {} }} editPath = ['mods'].concat(path);
/> }}
</TabPanel> tree={fileTree.children.mods?.type == 'folder'
<TabPanel>{JSON.stringify(fileTree)}</TabPanel> ? fileTree.children.mods
</TabPanels> : { type: 'folder', children: {} }}
</TabGroup> />
</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)} class="fixed top-0 left-0 w-screen h-screen flex justify-center items-center"> <Dialog
<DialogOverlay class="fixed top-0 left-0 bg-base opacity-50 w-screen h-screen" /> open={modalStatus != 'none'}
<div class="z-10 bg-surface0 rounded-xl shadow-xl p-5"> on:close={() => (modalStatus = 'none')}
<DialogTitle>Add Mod</DialogTitle> class="fixed top-0 left-0 w-screen h-screen flex overflow-scroll"
<DialogDescription>Add a mod to your modpack</DialogDescription> >
<input type="text" placeholder="Search" bind:value={searchTerm}><button on:click={search}>Search</button> <DialogOverlay class="fixed top-0 left-0 bg-overlay0 opacity-50 w-screen h-screen" />
<div class="m-auto z-10">
<div class="bg-base rounded-xl shadow-xl p-5 m-2 flex flex-col gap-1">
{#if modalStatus == 'search:mods' || modalStatus == 'search:resourcepacks' || modalStatus == 'search:shaderpacks'}
{#if modalStatus == 'search:mods'}
<DialogTitle class="text-xl">Add Mod</DialogTitle>
<DialogDescription>Add a mod to your modpack</DialogDescription>
{: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> </div>
</Dialog> </Dialog>

View 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>

View file

@ -2,8 +2,15 @@
import type { FileTree, File, Folder } 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';
import { XIcon } from 'svelte-feather-icons'; import {
Edit3Icon,
ExternalLinkIcon,
MonitorIcon,
ServerIcon,
XIcon
} from 'svelte-feather-icons';
export let tree: Folder; 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]];
@ -32,29 +39,56 @@
} }
</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> <div class="flex flex-row items-end gap-1">
{parsed.name} ({mod[0].join('/')}) <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 <button
on:click={() => { on:click={() => {
deletePath(mod[0], tree); deletePath(mod[0], tree);
tree = tree; tree = tree;
}}><XIcon /></button }}
class="rounded w-6 h-6 bg-red outline-lavender flex justify-center items-center shrink-0"
> >
</h2> <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